From a0bd51f51160ed6f3bb47ba36c883d27c0167066 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:08:59 -0400 Subject: [PATCH 1/8] fix: detect gitlink files when pushing org repos pushOrgRepos only checked for .git directories (-d) but org repos cloned as submodules use .git files (gitlinks). This caused "No org repos found" on every run, silently dropping all customer changes to artist directories. Matches the pattern already used in addOrgSubmodules.ts. Made-with: Cursor --- src/sandboxes/git/pushOrgRepos.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sandboxes/git/pushOrgRepos.ts b/src/sandboxes/git/pushOrgRepos.ts index 969a4f6..c395e8d 100644 --- a/src/sandboxes/git/pushOrgRepos.ts +++ b/src/sandboxes/git/pushOrgRepos.ts @@ -30,7 +30,7 @@ export async function pushOrgRepos( cmd: "sh", args: [ "-c", - `find ${workspaceOrgs} -mindepth 1 -maxdepth 1 -type d -exec test -d {}/.git \\; -print 2>/dev/null | xargs -I{} basename {}`, + `find ${workspaceOrgs} -mindepth 1 -maxdepth 1 -type d '(' -exec test -d {}/.git ';' -o -exec test -f {}/.git ';' ')' -print 2>/dev/null | xargs -I{} basename {}`, ], }); From 692975a1aeb959b767cc33538484e5f69f00e707 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:17:23 -0400 Subject: [PATCH 2/8] test: add regression test for gitlink detection in pushOrgRepos Asserts the find command checks for .git files (test -f), not just .git directories, preventing this bug from recurring. Made-with: Cursor --- src/sandboxes/__tests__/pushOrgRepos.test.ts | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/sandboxes/__tests__/pushOrgRepos.test.ts b/src/sandboxes/__tests__/pushOrgRepos.test.ts index 54231f4..b562adc 100644 --- a/src/sandboxes/__tests__/pushOrgRepos.test.ts +++ b/src/sandboxes/__tests__/pushOrgRepos.test.ts @@ -170,6 +170,33 @@ describe("pushOrgRepos", () => { expect(findCall![0].args[1]).not.toContain("~"); }); + it("detects gitlink files (.git file) not just .git directories", async () => { + const sandbox = createMockSandbox(); + + sandbox.runCommand.mockImplementation(async (opts: any) => { + if (opts.cmd === "sh" && opts.args?.[1]?.includes("find")) { + return { + exitCode: 0, + stdout: async () => "recoup\n", + stderr: async () => "", + }; + } + const finished = { exitCode: 0, stdout: async () => "", stderr: async () => "" }; + if (opts.cmd === "openclaw") { + return { wait: vi.fn().mockResolvedValue(finished) }; + } + return finished; + }); + + await pushOrgRepos(sandbox); + + const findCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => + call[0]?.cmd === "sh" && call[0]?.args?.[1]?.includes("find") + ); + expect(findCall![0].args[1]).toContain("test -f {}/.git"); + }); + it("logs error when OpenClaw fails", async () => { const { logger } = await import("@trigger.dev/sdk/v3"); const sandbox = createMockSandbox(); From 9ad92556c871215d735d86c4bab2bbfe84b55395 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:42:30 -0400 Subject: [PATCH 3/8] fix: skip setup when sandbox already provisioned, remove setup-artist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RECOUP.md check to ensureSetupSandbox — skips skills install and agent runs when sandbox is already set up (~3-7 min saved per run) - Remove runSetupArtistSkill (skill renamed to artist-workspace, and the setup-sandbox skill already handles the handoff) - Replace 3 individual installSkill calls with single installSkills from recoupable/skills repo - Delete old installSkill.ts Made-with: Cursor --- ...allSkill.test.ts => installSkills.test.ts} | 38 +++++++---- src/sandboxes/ensureSetupSandbox.ts | 27 ++++---- src/sandboxes/installSkill.ts | 63 ------------------- src/sandboxes/installSkills.ts | 61 ++++++++++++++++++ src/sandboxes/runSetupArtistSkill.ts | 25 -------- 5 files changed, 102 insertions(+), 112 deletions(-) rename src/sandboxes/__tests__/{installSkill.test.ts => installSkills.test.ts} (54%) delete mode 100644 src/sandboxes/installSkill.ts create mode 100644 src/sandboxes/installSkills.ts delete mode 100644 src/sandboxes/runSetupArtistSkill.ts diff --git a/src/sandboxes/__tests__/installSkill.test.ts b/src/sandboxes/__tests__/installSkills.test.ts similarity index 54% rename from src/sandboxes/__tests__/installSkill.test.ts rename to src/sandboxes/__tests__/installSkills.test.ts index a44f76a..c8aaa4b 100644 --- a/src/sandboxes/__tests__/installSkill.test.ts +++ b/src/sandboxes/__tests__/installSkills.test.ts @@ -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(); @@ -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/"); }); }); diff --git a/src/sandboxes/ensureSetupSandbox.ts b/src/sandboxes/ensureSetupSandbox.ts index f0ad567..62e0548 100644 --- a/src/sandboxes/ensureSetupSandbox.ts +++ b/src/sandboxes/ensureSetupSandbox.ts @@ -1,12 +1,12 @@ 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. + * Skips entirely if artist RECOUP.md files already exist (setup already ran). * * @param sandbox - The Vercel Sandbox instance * @param accountId - The account ID for the sandbox owner @@ -15,11 +15,18 @@ export async function ensureSetupSandbox( sandbox: Sandbox, accountId: string ): Promise { - logStep("Installing skills"); + const check = await sandbox.runCommand({ + cmd: "sh", + args: ["-c", "ls orgs/*/artists/*/RECOUP.md 2>/dev/null | head -1"], + }); + + if (check.exitCode === 0 && ((await check.stdout()) || "").trim()) { + logStep("Sandbox already set up, skipping", false); + return; + } - await installSkill(sandbox, "recoupable/setup-sandbox"); - await installSkill(sandbox, "recoupable/setup-artist"); - await installSkill(sandbox, "recoupable/release-management"); + logStep("Installing skills"); + await installSkills(sandbox, "recoupable/skills"); if (!process.env.RECOUP_API_KEY) { throw new Error("Missing RECOUP_API_KEY environment variable"); @@ -33,8 +40,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); } diff --git a/src/sandboxes/installSkill.ts b/src/sandboxes/installSkill.ts deleted file mode 100644 index 4b974f1..0000000 --- a/src/sandboxes/installSkill.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Sandbox } from "@vercel/sandbox"; -import { logger } from "@trigger.dev/sdk/v3"; - -/** - * Installs a skills.sh skill into the OpenClaw workspace skills directory. - * - * skills.sh installs to .agents/skills/ — this function copies the result - * to ~/.openclaw/workspace/skills/ so OpenClaw discovers it natively. - * - * @param sandbox - The Vercel Sandbox instance - * @param skill - The skills.sh skill identifier (e.g. "recoupable/setup-sandbox") - */ -export async function installSkill( - sandbox: Sandbox, - skill: string -): Promise { - const skillName = skill.split("/").pop()!; - - logger.log("Installing skill via skills.sh", { skill }); - - const install = await sandbox.runCommand({ - cmd: "npx", - args: ["skills", "add", skill, "-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("Skill install failed, continuing without it", { - skill, - stderr: installStderr, - }); - return; - } - - // Copy from skills.sh location to OpenClaw workspace skills directory - const copy = await sandbox.runCommand({ - cmd: "sh", - args: [ - "-c", - `mkdir -p ~/.openclaw/workspace/skills && rm -rf ~/.openclaw/workspace/skills/${skillName} && cp -r .agents/skills/${skillName} ~/.openclaw/workspace/skills/${skillName}`, - ], - }); - - const copyStderr = (await copy.stderr()) || ""; - - if (copy.exitCode !== 0) { - logger.warn("Failed to copy skill to OpenClaw workspace, continuing without it", { - skill, - stderr: copyStderr, - }); - return; - } - - logger.log("Skill installed to OpenClaw workspace", { skillName }); -} diff --git a/src/sandboxes/installSkills.ts b/src/sandboxes/installSkills.ts new file mode 100644 index 0000000..15fc88a --- /dev/null +++ b/src/sandboxes/installSkills.ts @@ -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 -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 { + 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; + } + + logger.log("Skills installed to OpenClaw workspace", { source }); +} diff --git a/src/sandboxes/runSetupArtistSkill.ts b/src/sandboxes/runSetupArtistSkill.ts deleted file mode 100644 index b5956f9..0000000 --- a/src/sandboxes/runSetupArtistSkill.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Sandbox } from "@vercel/sandbox"; -import { runOpenClawAgent } from "./runOpenClawAgent"; - -/** - * Runs the /setup-artist skill via OpenClaw for each artist - * folder that exists under orgs/. - * - * @param sandbox - The Vercel Sandbox instance - * @param env - Environment variables for the agent - */ -export async function runSetupArtistSkill( - sandbox: Sandbox, - env: Record -): Promise { - const result = await runOpenClawAgent(sandbox, { - label: "Running setup-artist skill", - message: - "Run the /setup-artist skill for EACH artist folder that exists under orgs/.\n\nRECOUP_API_KEY and RECOUP_ACCOUNT_ID are available as environment variables.", - env, - }); - - if (result.exitCode !== 0) { - throw new Error("Failed to set up artists via OpenClaw"); - } -} From 74edc58a251ad132496b532cf04c605131b191d6 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:09:36 -0400 Subject: [PATCH 4/8] chore: trigger CI Made-with: Cursor From f7e74b383802ab6a92a15e9df3d9de7014325fd8 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:10:27 -0400 Subject: [PATCH 5/8] fix: use absolute path for setup skip check The RECOUP.md check used a relative `orgs/*/artists/*/RECOUP.md` glob which only works if the sandbox cwd is the OpenClaw workspace. The sandbox cwd is /vercel/sandbox, so the check never matched and setup re-ran every time (~7 min wasted). Use getSandboxHomeDir to build the correct absolute path. Made-with: Cursor --- src/sandboxes/ensureSetupSandbox.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sandboxes/ensureSetupSandbox.ts b/src/sandboxes/ensureSetupSandbox.ts index 62e0548..c1cb358 100644 --- a/src/sandboxes/ensureSetupSandbox.ts +++ b/src/sandboxes/ensureSetupSandbox.ts @@ -2,6 +2,7 @@ import type { Sandbox } from "@vercel/sandbox"; import { installSkills } from "./installSkills"; import { runSetupSandboxSkill } from "./runSetupSandboxSkill"; import { logStep } from "./logStep"; +import { getSandboxHomeDir } from "./getSandboxHomeDir"; /** * Ensures the sandbox has the org/artist folder structure set up. @@ -15,9 +16,12 @@ export async function ensureSetupSandbox( sandbox: Sandbox, accountId: string ): Promise { + const homeDir = await getSandboxHomeDir(sandbox); + const orgsPath = `${homeDir}/.openclaw/workspace/orgs`; + const check = await sandbox.runCommand({ cmd: "sh", - args: ["-c", "ls orgs/*/artists/*/RECOUP.md 2>/dev/null | head -1"], + args: ["-c", `ls ${orgsPath}/*/artists/*/RECOUP.md 2>/dev/null | head -1`], }); if (check.exitCode === 0 && ((await check.stdout()) || "").trim()) { From d3e0ad1115baf28e58e1652a36f7a3133daaca99 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:40:06 -0400 Subject: [PATCH 6/8] debug: add logging to pushOrgRepos to diagnose find failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Staging test shows org repos still not found even with gitlink fix. Adding debug output to see what's actually on disk at the workspace orgs path — ls -la, find .git entries, and resolved HOME/CWD. Made-with: Cursor --- src/sandboxes/__tests__/pushOrgRepos.test.ts | 2 +- src/sandboxes/git/pushOrgRepos.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/sandboxes/__tests__/pushOrgRepos.test.ts b/src/sandboxes/__tests__/pushOrgRepos.test.ts index b562adc..446d87f 100644 --- a/src/sandboxes/__tests__/pushOrgRepos.test.ts +++ b/src/sandboxes/__tests__/pushOrgRepos.test.ts @@ -192,7 +192,7 @@ describe("pushOrgRepos", () => { const findCall = sandbox.runCommand.mock.calls.find( (call: any[]) => - call[0]?.cmd === "sh" && call[0]?.args?.[1]?.includes("find") + call[0]?.cmd === "sh" && call[0]?.args?.[1]?.includes("find") && call[0]?.args?.[1]?.includes("mindepth") ); expect(findCall![0].args[1]).toContain("test -f {}/.git"); }); diff --git a/src/sandboxes/git/pushOrgRepos.ts b/src/sandboxes/git/pushOrgRepos.ts index c395e8d..01b58d6 100644 --- a/src/sandboxes/git/pushOrgRepos.ts +++ b/src/sandboxes/git/pushOrgRepos.ts @@ -25,7 +25,17 @@ export async function pushOrgRepos( const homeDir = await getSandboxHomeDir(sandbox); const workspaceOrgs = `${homeDir}/.openclaw/workspace/orgs`; - // Find org directories that are git repos + const debugResult = await sandbox.runCommand({ + cmd: "sh", + args: ["-c", `echo "HOME=$HOME CWD=$(pwd)"; ls -la ${workspaceOrgs}/ 2>&1 || echo "DIR_NOT_FOUND"; find ${workspaceOrgs} -maxdepth 2 -name '.git' 2>/dev/null || echo "NO_GIT_ENTRIES"`], + }); + logger.log("pushOrgRepos debug", { + homeDir, + workspaceOrgs, + debug: (await debugResult.stdout()) || "", + debugErr: (await debugResult.stderr()) || "", + }); + const findResult = await sandbox.runCommand({ cmd: "sh", args: [ From d6d45d2cd2a5fc536388fc084fea97c35404b66f Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:50:22 -0400 Subject: [PATCH 7/8] fix: clone org repos deterministically instead of via AI agent The OpenClaw agent was told to clone org repos but didn't reliably handle directories that exist without .git entries (common after snapshot restore). Replaced the AI-delegated clone with deterministic sandbox.runCommand calls that check for .git, pull if present, or rm + clone fresh if missing. Also removed debug logging from pushOrgRepos. Made-with: Cursor --- .../__tests__/ensureOrgRepos.test.ts | 99 ++++++++++--------- src/sandboxes/ensureOrgRepos.ts | 82 +++++++++------ src/sandboxes/git/pushOrgRepos.ts | 11 --- 3 files changed, 100 insertions(+), 92 deletions(-) diff --git a/src/sandboxes/__tests__/ensureOrgRepos.test.ts b/src/sandboxes/__tests__/ensureOrgRepos.test.ts index 645d261..89cd84e 100644 --- a/src/sandboxes/__tests__/ensureOrgRepos.test.ts +++ b/src/sandboxes/__tests__/ensureOrgRepos.test.ts @@ -19,16 +19,12 @@ vi.mock("../../github/createOrgGithubRepo", () => ({ const { ensureOrgRepos } = await import("../ensureOrgRepos"); function createMockSandbox() { - const runCommand = vi.fn().mockImplementation((opts: any) => { - const finished = { + const runCommand = vi.fn().mockImplementation(() => { + return Promise.resolve({ exitCode: 0, - stdout: async () => "", + stdout: async () => "/root", stderr: async () => "", - }; - if (opts?.cmd === "openclaw") { - return Promise.resolve({ wait: vi.fn().mockResolvedValue(finished) }); - } - return Promise.resolve(finished); + }); }); return { runCommand } as any; @@ -85,7 +81,7 @@ describe("ensureOrgRepos", () => { expect(mockCreateOrgGithubRepo).toHaveBeenCalledWith("Org Two", "org-2"); }); - it("runs an openclaw command to clone org repos into workspace", async () => { + it("clones org repos deterministically via git clone", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Test Org" }, ]); @@ -95,28 +91,24 @@ describe("ensureOrgRepos", () => { ); const sandbox = createMockSandbox(); + // git check returns 1 = no .git found, needs clone + sandbox.runCommand.mockImplementation((opts: any) => { + if (opts?.args?.[1]?.includes("test -d") || opts?.args?.[1]?.includes("test -f")) { + return Promise.resolve({ exitCode: 1, stdout: async () => "", stderr: async () => "" }); + } + return Promise.resolve({ exitCode: 0, stdout: async () => "/root", stderr: async () => "" }); + }); await ensureOrgRepos(sandbox, "account-1"); - // Should have called openclaw agent with a message about cloning - const openclawCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => call[0]?.cmd === "openclaw" - ); - expect(openclawCall).toBeDefined(); - - // The openclaw args should include the repo URL - const args = openclawCall![0].args; - const message = args.find( - (a: string, i: number) => args[i - 1] === "--message" + const cloneCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => + call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" ); - expect(message).toContain("org-test-org-org-1"); - - // GITHUB_TOKEN is injected into openclaw.json by setupOpenClaw, - // not passed via env on this runCommand call. - expect(openclawCall![0].env).toBeUndefined(); + expect(cloneCall).toBeDefined(); + expect(cloneCall![0].args[1]).toContain("org-test-org-org-1"); }); - // Should NOT use git submodule add — that's the old approach it("does not use git submodule add", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Test Org" }, @@ -138,7 +130,7 @@ describe("ensureOrgRepos", () => { expect(submoduleCall).toBeUndefined(); }); - it("continues creating repos when one fails", async () => { + it("continues when one repo creation fails", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Failing Org" }, { organizationId: "org-2", organizationName: "Working Org" }, @@ -151,25 +143,25 @@ describe("ensureOrgRepos", () => { ); const sandbox = createMockSandbox(); + sandbox.runCommand.mockImplementation((opts: any) => { + if (opts?.args?.[1]?.includes("test -d") || opts?.args?.[1]?.includes("test -f")) { + return Promise.resolve({ exitCode: 1, stdout: async () => "", stderr: async () => "" }); + } + return Promise.resolve({ exitCode: 0, stdout: async () => "/root", stderr: async () => "" }); + }); await ensureOrgRepos(sandbox, "account-1"); expect(mockCreateOrgGithubRepo).toHaveBeenCalledTimes(2); - // Openclaw should still be called with the one that succeeded - const openclawCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => call[0]?.cmd === "openclaw" + const cloneCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => + call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" ); - expect(openclawCall).toBeDefined(); + expect(cloneCall).toBeDefined(); }); - /** - * Regression: submodules use a .git FILE (gitlink), not a .git DIRECTORY. - * If the clone prompt only checks for a .git directory, it treats - * submodule dirs as "not a git repo", removes them, and clones fresh — - * destroying the submodule registration every re-run. - */ - it("instructs OpenClaw to detect .git files (gitlinks) for submodules", async () => { + it("pulls instead of cloning when .git exists", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Test Org" }, ]); @@ -179,22 +171,30 @@ describe("ensureOrgRepos", () => { ); const sandbox = createMockSandbox(); + // git check returns 0 = .git found + sandbox.runCommand.mockImplementation((opts: any) => { + if (opts?.args?.[1]?.includes("test -d") || opts?.args?.[1]?.includes("test -f")) { + return Promise.resolve({ exitCode: 0, stdout: async () => "", stderr: async () => "" }); + } + return Promise.resolve({ exitCode: 0, stdout: async () => "/root", stderr: async () => "" }); + }); await ensureOrgRepos(sandbox, "account-1"); - const openclawCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => call[0]?.cmd === "openclaw" - ); - const args = openclawCall![0].args; - const message = args.find( - (a: string, i: number) => args[i - 1] === "--message" + const pullCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => + call[0]?.cmd === "git" && call[0]?.args?.includes("pull") ); + expect(pullCall).toBeDefined(); - // Message must handle .git as a file (submodule gitlink), not just directory - expect(message).toContain(".git file"); + const cloneCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => + call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" + ); + expect(cloneCall).toBeUndefined(); }); - it("skips openclaw when all repo creations fail", async () => { + it("skips clone when all repo creations fail", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Failing Org" }, ]); @@ -205,9 +205,10 @@ describe("ensureOrgRepos", () => { await ensureOrgRepos(sandbox, "account-1"); - const openclawCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => call[0]?.cmd === "openclaw" + const cloneCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => + call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" ); - expect(openclawCall).toBeUndefined(); + expect(cloneCall).toBeUndefined(); }); }); diff --git a/src/sandboxes/ensureOrgRepos.ts b/src/sandboxes/ensureOrgRepos.ts index 512c4c7..51af75c 100644 --- a/src/sandboxes/ensureOrgRepos.ts +++ b/src/sandboxes/ensureOrgRepos.ts @@ -3,12 +3,16 @@ import { logger } from "@trigger.dev/sdk/v3"; import { getAccountOrgs } from "../recoup/getAccountOrgs"; import { createOrgGithubRepo } from "../github/createOrgGithubRepo"; import { sanitizeRepoName } from "../github/sanitizeRepoName"; -import { runOpenClawAgent } from "./runOpenClawAgent"; import { logStep } from "./logStep"; +import { getSandboxHomeDir } from "./getSandboxHomeDir"; +import { getGitHubAuthPrefix } from "./getGitHubAuthPrefix"; /** * Ensures each of the account's organizations has a GitHub repo and - * tells OpenClaw to clone them into `orgs/` in the workspace. + * is cloned into orgs/ in the OpenClaw workspace. + * + * Clones deterministically via sandbox.runCommand — no AI agent delegation. + * If a directory exists without .git, removes it and clones fresh. * * Must be called AFTER `setupOpenClaw` (so OpenClaw is available) and * BEFORE `ensureSetupSandbox` (so skills write into existing org dirs). @@ -20,9 +24,9 @@ export async function ensureOrgRepos( sandbox: Sandbox, accountId: string ): Promise { - const githubToken = process.env.GITHUB_TOKEN; + const authPrefix = getGitHubAuthPrefix(); - if (!githubToken) { + if (!authPrefix) { logger.error("Missing GITHUB_TOKEN for org repos"); return; } @@ -37,7 +41,6 @@ export async function ensureOrgRepos( logStep("Setting up org repos"); - // Create GitHub repos for each org and collect URLs const orgRepos: Array<{ name: string; url: string }> = []; for (const org of orgs) { @@ -65,33 +68,48 @@ export async function ensureOrgRepos( return; } - // Build the prompt for OpenClaw to clone the repos - const repoList = orgRepos - .map((r) => `- "${r.name}" → ${r.url}`) - .join("\n"); - - const message = [ - "Clone the following GitHub repositories into orgs/ in your workspace.", - "Use the GITHUB_TOKEN environment variable for authentication.", - "Replace https://github.com/ with https://x-access-token:$GITHUB_TOKEN@github.com/ in the clone URL.", - "", - "For each repo, check orgs/{name}:", - "- If it has a .git directory OR a .git file (submodule gitlink), it's already a git repo — run: git -C orgs/{name} pull origin main", - "- If it exists but has neither a .git directory nor a .git file, remove it and clone fresh.", - "- If it does not exist, clone the repo.", - "", - "IMPORTANT: Submodules use a .git file (gitlink), not a .git directory.", - "Always check for BOTH: [ -d orgs/{name}/.git ] || [ -f orgs/{name}/.git ]", - "", - repoList, - ].join("\n"); - - // GITHUB_TOKEN and RECOUP_API_KEY are injected into openclaw.json - // by setupOpenClaw — no need to pass them via env here. - await runOpenClawAgent(sandbox, { - label: "Cloning org repos", - message, - }); + const homeDir = await getSandboxHomeDir(sandbox); + const orgsDir = `${homeDir}/.openclaw/workspace/orgs`; + + await sandbox.runCommand({ cmd: "mkdir", args: ["-p", orgsDir] }); + + for (const repo of orgRepos) { + const repoDir = `${orgsDir}/${repo.name}`; + const authedUrl = repo.url.replace("https://github.com/", authPrefix); + + const gitCheck = await sandbox.runCommand({ + cmd: "sh", + args: ["-c", `test -d ${repoDir}/.git || test -f ${repoDir}/.git`], + }); + + if (gitCheck.exitCode === 0) { + logger.log("Org repo already cloned, pulling latest", { name: repo.name }); + await sandbox.runCommand({ + cmd: "git", + args: ["-C", repoDir, "pull", "origin", "main"], + }); + continue; + } + + // Directory exists but is not a git repo — remove and clone fresh + await sandbox.runCommand({ + cmd: "sh", + args: ["-c", `rm -rf ${repoDir}`], + }); + + logger.log("Cloning org repo", { name: repo.name }); + const clone = await sandbox.runCommand({ + cmd: "git", + args: ["clone", authedUrl, repoDir], + }); + + if (clone.exitCode !== 0) { + logger.error("Failed to clone org repo", { + name: repo.name, + stderr: (await clone.stderr()) || "", + }); + } + } logStep("Org repo setup complete"); } diff --git a/src/sandboxes/git/pushOrgRepos.ts b/src/sandboxes/git/pushOrgRepos.ts index 01b58d6..dd2f6c4 100644 --- a/src/sandboxes/git/pushOrgRepos.ts +++ b/src/sandboxes/git/pushOrgRepos.ts @@ -25,17 +25,6 @@ export async function pushOrgRepos( const homeDir = await getSandboxHomeDir(sandbox); const workspaceOrgs = `${homeDir}/.openclaw/workspace/orgs`; - const debugResult = await sandbox.runCommand({ - cmd: "sh", - args: ["-c", `echo "HOME=$HOME CWD=$(pwd)"; ls -la ${workspaceOrgs}/ 2>&1 || echo "DIR_NOT_FOUND"; find ${workspaceOrgs} -maxdepth 2 -name '.git' 2>/dev/null || echo "NO_GIT_ENTRIES"`], - }); - logger.log("pushOrgRepos debug", { - homeDir, - workspaceOrgs, - debug: (await debugResult.stdout()) || "", - debugErr: (await debugResult.stderr()) || "", - }); - const findResult = await sandbox.runCommand({ cmd: "sh", args: [ From 274405e278df55b55b6414050668a9427fcdbb8e Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:42:12 -0400 Subject: [PATCH 8/8] refactor: scope PR to skills consolidation only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert git/org repo fixes (gitlink detection, deterministic clone, setup skip check) to keep this PR focused on updating skills in task runs. Remaining changes: - installSkill → installSkills (one-shot from recoupable/skills) - Remove runSetupArtistSkill (setup-sandbox skill handles it) - ensureSetupSandbox uses installSkills, drops setup-artist call Made-with: Cursor --- .../__tests__/ensureOrgRepos.test.ts | 99 +++++++++---------- src/sandboxes/__tests__/pushOrgRepos.test.ts | 27 ----- src/sandboxes/ensureOrgRepos.ts | 82 ++++++--------- src/sandboxes/ensureSetupSandbox.ts | 15 --- src/sandboxes/git/pushOrgRepos.ts | 3 +- 5 files changed, 83 insertions(+), 143 deletions(-) diff --git a/src/sandboxes/__tests__/ensureOrgRepos.test.ts b/src/sandboxes/__tests__/ensureOrgRepos.test.ts index 89cd84e..645d261 100644 --- a/src/sandboxes/__tests__/ensureOrgRepos.test.ts +++ b/src/sandboxes/__tests__/ensureOrgRepos.test.ts @@ -19,12 +19,16 @@ vi.mock("../../github/createOrgGithubRepo", () => ({ const { ensureOrgRepos } = await import("../ensureOrgRepos"); function createMockSandbox() { - const runCommand = vi.fn().mockImplementation(() => { - return Promise.resolve({ + const runCommand = vi.fn().mockImplementation((opts: any) => { + const finished = { exitCode: 0, - stdout: async () => "/root", + stdout: async () => "", stderr: async () => "", - }); + }; + if (opts?.cmd === "openclaw") { + return Promise.resolve({ wait: vi.fn().mockResolvedValue(finished) }); + } + return Promise.resolve(finished); }); return { runCommand } as any; @@ -81,7 +85,7 @@ describe("ensureOrgRepos", () => { expect(mockCreateOrgGithubRepo).toHaveBeenCalledWith("Org Two", "org-2"); }); - it("clones org repos deterministically via git clone", async () => { + it("runs an openclaw command to clone org repos into workspace", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Test Org" }, ]); @@ -91,24 +95,28 @@ describe("ensureOrgRepos", () => { ); const sandbox = createMockSandbox(); - // git check returns 1 = no .git found, needs clone - sandbox.runCommand.mockImplementation((opts: any) => { - if (opts?.args?.[1]?.includes("test -d") || opts?.args?.[1]?.includes("test -f")) { - return Promise.resolve({ exitCode: 1, stdout: async () => "", stderr: async () => "" }); - } - return Promise.resolve({ exitCode: 0, stdout: async () => "/root", stderr: async () => "" }); - }); await ensureOrgRepos(sandbox, "account-1"); - const cloneCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => - call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" + // Should have called openclaw agent with a message about cloning + const openclawCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => call[0]?.cmd === "openclaw" + ); + expect(openclawCall).toBeDefined(); + + // The openclaw args should include the repo URL + const args = openclawCall![0].args; + const message = args.find( + (a: string, i: number) => args[i - 1] === "--message" ); - expect(cloneCall).toBeDefined(); - expect(cloneCall![0].args[1]).toContain("org-test-org-org-1"); + expect(message).toContain("org-test-org-org-1"); + + // GITHUB_TOKEN is injected into openclaw.json by setupOpenClaw, + // not passed via env on this runCommand call. + expect(openclawCall![0].env).toBeUndefined(); }); + // Should NOT use git submodule add — that's the old approach it("does not use git submodule add", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Test Org" }, @@ -130,7 +138,7 @@ describe("ensureOrgRepos", () => { expect(submoduleCall).toBeUndefined(); }); - it("continues when one repo creation fails", async () => { + it("continues creating repos when one fails", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Failing Org" }, { organizationId: "org-2", organizationName: "Working Org" }, @@ -143,25 +151,25 @@ describe("ensureOrgRepos", () => { ); const sandbox = createMockSandbox(); - sandbox.runCommand.mockImplementation((opts: any) => { - if (opts?.args?.[1]?.includes("test -d") || opts?.args?.[1]?.includes("test -f")) { - return Promise.resolve({ exitCode: 1, stdout: async () => "", stderr: async () => "" }); - } - return Promise.resolve({ exitCode: 0, stdout: async () => "/root", stderr: async () => "" }); - }); await ensureOrgRepos(sandbox, "account-1"); expect(mockCreateOrgGithubRepo).toHaveBeenCalledTimes(2); - const cloneCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => - call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" + // Openclaw should still be called with the one that succeeded + const openclawCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => call[0]?.cmd === "openclaw" ); - expect(cloneCall).toBeDefined(); + expect(openclawCall).toBeDefined(); }); - it("pulls instead of cloning when .git exists", async () => { + /** + * Regression: submodules use a .git FILE (gitlink), not a .git DIRECTORY. + * If the clone prompt only checks for a .git directory, it treats + * submodule dirs as "not a git repo", removes them, and clones fresh — + * destroying the submodule registration every re-run. + */ + it("instructs OpenClaw to detect .git files (gitlinks) for submodules", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Test Org" }, ]); @@ -171,30 +179,22 @@ describe("ensureOrgRepos", () => { ); const sandbox = createMockSandbox(); - // git check returns 0 = .git found - sandbox.runCommand.mockImplementation((opts: any) => { - if (opts?.args?.[1]?.includes("test -d") || opts?.args?.[1]?.includes("test -f")) { - return Promise.resolve({ exitCode: 0, stdout: async () => "", stderr: async () => "" }); - } - return Promise.resolve({ exitCode: 0, stdout: async () => "/root", stderr: async () => "" }); - }); await ensureOrgRepos(sandbox, "account-1"); - const pullCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => - call[0]?.cmd === "git" && call[0]?.args?.includes("pull") + const openclawCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => call[0]?.cmd === "openclaw" ); - expect(pullCall).toBeDefined(); - - const cloneCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => - call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" + const args = openclawCall![0].args; + const message = args.find( + (a: string, i: number) => args[i - 1] === "--message" ); - expect(cloneCall).toBeUndefined(); + + // Message must handle .git as a file (submodule gitlink), not just directory + expect(message).toContain(".git file"); }); - it("skips clone when all repo creations fail", async () => { + it("skips openclaw when all repo creations fail", async () => { mockGetAccountOrgs.mockResolvedValueOnce([ { organizationId: "org-1", organizationName: "Failing Org" }, ]); @@ -205,10 +205,9 @@ describe("ensureOrgRepos", () => { await ensureOrgRepos(sandbox, "account-1"); - const cloneCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => - call[0]?.cmd === "git" && call[0]?.args?.[0] === "clone" + const openclawCall = sandbox.runCommand.mock.calls.find( + (call: any[]) => call[0]?.cmd === "openclaw" ); - expect(cloneCall).toBeUndefined(); + expect(openclawCall).toBeUndefined(); }); }); diff --git a/src/sandboxes/__tests__/pushOrgRepos.test.ts b/src/sandboxes/__tests__/pushOrgRepos.test.ts index 446d87f..54231f4 100644 --- a/src/sandboxes/__tests__/pushOrgRepos.test.ts +++ b/src/sandboxes/__tests__/pushOrgRepos.test.ts @@ -170,33 +170,6 @@ describe("pushOrgRepos", () => { expect(findCall![0].args[1]).not.toContain("~"); }); - it("detects gitlink files (.git file) not just .git directories", async () => { - const sandbox = createMockSandbox(); - - sandbox.runCommand.mockImplementation(async (opts: any) => { - if (opts.cmd === "sh" && opts.args?.[1]?.includes("find")) { - return { - exitCode: 0, - stdout: async () => "recoup\n", - stderr: async () => "", - }; - } - const finished = { exitCode: 0, stdout: async () => "", stderr: async () => "" }; - if (opts.cmd === "openclaw") { - return { wait: vi.fn().mockResolvedValue(finished) }; - } - return finished; - }); - - await pushOrgRepos(sandbox); - - const findCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => - call[0]?.cmd === "sh" && call[0]?.args?.[1]?.includes("find") && call[0]?.args?.[1]?.includes("mindepth") - ); - expect(findCall![0].args[1]).toContain("test -f {}/.git"); - }); - it("logs error when OpenClaw fails", async () => { const { logger } = await import("@trigger.dev/sdk/v3"); const sandbox = createMockSandbox(); diff --git a/src/sandboxes/ensureOrgRepos.ts b/src/sandboxes/ensureOrgRepos.ts index 51af75c..512c4c7 100644 --- a/src/sandboxes/ensureOrgRepos.ts +++ b/src/sandboxes/ensureOrgRepos.ts @@ -3,16 +3,12 @@ import { logger } from "@trigger.dev/sdk/v3"; import { getAccountOrgs } from "../recoup/getAccountOrgs"; import { createOrgGithubRepo } from "../github/createOrgGithubRepo"; import { sanitizeRepoName } from "../github/sanitizeRepoName"; +import { runOpenClawAgent } from "./runOpenClawAgent"; import { logStep } from "./logStep"; -import { getSandboxHomeDir } from "./getSandboxHomeDir"; -import { getGitHubAuthPrefix } from "./getGitHubAuthPrefix"; /** * Ensures each of the account's organizations has a GitHub repo and - * is cloned into orgs/ in the OpenClaw workspace. - * - * Clones deterministically via sandbox.runCommand — no AI agent delegation. - * If a directory exists without .git, removes it and clones fresh. + * tells OpenClaw to clone them into `orgs/` in the workspace. * * Must be called AFTER `setupOpenClaw` (so OpenClaw is available) and * BEFORE `ensureSetupSandbox` (so skills write into existing org dirs). @@ -24,9 +20,9 @@ export async function ensureOrgRepos( sandbox: Sandbox, accountId: string ): Promise { - const authPrefix = getGitHubAuthPrefix(); + const githubToken = process.env.GITHUB_TOKEN; - if (!authPrefix) { + if (!githubToken) { logger.error("Missing GITHUB_TOKEN for org repos"); return; } @@ -41,6 +37,7 @@ export async function ensureOrgRepos( logStep("Setting up org repos"); + // Create GitHub repos for each org and collect URLs const orgRepos: Array<{ name: string; url: string }> = []; for (const org of orgs) { @@ -68,48 +65,33 @@ export async function ensureOrgRepos( return; } - const homeDir = await getSandboxHomeDir(sandbox); - const orgsDir = `${homeDir}/.openclaw/workspace/orgs`; - - await sandbox.runCommand({ cmd: "mkdir", args: ["-p", orgsDir] }); - - for (const repo of orgRepos) { - const repoDir = `${orgsDir}/${repo.name}`; - const authedUrl = repo.url.replace("https://github.com/", authPrefix); - - const gitCheck = await sandbox.runCommand({ - cmd: "sh", - args: ["-c", `test -d ${repoDir}/.git || test -f ${repoDir}/.git`], - }); - - if (gitCheck.exitCode === 0) { - logger.log("Org repo already cloned, pulling latest", { name: repo.name }); - await sandbox.runCommand({ - cmd: "git", - args: ["-C", repoDir, "pull", "origin", "main"], - }); - continue; - } - - // Directory exists but is not a git repo — remove and clone fresh - await sandbox.runCommand({ - cmd: "sh", - args: ["-c", `rm -rf ${repoDir}`], - }); - - logger.log("Cloning org repo", { name: repo.name }); - const clone = await sandbox.runCommand({ - cmd: "git", - args: ["clone", authedUrl, repoDir], - }); - - if (clone.exitCode !== 0) { - logger.error("Failed to clone org repo", { - name: repo.name, - stderr: (await clone.stderr()) || "", - }); - } - } + // Build the prompt for OpenClaw to clone the repos + const repoList = orgRepos + .map((r) => `- "${r.name}" → ${r.url}`) + .join("\n"); + + const message = [ + "Clone the following GitHub repositories into orgs/ in your workspace.", + "Use the GITHUB_TOKEN environment variable for authentication.", + "Replace https://github.com/ with https://x-access-token:$GITHUB_TOKEN@github.com/ in the clone URL.", + "", + "For each repo, check orgs/{name}:", + "- If it has a .git directory OR a .git file (submodule gitlink), it's already a git repo — run: git -C orgs/{name} pull origin main", + "- If it exists but has neither a .git directory nor a .git file, remove it and clone fresh.", + "- If it does not exist, clone the repo.", + "", + "IMPORTANT: Submodules use a .git file (gitlink), not a .git directory.", + "Always check for BOTH: [ -d orgs/{name}/.git ] || [ -f orgs/{name}/.git ]", + "", + repoList, + ].join("\n"); + + // GITHUB_TOKEN and RECOUP_API_KEY are injected into openclaw.json + // by setupOpenClaw — no need to pass them via env here. + await runOpenClawAgent(sandbox, { + label: "Cloning org repos", + message, + }); logStep("Org repo setup complete"); } diff --git a/src/sandboxes/ensureSetupSandbox.ts b/src/sandboxes/ensureSetupSandbox.ts index c1cb358..94cf90b 100644 --- a/src/sandboxes/ensureSetupSandbox.ts +++ b/src/sandboxes/ensureSetupSandbox.ts @@ -2,12 +2,10 @@ import type { Sandbox } from "@vercel/sandbox"; import { installSkills } from "./installSkills"; import { runSetupSandboxSkill } from "./runSetupSandboxSkill"; import { logStep } from "./logStep"; -import { getSandboxHomeDir } from "./getSandboxHomeDir"; /** * Ensures the sandbox has the org/artist folder structure set up. * Installs skills from recoupable/skills, then runs setup-sandbox. - * Skips entirely if artist RECOUP.md files already exist (setup already ran). * * @param sandbox - The Vercel Sandbox instance * @param accountId - The account ID for the sandbox owner @@ -16,19 +14,6 @@ export async function ensureSetupSandbox( sandbox: Sandbox, accountId: string ): Promise { - const homeDir = await getSandboxHomeDir(sandbox); - const orgsPath = `${homeDir}/.openclaw/workspace/orgs`; - - const check = await sandbox.runCommand({ - cmd: "sh", - args: ["-c", `ls ${orgsPath}/*/artists/*/RECOUP.md 2>/dev/null | head -1`], - }); - - if (check.exitCode === 0 && ((await check.stdout()) || "").trim()) { - logStep("Sandbox already set up, skipping", false); - return; - } - logStep("Installing skills"); await installSkills(sandbox, "recoupable/skills"); diff --git a/src/sandboxes/git/pushOrgRepos.ts b/src/sandboxes/git/pushOrgRepos.ts index dd2f6c4..969a4f6 100644 --- a/src/sandboxes/git/pushOrgRepos.ts +++ b/src/sandboxes/git/pushOrgRepos.ts @@ -25,11 +25,12 @@ export async function pushOrgRepos( const homeDir = await getSandboxHomeDir(sandbox); const workspaceOrgs = `${homeDir}/.openclaw/workspace/orgs`; + // Find org directories that are git repos const findResult = await sandbox.runCommand({ cmd: "sh", args: [ "-c", - `find ${workspaceOrgs} -mindepth 1 -maxdepth 1 -type d '(' -exec test -d {}/.git ';' -o -exec test -f {}/.git ';' ')' -print 2>/dev/null | xargs -I{} basename {}`, + `find ${workspaceOrgs} -mindepth 1 -maxdepth 1 -type d -exec test -d {}/.git \\; -print 2>/dev/null | xargs -I{} basename {}`, ], });