@ai-hero/sandcastle 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +40 -14
  2. package/dist/AgentProvider.d.ts +4 -1
  3. package/dist/AgentProvider.d.ts.map +1 -1
  4. package/dist/AgentProvider.js +7 -6
  5. package/dist/AgentProvider.js.map +1 -1
  6. package/dist/SandboxFactory.d.ts +15 -4
  7. package/dist/SandboxFactory.d.ts.map +1 -1
  8. package/dist/SandboxFactory.js +156 -203
  9. package/dist/SandboxFactory.js.map +1 -1
  10. package/dist/SandboxProvider.d.ts +114 -0
  11. package/dist/SandboxProvider.d.ts.map +1 -0
  12. package/dist/SandboxProvider.js +25 -0
  13. package/dist/SandboxProvider.js.map +1 -0
  14. package/dist/cli.d.ts +7 -5
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +10 -9
  17. package/dist/cli.js.map +1 -1
  18. package/dist/createSandbox.d.ts +3 -2
  19. package/dist/createSandbox.d.ts.map +1 -1
  20. package/dist/createSandbox.js +19 -32
  21. package/dist/createSandbox.js.map +1 -1
  22. package/dist/index.d.ts +3 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/run.d.ts +4 -9
  27. package/dist/run.d.ts.map +1 -1
  28. package/dist/run.js +3 -15
  29. package/dist/run.js.map +1 -1
  30. package/dist/sandboxExec.d.ts +12 -0
  31. package/dist/sandboxExec.d.ts.map +1 -0
  32. package/dist/sandboxExec.js +26 -0
  33. package/dist/sandboxExec.js.map +1 -0
  34. package/dist/sandboxes/docker.d.ts +26 -0
  35. package/dist/sandboxes/docker.d.ts.map +1 -0
  36. package/dist/sandboxes/docker.js +133 -0
  37. package/dist/sandboxes/docker.js.map +1 -0
  38. package/dist/sandboxes/test-isolated.d.ts +17 -0
  39. package/dist/sandboxes/test-isolated.d.ts.map +1 -0
  40. package/dist/sandboxes/test-isolated.js +86 -0
  41. package/dist/sandboxes/test-isolated.js.map +1 -0
  42. package/dist/syncIn.d.ts +22 -0
  43. package/dist/syncIn.d.ts.map +1 -0
  44. package/dist/syncIn.js +57 -0
  45. package/dist/syncIn.js.map +1 -0
  46. package/dist/syncOut.d.ts +25 -0
  47. package/dist/syncOut.d.ts.map +1 -0
  48. package/dist/syncOut.js +192 -0
  49. package/dist/syncOut.js.map +1 -0
  50. package/dist/templates/blank/main.mts +2 -0
  51. package/dist/templates/parallel-planner/main.mts +4 -0
  52. package/dist/templates/sequential-reviewer/main.mts +3 -0
  53. package/dist/templates/simple-loop/main.mts +4 -0
  54. package/package.json +5 -1
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Docker sandbox provider — wraps DockerLifecycle into a SandboxProvider.
3
+ *
4
+ * Usage:
5
+ * import { docker } from "sandcastle/sandboxes/docker";
6
+ * await run({ agent: claudeCode("claude-opus-4-6"), sandbox: docker() });
7
+ */
8
+ import { execFile, execFileSync, spawn } from "node:child_process";
9
+ import { randomUUID } from "node:crypto";
10
+ import { createInterface } from "node:readline";
11
+ import { Effect } from "effect";
12
+ import { startContainer, removeContainer, chownInContainer, } from "../DockerLifecycle.js";
13
+ import { createBindMountSandboxProvider, } from "../SandboxProvider.js";
14
+ /**
15
+ * Create a Docker sandbox provider.
16
+ *
17
+ * The returned provider creates Docker containers with bind-mounts
18
+ * for the worktree and git directories.
19
+ */
20
+ export const docker = (options) => {
21
+ const configuredImageName = options?.imageName;
22
+ return createBindMountSandboxProvider({
23
+ name: "docker",
24
+ create: async (createOptions) => {
25
+ const containerName = `sandcastle-${randomUUID()}`;
26
+ const workspacePath = createOptions.mounts.find((m) => m.hostPath === createOptions.worktreePath)?.sandboxPath ?? "/home/agent/workspace";
27
+ // Build volume mount strings
28
+ const volumeMounts = createOptions.mounts.map((m) => {
29
+ const base = `${m.hostPath}:${m.sandboxPath}`;
30
+ return m.readonly ? `${base}:ro` : base;
31
+ });
32
+ // Resolve image name
33
+ const imageName = configuredImageName ?? defaultImageName(createOptions.hostRepoPath);
34
+ const hostUid = process.getuid?.() ?? 1000;
35
+ const hostGid = process.getgid?.() ?? 1000;
36
+ // Start container
37
+ await Effect.runPromise(startContainer(containerName, imageName, {
38
+ ...createOptions.env,
39
+ HOME: "/home/agent",
40
+ }, {
41
+ volumeMounts,
42
+ workdir: workspacePath,
43
+ user: `${hostUid}:${hostGid}`,
44
+ }).pipe(Effect.andThen(chownInContainer(containerName, `${hostUid}:${hostGid}`, "/home/agent"))));
45
+ // Set up signal handlers for cleanup
46
+ const onExit = () => {
47
+ try {
48
+ execFileSync("docker", ["rm", "-f", containerName], {
49
+ stdio: "ignore",
50
+ });
51
+ }
52
+ catch {
53
+ /* best-effort */
54
+ }
55
+ };
56
+ const onSignal = () => {
57
+ onExit();
58
+ process.exit(1);
59
+ };
60
+ process.on("exit", onExit);
61
+ process.on("SIGINT", onSignal);
62
+ process.on("SIGTERM", onSignal);
63
+ const handle = {
64
+ workspacePath,
65
+ exec: (command, opts) => new Promise((resolve, reject) => {
66
+ const args = ["exec"];
67
+ if (opts?.cwd)
68
+ args.push("-w", opts.cwd);
69
+ args.push(containerName, "sh", "-c", command);
70
+ execFile("docker", args, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
71
+ if (error && error.code === undefined) {
72
+ reject(new Error(`docker exec failed: ${error.message}`));
73
+ }
74
+ else {
75
+ resolve({
76
+ stdout: stdout.toString(),
77
+ stderr: stderr.toString(),
78
+ exitCode: typeof error?.code === "number" ? error.code : 0,
79
+ });
80
+ }
81
+ });
82
+ }),
83
+ execStreaming: (command, onLine, opts) => new Promise((resolve, reject) => {
84
+ const args = ["exec"];
85
+ if (opts?.cwd)
86
+ args.push("-w", opts.cwd);
87
+ args.push(containerName, "sh", "-c", command);
88
+ const proc = spawn("docker", args, {
89
+ stdio: ["ignore", "pipe", "pipe"],
90
+ });
91
+ const stdoutChunks = [];
92
+ const stderrChunks = [];
93
+ const rl = createInterface({ input: proc.stdout });
94
+ rl.on("line", (line) => {
95
+ stdoutChunks.push(line);
96
+ onLine(line);
97
+ });
98
+ proc.stderr.on("data", (chunk) => {
99
+ stderrChunks.push(chunk.toString());
100
+ });
101
+ proc.on("error", (error) => {
102
+ reject(new Error(`docker exec streaming failed: ${error.message}`));
103
+ });
104
+ proc.on("close", (code) => {
105
+ resolve({
106
+ stdout: stdoutChunks.join("\n"),
107
+ stderr: stderrChunks.join(""),
108
+ exitCode: code ?? 0,
109
+ });
110
+ });
111
+ }),
112
+ close: async () => {
113
+ process.removeListener("exit", onExit);
114
+ process.removeListener("SIGINT", onSignal);
115
+ process.removeListener("SIGTERM", onSignal);
116
+ await Effect.runPromise(removeContainer(containerName));
117
+ },
118
+ };
119
+ return handle;
120
+ },
121
+ });
122
+ };
123
+ /**
124
+ * Derive the default Docker image name from the repo directory.
125
+ * Returns `sandcastle:<dir-name>` where dir-name is the last path segment,
126
+ * lowercased and sanitized for Docker image tag rules.
127
+ */
128
+ export const defaultImageName = (repoDir) => {
129
+ const dirName = repoDir.replace(/\/+$/, "").split("/").pop() ?? "local";
130
+ const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
131
+ return `sandcastle:${sanitized}`;
132
+ };
133
+ //# sourceMappingURL=docker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker.js","sourceRoot":"","sources":["../../src/sandboxes/docker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Filesystem-based test isolated sandbox provider.
3
+ *
4
+ * Uses a temp directory on the local filesystem as the "sandbox".
5
+ * Intended for testing the isolated provider abstraction without
6
+ * requiring a real remote environment.
7
+ */
8
+ import { type IsolatedSandboxProvider } from "../SandboxProvider.js";
9
+ /**
10
+ * Create a filesystem-based test isolated sandbox provider.
11
+ *
12
+ * The "sandbox" is a temp directory. `exec` runs shell commands in it,
13
+ * `copyIn`/`copyOut` copy files between host and the temp dir,
14
+ * and `close` removes the temp dir.
15
+ */
16
+ export declare const testIsolated: () => IsolatedSandboxProvider;
17
+ //# sourceMappingURL=test-isolated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-isolated.d.ts","sourceRoot":"","sources":["../../src/sandboxes/test-isolated.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,EAIL,KAAK,uBAAuB,EAC7B,MAAM,uBAAuB,CAAC;AAE/B;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,+BA+FrB,CAAC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Filesystem-based test isolated sandbox provider.
3
+ *
4
+ * Uses a temp directory on the local filesystem as the "sandbox".
5
+ * Intended for testing the isolated provider abstraction without
6
+ * requiring a real remote environment.
7
+ */
8
+ import { execFile, spawn } from "node:child_process";
9
+ import { copyFile, mkdir, mkdtemp, rm } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import { createInterface } from "node:readline";
13
+ import { createIsolatedSandboxProvider, } from "../SandboxProvider.js";
14
+ /**
15
+ * Create a filesystem-based test isolated sandbox provider.
16
+ *
17
+ * The "sandbox" is a temp directory. `exec` runs shell commands in it,
18
+ * `copyIn`/`copyOut` copy files between host and the temp dir,
19
+ * and `close` removes the temp dir.
20
+ */
21
+ export const testIsolated = () => createIsolatedSandboxProvider({
22
+ name: "test-isolated",
23
+ create: async () => {
24
+ const sandboxRoot = await mkdtemp(join(tmpdir(), "sandcastle-test-"));
25
+ const workspacePath = join(sandboxRoot, "workspace");
26
+ await mkdir(workspacePath, { recursive: true });
27
+ return {
28
+ workspacePath,
29
+ exec: (command, options) => new Promise((resolve, reject) => {
30
+ execFile("sh", ["-c", command], {
31
+ cwd: options?.cwd ?? workspacePath,
32
+ maxBuffer: 10 * 1024 * 1024,
33
+ }, (error, stdout, stderr) => {
34
+ if (error && error.code === undefined) {
35
+ reject(new Error(`exec failed: ${error.message}`));
36
+ }
37
+ else {
38
+ resolve({
39
+ stdout: stdout.toString(),
40
+ stderr: stderr.toString(),
41
+ exitCode: typeof error?.code === "number" ? error.code : 0,
42
+ });
43
+ }
44
+ });
45
+ }),
46
+ execStreaming: (command, onLine, options) => new Promise((resolve, reject) => {
47
+ const proc = spawn("sh", ["-c", command], {
48
+ cwd: options?.cwd ?? workspacePath,
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ });
51
+ const stdoutChunks = [];
52
+ const stderrChunks = [];
53
+ const rl = createInterface({ input: proc.stdout });
54
+ rl.on("line", (line) => {
55
+ stdoutChunks.push(line);
56
+ onLine(line);
57
+ });
58
+ proc.stderr.on("data", (chunk) => {
59
+ stderrChunks.push(chunk.toString());
60
+ });
61
+ proc.on("error", (error) => {
62
+ reject(new Error(`exec streaming failed: ${error.message}`));
63
+ });
64
+ proc.on("close", (code) => {
65
+ resolve({
66
+ stdout: stdoutChunks.join("\n"),
67
+ stderr: stderrChunks.join(""),
68
+ exitCode: code ?? 0,
69
+ });
70
+ });
71
+ }),
72
+ copyIn: async (hostPath, sandboxPath) => {
73
+ await mkdir(dirname(sandboxPath), { recursive: true });
74
+ await copyFile(hostPath, sandboxPath);
75
+ },
76
+ copyOut: async (sandboxPath, hostPath) => {
77
+ await mkdir(dirname(hostPath), { recursive: true });
78
+ await copyFile(sandboxPath, hostPath);
79
+ },
80
+ close: async () => {
81
+ await rm(sandboxRoot, { recursive: true, force: true });
82
+ },
83
+ };
84
+ },
85
+ });
86
+ //# sourceMappingURL=test-isolated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-isolated.js","sourceRoot":"","sources":["../../src/sandboxes/test-isolated.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Sync-in: transfer a host git repo into an isolated sandbox via git bundle.
3
+ *
4
+ * Creates a git bundle capturing all refs from the host repo,
5
+ * copies it into the sandbox via the provider's copyIn, and
6
+ * clones from the bundle inside the sandbox.
7
+ */
8
+ import type { IsolatedSandboxHandle } from "./SandboxProvider.js";
9
+ /**
10
+ * Sync a host git repo into an isolated sandbox.
11
+ *
12
+ * 1. `git bundle create --all` on the host
13
+ * 2. `copyIn` the bundle to the sandbox
14
+ * 3. `git clone` from the bundle inside the sandbox
15
+ * 4. Verify HEAD matches
16
+ *
17
+ * @returns The branch name that was checked out
18
+ */
19
+ export declare const syncIn: (hostRepoDir: string, handle: IsolatedSandboxHandle) => Promise<{
20
+ branch: string;
21
+ }>;
22
+ //# sourceMappingURL=syncIn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncIn.d.ts","sourceRoot":"","sources":["../src/syncIn.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAGlE;;;;;;;;;GASG;AACH,eAAO,MAAM,MAAM;;EA0DlB,CAAC"}
package/dist/syncIn.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Sync-in: transfer a host git repo into an isolated sandbox via git bundle.
3
+ *
4
+ * Creates a git bundle capturing all refs from the host repo,
5
+ * copies it into the sandbox via the provider's copyIn, and
6
+ * clones from the bundle inside the sandbox.
7
+ */
8
+ import { mkdtemp, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { execHost, execOk } from "./sandboxExec.js";
12
+ /**
13
+ * Sync a host git repo into an isolated sandbox.
14
+ *
15
+ * 1. `git bundle create --all` on the host
16
+ * 2. `copyIn` the bundle to the sandbox
17
+ * 3. `git clone` from the bundle inside the sandbox
18
+ * 4. Verify HEAD matches
19
+ *
20
+ * @returns The branch name that was checked out
21
+ */
22
+ export const syncIn = async (hostRepoDir, handle) => {
23
+ // Get current branch from host
24
+ const branch = (await execHost("git rev-parse --abbrev-ref HEAD", hostRepoDir)).trim();
25
+ // Create git bundle on host capturing all refs
26
+ const bundleDir = await mkdtemp(join(tmpdir(), "sandcastle-bundle-"));
27
+ const bundleHostPath = join(bundleDir, "repo.bundle");
28
+ try {
29
+ await execHost(`git bundle create "${bundleHostPath}" --all`, hostRepoDir);
30
+ // Create temp dir in sandbox and copy bundle in
31
+ const mkTempResult = await execOk(handle, "mktemp -d -t sandcastle-XXXXXX");
32
+ const sandboxTmpDir = mkTempResult.stdout.trim();
33
+ const bundleSandboxPath = `${sandboxTmpDir}/repo.bundle`;
34
+ await handle.copyIn(bundleHostPath, bundleSandboxPath);
35
+ // Clone from bundle into the workspace
36
+ const workspacePath = handle.workspacePath;
37
+ await execOk(handle, `git clone "${bundleSandboxPath}" "${workspacePath}_clone"`);
38
+ // Move contents from clone into workspace (git clone requires empty target)
39
+ await execOk(handle, `rm -rf "${workspacePath}" && mv "${workspacePath}_clone" "${workspacePath}"`);
40
+ // Checkout the correct branch
41
+ await execOk(handle, `git checkout "${branch}"`, { cwd: workspacePath });
42
+ // Clean up sandbox temp files
43
+ await handle.exec(`rm -rf "${sandboxTmpDir}"`);
44
+ // Verify sync succeeded
45
+ const hostHead = (await execHost("git rev-parse HEAD", hostRepoDir)).trim();
46
+ const sandboxHead = (await execOk(handle, "git rev-parse HEAD", { cwd: workspacePath })).stdout.trim();
47
+ if (hostHead !== sandboxHead) {
48
+ throw new Error(`HEAD mismatch after sync-in: host=${hostHead} sandbox=${sandboxHead}`);
49
+ }
50
+ return { branch };
51
+ }
52
+ finally {
53
+ // Clean up host-side bundle temp dir
54
+ await rm(bundleDir, { recursive: true, force: true });
55
+ }
56
+ };
57
+ //# sourceMappingURL=syncIn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncIn.js","sourceRoot":"","sources":["../src/syncIn.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,EACzB,WAAmB,EACnB,MAA6B,EACA,EAAE;IAC/B,+BAA+B;IAC/B,MAAM,MAAM,GAAG,CACb,MAAM,QAAQ,CAAC,iCAAiC,EAAE,WAAW,CAAC,CAC/D,CAAC,IAAI,EAAE,CAAC;IAET,+CAA+C;IAC/C,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;IACtE,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IACtD,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC,sBAAsB,cAAc,SAAS,EAAE,WAAW,CAAC,CAAC;QAE3E,gDAAgD;QAChD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,gCAAgC,CAAC,CAAC;QAC5E,MAAM,aAAa,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjD,MAAM,iBAAiB,GAAG,GAAG,aAAa,cAAc,CAAC;QAEzD,MAAM,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,CAAC;QAEvD,uCAAuC;QACvC,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC;QAC3C,MAAM,MAAM,CACV,MAAM,EACN,cAAc,iBAAiB,MAAM,aAAa,SAAS,CAC5D,CAAC;QAEF,4EAA4E;QAC5E,MAAM,MAAM,CACV,MAAM,EACN,WAAW,aAAa,YAAY,aAAa,YAAY,aAAa,GAAG,CAC9E,CAAC;QAEF,8BAA8B;QAC9B,MAAM,MAAM,CAAC,MAAM,EAAE,iBAAiB,MAAM,GAAG,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,CAAC;QAEzE,8BAA8B;QAC9B,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,aAAa,GAAG,CAAC,CAAC;QAE/C,wBAAwB;QACxB,MAAM,QAAQ,GAAG,CAAC,MAAM,QAAQ,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5E,MAAM,WAAW,GAAG,CAClB,MAAM,MAAM,CAAC,MAAM,EAAE,oBAAoB,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,CACnE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAEhB,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,qCAAqC,QAAQ,YAAY,WAAW,EAAE,CACvE,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC;YAAS,CAAC;QACT,qCAAqC;QACrC,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;AACH,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Sync-out: extract changes from an isolated sandbox back to the host.
3
+ *
4
+ * Two-phase approach:
5
+ * 1. Save phase: eagerly save all artifacts (patches, diff, untracked files)
6
+ * to `.sandcastle/patches/<timestamp>/` before attempting to apply.
7
+ * 2. Apply phase: apply from the saved directory.
8
+ * - On success: clean up the patch directory.
9
+ * - On failure: preserve the patch directory and print recovery commands.
10
+ *
11
+ * Three-prong extraction within each phase:
12
+ * 1. Committed changes: `git format-patch` + `git am --3way`
13
+ * 2. Uncommitted changes (staged + unstaged): `git diff HEAD` + `git apply`
14
+ * 3. Untracked files: `git ls-files --others` + `copyOut` each file
15
+ */
16
+ import type { IsolatedSandboxHandle } from "./SandboxProvider.js";
17
+ /**
18
+ * Sync changes from an isolated sandbox back to the host repo.
19
+ *
20
+ * Two-phase extraction with artifact persistence:
21
+ * 1. Save all artifacts to `.sandcastle/patches/<timestamp>/`
22
+ * 2. Apply from saved directory; on failure, preserve artifacts and print recovery
23
+ */
24
+ export declare const syncOut: (hostRepoDir: string, handle: IsolatedSandboxHandle) => Promise<void>;
25
+ //# sourceMappingURL=syncOut.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncOut.d.ts","sourceRoot":"","sources":["../src/syncOut.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAYH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAyClE;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,uEAkKnB,CAAC"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Sync-out: extract changes from an isolated sandbox back to the host.
3
+ *
4
+ * Two-phase approach:
5
+ * 1. Save phase: eagerly save all artifacts (patches, diff, untracked files)
6
+ * to `.sandcastle/patches/<timestamp>/` before attempting to apply.
7
+ * 2. Apply phase: apply from the saved directory.
8
+ * - On success: clean up the patch directory.
9
+ * - On failure: preserve the patch directory and print recovery commands.
10
+ *
11
+ * Three-prong extraction within each phase:
12
+ * 1. Committed changes: `git format-patch` + `git am --3way`
13
+ * 2. Uncommitted changes (staged + unstaged): `git diff HEAD` + `git apply`
14
+ * 3. Untracked files: `git ls-files --others` + `copyOut` each file
15
+ */
16
+ import { existsSync } from "node:fs";
17
+ import { mkdir, readdir, readFile, rm, stat, writeFile, } from "node:fs/promises";
18
+ import { basename, dirname, join } from "node:path";
19
+ import { buildRecoveryMessage } from "./RecoveryMessage.js";
20
+ import { execHost, execOk } from "./sandboxExec.js";
21
+ /**
22
+ * Check if a patch file is empty or header-only.
23
+ * Merge commits produce patches with headers but no diff content.
24
+ * A patch is considered empty if it has no lines starting with "diff --git".
25
+ */
26
+ const isEmptyPatch = async (patchPath) => {
27
+ const info = await stat(patchPath);
28
+ if (info.size === 0)
29
+ return true;
30
+ const content = await readFile(patchPath, "utf-8");
31
+ return !content.includes("diff --git");
32
+ };
33
+ /**
34
+ * Generate a YYYYMMDD-HHMMSS timestamp directory name.
35
+ * Appends a counter suffix (-1, -2, ...) if the directory already exists.
36
+ */
37
+ const createPatchDir = async (hostRepoDir) => {
38
+ const now = new Date();
39
+ const pad = (n) => String(n).padStart(2, "0");
40
+ const base = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
41
+ const patchesRoot = join(hostRepoDir, ".sandcastle", "patches");
42
+ await mkdir(patchesRoot, { recursive: true });
43
+ let dirName = base;
44
+ let counter = 0;
45
+ while (existsSync(join(patchesRoot, dirName))) {
46
+ counter++;
47
+ dirName = `${base}-${counter}`;
48
+ }
49
+ const patchDir = join(patchesRoot, dirName);
50
+ await mkdir(patchDir, { recursive: true });
51
+ return patchDir;
52
+ };
53
+ /**
54
+ * Sync changes from an isolated sandbox back to the host repo.
55
+ *
56
+ * Two-phase extraction with artifact persistence:
57
+ * 1. Save all artifacts to `.sandcastle/patches/<timestamp>/`
58
+ * 2. Apply from saved directory; on failure, preserve artifacts and print recovery
59
+ */
60
+ export const syncOut = async (hostRepoDir, handle) => {
61
+ const workspacePath = handle.workspacePath;
62
+ const hostHead = (await execHost("git rev-parse HEAD", hostRepoDir)).trim();
63
+ const sandboxHead = (await execOk(handle, "git rev-parse HEAD", { cwd: workspacePath })).stdout.trim();
64
+ const hasCommits = hostHead !== sandboxHead;
65
+ // Check for uncommitted changes
66
+ const diffResult = await handle.exec("git diff HEAD", { cwd: workspacePath });
67
+ const hasDiff = diffResult.exitCode === 0 && diffResult.stdout.trim().length > 0;
68
+ // Check for untracked files
69
+ const lsFilesResult = await handle.exec("git ls-files --others --exclude-standard", { cwd: workspacePath });
70
+ const hasUntracked = lsFilesResult.exitCode === 0 && lsFilesResult.stdout.trim().length > 0;
71
+ const untrackedFiles = hasUntracked
72
+ ? lsFilesResult.stdout
73
+ .trim()
74
+ .split("\n")
75
+ .filter((f) => f.length > 0)
76
+ : [];
77
+ // Nothing to sync
78
+ if (!hasCommits && !hasDiff && !hasUntracked) {
79
+ return;
80
+ }
81
+ // --- Phase 1: Save all artifacts ---
82
+ const patchDir = await createPatchDir(hostRepoDir);
83
+ const relativePatchDir = join(".sandcastle", "patches", basename(patchDir));
84
+ const nonEmptyPatches = [];
85
+ // Save committed patches
86
+ if (hasCommits) {
87
+ const mkTempResult = await execOk(handle, "mktemp -d -t sandcastle-patches-XXXXXX");
88
+ const sandboxPatchDir = mkTempResult.stdout.trim();
89
+ try {
90
+ await execOk(handle, `git format-patch "${hostHead}..HEAD" -o "${sandboxPatchDir}"`, { cwd: workspacePath });
91
+ const lsResult = await execOk(handle, `ls -1 "${sandboxPatchDir}"`);
92
+ const patchNames = lsResult.stdout
93
+ .trim()
94
+ .split("\n")
95
+ .filter((name) => name.length > 0);
96
+ for (const patchName of patchNames) {
97
+ const sandboxPatchPath = `${sandboxPatchDir}/${patchName}`;
98
+ const hostPatchPath = join(patchDir, patchName);
99
+ await handle.copyOut(sandboxPatchPath, hostPatchPath);
100
+ if (!(await isEmptyPatch(hostPatchPath))) {
101
+ nonEmptyPatches.push(hostPatchPath);
102
+ }
103
+ }
104
+ }
105
+ finally {
106
+ await handle.exec(`rm -rf "${sandboxPatchDir}"`);
107
+ }
108
+ }
109
+ // Save uncommitted diff
110
+ if (hasDiff) {
111
+ const diffPath = join(patchDir, "changes.patch");
112
+ await writeFile(diffPath, diffResult.stdout);
113
+ }
114
+ // Save untracked files
115
+ if (hasUntracked) {
116
+ const untrackedDir = join(patchDir, "untracked");
117
+ for (const relPath of untrackedFiles) {
118
+ const sandboxFilePath = `${workspacePath}/${relPath}`;
119
+ const hostFilePath = join(untrackedDir, relPath);
120
+ await mkdir(dirname(hostFilePath), { recursive: true });
121
+ await handle.copyOut(sandboxFilePath, hostFilePath);
122
+ }
123
+ }
124
+ // --- Phase 2: Apply from saved directory ---
125
+ let failedStep;
126
+ // Apply committed patches
127
+ if (nonEmptyPatches.length > 0) {
128
+ try {
129
+ await execHost("git am --abort", hostRepoDir).catch(() => { });
130
+ const patchArgs = nonEmptyPatches.map((p) => `"${p}"`).join(" ");
131
+ await execHost(`git am --3way ${patchArgs}`, hostRepoDir);
132
+ }
133
+ catch {
134
+ failedStep = "commits";
135
+ }
136
+ }
137
+ // Apply uncommitted diff
138
+ if (!failedStep && hasDiff) {
139
+ const diffPath = join(patchDir, "changes.patch");
140
+ try {
141
+ await execHost(`git apply "${diffPath}"`, hostRepoDir);
142
+ }
143
+ catch {
144
+ failedStep = "diff";
145
+ }
146
+ }
147
+ // Copy untracked files
148
+ if (!failedStep && hasUntracked) {
149
+ try {
150
+ const untrackedDir = join(patchDir, "untracked");
151
+ for (const relPath of untrackedFiles) {
152
+ const srcPath = join(untrackedDir, relPath);
153
+ const destPath = join(hostRepoDir, relPath);
154
+ await mkdir(dirname(destPath), { recursive: true });
155
+ const content = await readFile(srcPath);
156
+ await writeFile(destPath, content);
157
+ }
158
+ }
159
+ catch {
160
+ failedStep = "untracked";
161
+ }
162
+ }
163
+ // --- Cleanup or preserve ---
164
+ if (failedStep) {
165
+ const msg = buildRecoveryMessage({
166
+ patchDir: relativePatchDir,
167
+ failedStep,
168
+ hasCommits: nonEmptyPatches.length > 0,
169
+ hasDiff,
170
+ hasUntracked,
171
+ });
172
+ console.error(`\n${msg}`);
173
+ }
174
+ else {
175
+ await rm(patchDir, { recursive: true, force: true });
176
+ // Clean up empty parent dirs
177
+ const patchesRoot = join(hostRepoDir, ".sandcastle", "patches");
178
+ try {
179
+ const remaining = await readdir(patchesRoot);
180
+ if (remaining.length === 0) {
181
+ await rm(join(hostRepoDir, ".sandcastle"), {
182
+ recursive: true,
183
+ force: true,
184
+ });
185
+ }
186
+ }
187
+ catch {
188
+ // ignore
189
+ }
190
+ }
191
+ };
192
+ //# sourceMappingURL=syncOut.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncOut.js","sourceRoot":"","sources":["../src/syncOut.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EACL,KAAK,EACL,OAAO,EACP,QAAQ,EACR,EAAE,EACF,IAAI,EACJ,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,EAAE,oBAAoB,EAAmB,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,YAAY,GAAG,KAAK,EAAE,SAAiB,EAAoB,EAAE;IACjE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACnD,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;AACzC,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,cAAc,GAAG,KAAK,EAAE,WAAmB,EAAmB,EAAE;IACpE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;IAE1J,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;IAChE,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,IAAI,OAAO,GAAG,IAAI,CAAC;IACnB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,OAAO,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;QAC9C,OAAO,EAAE,CAAC;QACV,OAAO,GAAG,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC5C,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,EAC1B,WAAmB,EACnB,MAA6B,EACd,EAAE;IACjB,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC;IAE3C,MAAM,QAAQ,GAAG,CAAC,MAAM,QAAQ,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5E,MAAM,WAAW,GAAG,CAClB,MAAM,MAAM,CAAC,MAAM,EAAE,oBAAoB,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,CACnE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAEhB,MAAM,UAAU,GAAG,QAAQ,KAAK,WAAW,CAAC;IAE5C,gCAAgC;IAChC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,CAAC;IAC9E,MAAM,OAAO,GACX,UAAU,CAAC,QAAQ,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAEnE,4BAA4B;IAC5B,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,IAAI,CACrC,0CAA0C,EAC1C,EAAE,GAAG,EAAE,aAAa,EAAE,CACvB,CAAC;IACF,MAAM,YAAY,GAChB,aAAa,CAAC,QAAQ,KAAK,CAAC,IAAI,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAEzE,MAAM,cAAc,GAAG,YAAY;QACjC,CAAC,CAAC,aAAa,CAAC,MAAM;aACjB,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QAChC,CAAC,CAAC,EAAE,CAAC;IAEP,kBAAkB;IAClB,IAAI,CAAC,UAAU,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,sCAAsC;IACtC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;IACnD,MAAM,gBAAgB,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE5E,MAAM,eAAe,GAAa,EAAE,CAAC;IAErC,yBAAyB;IACzB,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,YAAY,GAAG,MAAM,MAAM,CAC/B,MAAM,EACN,wCAAwC,CACzC,CAAC;QACF,MAAM,eAAe,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAEnD,IAAI,CAAC;YACH,MAAM,MAAM,CACV,MAAM,EACN,qBAAqB,QAAQ,eAAe,eAAe,GAAG,EAC9D,EAAE,GAAG,EAAE,aAAa,EAAE,CACvB,CAAC;YAEF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,UAAU,eAAe,GAAG,CAAC,CAAC;YACpE,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM;iBAC/B,IAAI,EAAE;iBACN,KAAK,CAAC,IAAI,CAAC;iBACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAErC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACnC,MAAM,gBAAgB,GAAG,GAAG,eAAe,IAAI,SAAS,EAAE,CAAC;gBAC3D,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;gBAChD,MAAM,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;gBAEtD,IAAI,CAAC,CAAC,MAAM,YAAY,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;oBACzC,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBACtC,CAAC;YACH,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,eAAe,GAAG,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,wBAAwB;IACxB,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;QACjD,MAAM,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAC/C,CAAC;IAED,uBAAuB;IACvB,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACjD,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;YACrC,MAAM,eAAe,GAAG,GAAG,aAAa,IAAI,OAAO,EAAE,CAAC;YACtD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YACjD,MAAM,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAED,8CAA8C;IAC9C,IAAI,UAAkC,CAAC;IAEvC,0BAA0B;IAC1B,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC9D,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjE,MAAM,QAAQ,CAAC,iBAAiB,SAAS,EAAE,EAAE,WAAW,CAAC,CAAC;QAC5D,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,IAAI,CAAC,UAAU,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;QACjD,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,cAAc,QAAQ,GAAG,EAAE,WAAW,CAAC,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,GAAG,MAAM,CAAC;QACtB,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,IAAI,CAAC,UAAU,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;YACjD,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;gBACrC,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;gBAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;gBAC5C,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACpD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;gBACxC,MAAM,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,GAAG,WAAW,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,oBAAoB,CAAC;YAC/B,QAAQ,EAAE,gBAAgB;YAC1B,UAAU;YACV,UAAU,EAAE,eAAe,CAAC,MAAM,GAAG,CAAC;YACtC,OAAO;YACP,YAAY;SACb,CAAC,CAAC;QACH,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IAC5B,CAAC;SAAM,CAAC;QACN,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,6BAA6B;QAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;QAChE,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;YAC7C,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,EAAE;oBACzC,SAAS,EAAE,IAAI;oBACf,KAAK,EAAE,IAAI;iBACZ,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;AACH,CAAC,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { run, claudeCode } from "@ai-hero/sandcastle";
2
+ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
2
3
 
3
4
  // Blank template: customize this to build your own orchestration.
4
5
  // Run this with: npx tsx .sandcastle/main.mts
@@ -6,5 +7,6 @@ import { run, claudeCode } from "@ai-hero/sandcastle";
6
7
 
7
8
  await run({
8
9
  agent: claudeCode("claude-opus-4-6"),
10
+ sandbox: docker(),
9
11
  promptFile: "./.sandcastle/prompt.md",
10
12
  });
@@ -17,6 +17,7 @@
17
17
  // "scripts": { "sandcastle": "npx tsx .sandcastle/main.mts" }
18
18
 
19
19
  import * as sandcastle from "@ai-hero/sandcastle";
20
+ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Configuration
@@ -56,6 +57,7 @@ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
56
57
  const plan = await sandcastle.run({
57
58
  hooks,
58
59
  copyToSandbox,
60
+ sandbox: docker(),
59
61
  name: "planner",
60
62
  // One iteration is enough: the planner just needs to read and reason,
61
63
  // not write code.
@@ -105,6 +107,7 @@ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
105
107
  sandcastle.run({
106
108
  hooks,
107
109
  copyToSandbox,
110
+ sandbox: docker(),
108
111
  name: "implementer",
109
112
  // Give each agent plenty of room to implement and iterate on tests.
110
113
  maxIterations: 100,
@@ -179,6 +182,7 @@ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
179
182
  await sandcastle.run({
180
183
  hooks,
181
184
  copyToSandbox,
185
+ sandbox: docker(),
182
186
  name: "merger",
183
187
  maxIterations: 10,
184
188
  // Sonnet is sufficient for merge conflict resolution.
@@ -17,6 +17,7 @@
17
17
  // "scripts": { "sandcastle": "npx tsx .sandcastle/main.mts" }
18
18
 
19
19
  import * as sandcastle from "@ai-hero/sandcastle";
20
+ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Configuration
@@ -57,6 +58,7 @@ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
57
58
  const implement = await sandcastle.run({
58
59
  hooks,
59
60
  copyToSandbox,
61
+ sandbox: docker(),
60
62
  name: "implementer",
61
63
  maxIterations: 100,
62
64
  agent: sandcastle.claudeCode("claude-sonnet-4-6"),
@@ -84,6 +86,7 @@ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
84
86
  await sandcastle.run({
85
87
  hooks,
86
88
  copyToSandbox,
89
+ sandbox: docker(),
87
90
  name: "reviewer",
88
91
  maxIterations: 10,
89
92
  agent: sandcastle.claudeCode("claude-sonnet-4-6"),