@ai-hero/sandcastle 0.0.1

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 (133) hide show
  1. package/README.md +284 -0
  2. package/dist/AgentProvider.d.ts +8 -0
  3. package/dist/AgentProvider.d.ts.map +1 -0
  4. package/dist/AgentProvider.js +60 -0
  5. package/dist/AgentProvider.js.map +1 -0
  6. package/dist/Config.d.ts +35 -0
  7. package/dist/Config.d.ts.map +1 -0
  8. package/dist/Config.js +56 -0
  9. package/dist/Config.js.map +1 -0
  10. package/dist/CopyToSandbox.d.ts +8 -0
  11. package/dist/CopyToSandbox.d.ts.map +1 -0
  12. package/dist/CopyToSandbox.js +32 -0
  13. package/dist/CopyToSandbox.js.map +1 -0
  14. package/dist/Display.d.ts +52 -0
  15. package/dist/Display.d.ts.map +1 -0
  16. package/dist/Display.js +132 -0
  17. package/dist/Display.js.map +1 -0
  18. package/dist/DockerLifecycle.d.ts +37 -0
  19. package/dist/DockerLifecycle.d.ts.map +1 -0
  20. package/dist/DockerLifecycle.js +109 -0
  21. package/dist/DockerLifecycle.js.map +1 -0
  22. package/dist/DockerSandbox.d.ts +6 -0
  23. package/dist/DockerSandbox.d.ts.map +1 -0
  24. package/dist/DockerSandbox.js +122 -0
  25. package/dist/DockerSandbox.js.map +1 -0
  26. package/dist/EnvResolver.d.ts +11 -0
  27. package/dist/EnvResolver.d.ts.map +1 -0
  28. package/dist/EnvResolver.js +43 -0
  29. package/dist/EnvResolver.js.map +1 -0
  30. package/dist/ErrorHandler.d.ts +15 -0
  31. package/dist/ErrorHandler.d.ts.map +1 -0
  32. package/dist/ErrorHandler.js +58 -0
  33. package/dist/ErrorHandler.js.map +1 -0
  34. package/dist/FilesystemSandbox.d.ts +6 -0
  35. package/dist/FilesystemSandbox.d.ts.map +1 -0
  36. package/dist/FilesystemSandbox.js +83 -0
  37. package/dist/FilesystemSandbox.js.map +1 -0
  38. package/dist/InitService.d.ts +11 -0
  39. package/dist/InitService.d.ts.map +1 -0
  40. package/dist/InitService.js +111 -0
  41. package/dist/InitService.js.map +1 -0
  42. package/dist/Orchestrator.d.ts +49 -0
  43. package/dist/Orchestrator.d.ts.map +1 -0
  44. package/dist/Orchestrator.js +155 -0
  45. package/dist/Orchestrator.js.map +1 -0
  46. package/dist/PromptArgumentSubstitution.d.ts +6 -0
  47. package/dist/PromptArgumentSubstitution.d.ts.map +1 -0
  48. package/dist/PromptArgumentSubstitution.js +33 -0
  49. package/dist/PromptArgumentSubstitution.js.map +1 -0
  50. package/dist/PromptPreprocessor.d.ts +7 -0
  51. package/dist/PromptPreprocessor.d.ts.map +1 -0
  52. package/dist/PromptPreprocessor.js +34 -0
  53. package/dist/PromptPreprocessor.js.map +1 -0
  54. package/dist/PromptResolver.d.ts +9 -0
  55. package/dist/PromptResolver.d.ts.map +1 -0
  56. package/dist/PromptResolver.js +26 -0
  57. package/dist/PromptResolver.js.map +1 -0
  58. package/dist/RecoveryMessage.d.ts +15 -0
  59. package/dist/RecoveryMessage.d.ts.map +1 -0
  60. package/dist/RecoveryMessage.js +81 -0
  61. package/dist/RecoveryMessage.js.map +1 -0
  62. package/dist/Sandbox.d.ts +23 -0
  63. package/dist/Sandbox.d.ts.map +1 -0
  64. package/dist/Sandbox.js +5 -0
  65. package/dist/Sandbox.js.map +1 -0
  66. package/dist/SandboxFactory.d.ts +56 -0
  67. package/dist/SandboxFactory.d.ts.map +1 -0
  68. package/dist/SandboxFactory.js +219 -0
  69. package/dist/SandboxFactory.js.map +1 -0
  70. package/dist/SandboxLifecycle.d.ts +32 -0
  71. package/dist/SandboxLifecycle.d.ts.map +1 -0
  72. package/dist/SandboxLifecycle.js +152 -0
  73. package/dist/SandboxLifecycle.js.map +1 -0
  74. package/dist/SyncService.d.ts +20 -0
  75. package/dist/SyncService.d.ts.map +1 -0
  76. package/dist/SyncService.js +504 -0
  77. package/dist/SyncService.js.map +1 -0
  78. package/dist/TokenResolver.d.ts +6 -0
  79. package/dist/TokenResolver.d.ts.map +1 -0
  80. package/dist/TokenResolver.js +43 -0
  81. package/dist/TokenResolver.js.map +1 -0
  82. package/dist/WorktreeManager.d.ts +42 -0
  83. package/dist/WorktreeManager.d.ts.map +1 -0
  84. package/dist/WorktreeManager.js +170 -0
  85. package/dist/WorktreeManager.js.map +1 -0
  86. package/dist/cli.d.ts +22 -0
  87. package/dist/cli.d.ts.map +1 -0
  88. package/dist/cli.js +217 -0
  89. package/dist/cli.js.map +1 -0
  90. package/dist/errors.d.ts +95 -0
  91. package/dist/errors.d.ts.map +1 -0
  92. package/dist/errors.js +35 -0
  93. package/dist/errors.js.map +1 -0
  94. package/dist/index.d.ts +4 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -0
  98. package/dist/main.d.ts +3 -0
  99. package/dist/main.d.ts.map +1 -0
  100. package/dist/main.js +16 -0
  101. package/dist/main.js.map +1 -0
  102. package/dist/run.d.ts +91 -0
  103. package/dist/run.d.ts.map +1 -0
  104. package/dist/run.js +155 -0
  105. package/dist/run.js.map +1 -0
  106. package/dist/templates/blank/main.ts +9 -0
  107. package/dist/templates/blank/prompt.md +12 -0
  108. package/dist/templates/blank/template.json +4 -0
  109. package/dist/templates/parallel-planner/implement-prompt.md +62 -0
  110. package/dist/templates/parallel-planner/main.ts +200 -0
  111. package/dist/templates/parallel-planner/merge-prompt.md +22 -0
  112. package/dist/templates/parallel-planner/plan-prompt.md +33 -0
  113. package/dist/templates/parallel-planner/template.json +4 -0
  114. package/dist/templates/sequential-reviewer/implement-prompt.md +62 -0
  115. package/dist/templates/sequential-reviewer/main.ts +102 -0
  116. package/dist/templates/sequential-reviewer/review-prompt.md +43 -0
  117. package/dist/templates/sequential-reviewer/template.json +4 -0
  118. package/dist/templates/simple-loop/main.ts +37 -0
  119. package/dist/templates/simple-loop/prompt.md +51 -0
  120. package/dist/templates/simple-loop/template.json +4 -0
  121. package/dist/templates.d.ts +2 -0
  122. package/dist/templates.d.ts.map +1 -0
  123. package/dist/templates.js +26 -0
  124. package/dist/templates.js.map +1 -0
  125. package/dist/terminalCleanup.d.ts +30 -0
  126. package/dist/terminalCleanup.d.ts.map +1 -0
  127. package/dist/terminalCleanup.js +37 -0
  128. package/dist/terminalCleanup.js.map +1 -0
  129. package/dist/testSandbox.d.ts +8 -0
  130. package/dist/testSandbox.d.ts.map +1 -0
  131. package/dist/testSandbox.js +101 -0
  132. package/dist/testSandbox.js.map +1 -0
  133. package/package.json +62 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { run } from "./run.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
package/dist/main.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
3
+ import { Effect, Layer } from "effect";
4
+ import { cli } from "./cli.js";
5
+ import { ClackDisplay } from "./Display.js";
6
+ import { withFriendlyErrors } from "./ErrorHandler.js";
7
+ import { setupTerminalCleanup } from "./terminalCleanup.js";
8
+ // Restore terminal state on any exit.
9
+ // @clack/prompts' spinner/taskLog set stdin to raw mode and hide the cursor.
10
+ // If the process exits via a signal handler that calls process.exit() directly
11
+ // (e.g. the Ctrl-C handler in SandboxFactory), clack's own cleanup is
12
+ // bypassed, leaving the terminal broken. This exit hook fixes that.
13
+ setupTerminalCleanup();
14
+ const mainLayer = Layer.merge(NodeContext.layer, ClackDisplay.layer);
15
+ cli(process.argv).pipe(withFriendlyErrors, Effect.provide(mainLayer), NodeRuntime.runMain);
16
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,sCAAsC;AACtC,6EAA6E;AAC7E,+EAA+E;AAC/E,sEAAsE;AACtE,oEAAoE;AACpE,oBAAoB,EAAE,CAAC;AAEvB,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;AAErE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CACpB,kBAAkB,EAClB,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EACzB,WAAW,CAAC,OAAO,CACpB,CAAC"}
package/dist/run.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { type Severity } from "./Display.js";
2
+ import { type PromptArgs } from "./PromptArgumentSubstitution.js";
3
+ /** Default maximum number of iterations for a run. */
4
+ export declare const DEFAULT_MAX_ITERATIONS = 1;
5
+ /** Replace characters that are invalid or problematic in file paths with dashes. */
6
+ export declare const sanitizeBranchForFilename: (branch: string) => string;
7
+ export interface FileDisplayStartupOptions {
8
+ readonly logPath: string;
9
+ readonly agentName?: string;
10
+ readonly branch?: string;
11
+ }
12
+ /**
13
+ * Print the startup message to the terminal when using file-based logging.
14
+ * Uses styleText for lightweight bold/dim styling — does not use Clack.
15
+ */
16
+ export declare const printFileDisplayStartup: (options: FileDisplayStartupOptions) => void;
17
+ /**
18
+ * Derive the default Docker image name from the repo directory.
19
+ * Returns `sandcastle:<dir-name>` where dir-name is the last path segment,
20
+ * lowercased and sanitized for Docker image tag rules.
21
+ */
22
+ export declare const defaultImageName: (repoDir: string) => string;
23
+ /**
24
+ * Build the log filename for a run.
25
+ * When a targetBranch is provided (temp branch mode), prefixes the filename
26
+ * with the sanitized target branch name so developers can identify which
27
+ * branch the run was targeting: `<targetBranch>-<resolvedBranch>.log`
28
+ * When no targetBranch, uses just the resolved branch: `<resolvedBranch>.log`
29
+ */
30
+ export declare const buildLogFilename: (resolvedBranch: string, targetBranch?: string | undefined) => string;
31
+ /**
32
+ * Build the completion status message for a run, used in both terminal mode
33
+ * and log-to-file mode to record the final outcome.
34
+ */
35
+ export declare const buildCompletionMessage: (wasCompletionSignalDetected: boolean, iterationsRun: number) => {
36
+ readonly message: string;
37
+ readonly severity: Severity;
38
+ };
39
+ export type LoggingOption = {
40
+ readonly type: "file";
41
+ readonly path: string;
42
+ } | {
43
+ readonly type: "stdout";
44
+ };
45
+ export interface RunOptions {
46
+ /** Inline prompt string (mutually exclusive with promptFile) */
47
+ readonly prompt?: string;
48
+ /** Path to a prompt file (mutually exclusive with prompt) */
49
+ readonly promptFile?: string;
50
+ /** Maximum iterations to run (default: 1) */
51
+ readonly maxIterations?: number;
52
+ /** Hooks to run during sandbox lifecycle */
53
+ readonly hooks?: {
54
+ readonly onSandboxReady?: ReadonlyArray<{
55
+ command: string;
56
+ }>;
57
+ };
58
+ /** Target branch name for sandbox work */
59
+ readonly branch?: string;
60
+ /** Model to use for the agent (default: claude-opus-4-6) */
61
+ readonly model?: string;
62
+ /** Agent provider name (default: claude-code) */
63
+ readonly agent?: string;
64
+ /** Docker image name to use for the sandbox (default: sandcastle:<repo-dir-name>) */
65
+ readonly imageName?: string;
66
+ /** Key-value map for {{KEY}} placeholder substitution in prompts */
67
+ readonly promptArgs?: PromptArgs;
68
+ /** Logging mode (default: { type: 'file' } with auto-generated path under .sandcastle/logs/) */
69
+ readonly logging?: LoggingOption;
70
+ /** Custom completion signal string (default: "<promise>COMPLETE</promise>") */
71
+ readonly completionSignal?: string;
72
+ /** Timeout in seconds. If the run exceeds this, it fails. Default: 1200 (20 minutes) */
73
+ readonly timeoutSeconds?: number;
74
+ /** Optional name for the run, shown as a prefix in log output */
75
+ readonly name?: string;
76
+ /** Paths relative to the host repo root to copy into the worktree before container start. */
77
+ readonly copyToSandbox?: string[];
78
+ }
79
+ export interface RunResult {
80
+ readonly iterationsRun: number;
81
+ readonly wasCompletionSignalDetected: boolean;
82
+ readonly stdout: string;
83
+ readonly commits: {
84
+ sha: string;
85
+ }[];
86
+ readonly branch: string;
87
+ /** Path to the log file, if logging was drained to a file. */
88
+ readonly logFilePath?: string;
89
+ }
90
+ export declare const run: (options: RunOptions) => Promise<RunResult>;
91
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,QAAQ,EACd,MAAM,cAAc,CAAC;AAUtB,OAAO,EACL,KAAK,UAAU,EAEhB,MAAM,iCAAiC,CAAC;AAEzC,sDAAsD;AACtD,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,oFAAoF;AACpF,eAAO,MAAM,yBAAyB,4BACA,CAAC;AAEvC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,8CASnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,6BAI5B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,uEAS5B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;CAclC,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC9D,CAAC;IACF,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,gGAAgG;IAChG,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,wFAAwF;IACxF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,2BAA2B,EAAE,OAAO,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,eAAO,MAAM,GAAG,6CA0If,CAAC"}
package/dist/run.js ADDED
@@ -0,0 +1,155 @@
1
+ import { NodeContext, NodeFileSystem } from "@effect/platform-node";
2
+ import path, { join } from "node:path";
3
+ import { styleText } from "node:util";
4
+ import { Effect, Layer } from "effect";
5
+ import { getAgentProvider } from "./AgentProvider.js";
6
+ import { ClackDisplay, Display, FileDisplay, } from "./Display.js";
7
+ import { orchestrate } from "./Orchestrator.js";
8
+ import { resolvePrompt } from "./PromptResolver.js";
9
+ import { WorktreeDockerSandboxFactory, WorktreeSandboxConfig, SANDBOX_WORKSPACE_DIR, } from "./SandboxFactory.js";
10
+ import { resolveEnv } from "./EnvResolver.js";
11
+ import { generateTempBranchName, getCurrentBranch } from "./WorktreeManager.js";
12
+ import { substitutePromptArgs, } from "./PromptArgumentSubstitution.js";
13
+ /** Default maximum number of iterations for a run. */
14
+ export const DEFAULT_MAX_ITERATIONS = 1;
15
+ /** Replace characters that are invalid or problematic in file paths with dashes. */
16
+ export const sanitizeBranchForFilename = (branch) => branch.replace(/[/\\:*?"<>|]/g, "-");
17
+ /**
18
+ * Print the startup message to the terminal when using file-based logging.
19
+ * Uses styleText for lightweight bold/dim styling — does not use Clack.
20
+ */
21
+ export const printFileDisplayStartup = (options) => {
22
+ const name = options.agentName ?? "Agent";
23
+ const label = styleText("bold", `[${name}]`);
24
+ const branchPart = options.branch ? ` on branch ${options.branch}` : "";
25
+ const relativeLogPath = path.relative(process.cwd(), options.logPath);
26
+ console.log(`${label} Started${branchPart}`);
27
+ console.log(styleText("dim", ` tail -f ${relativeLogPath}`));
28
+ };
29
+ /**
30
+ * Derive the default Docker image name from the repo directory.
31
+ * Returns `sandcastle:<dir-name>` where dir-name is the last path segment,
32
+ * lowercased and sanitized for Docker image tag rules.
33
+ */
34
+ export const defaultImageName = (repoDir) => {
35
+ const dirName = repoDir.replace(/\/+$/, "").split("/").pop() ?? "local";
36
+ const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
37
+ return `sandcastle:${sanitized}`;
38
+ };
39
+ /**
40
+ * Build the log filename for a run.
41
+ * When a targetBranch is provided (temp branch mode), prefixes the filename
42
+ * with the sanitized target branch name so developers can identify which
43
+ * branch the run was targeting: `<targetBranch>-<resolvedBranch>.log`
44
+ * When no targetBranch, uses just the resolved branch: `<resolvedBranch>.log`
45
+ */
46
+ export const buildLogFilename = (resolvedBranch, targetBranch) => {
47
+ const sanitized = sanitizeBranchForFilename(resolvedBranch);
48
+ if (targetBranch) {
49
+ return `${sanitizeBranchForFilename(targetBranch)}-${sanitized}.log`;
50
+ }
51
+ return `${sanitized}.log`;
52
+ };
53
+ /**
54
+ * Build the completion status message for a run, used in both terminal mode
55
+ * and log-to-file mode to record the final outcome.
56
+ */
57
+ export const buildCompletionMessage = (wasCompletionSignalDetected, iterationsRun) => {
58
+ if (wasCompletionSignalDetected) {
59
+ return {
60
+ message: `Run complete: agent finished after ${iterationsRun} iteration(s).`,
61
+ severity: "success",
62
+ };
63
+ }
64
+ return {
65
+ message: `Run complete: reached ${iterationsRun} iteration(s) without completion signal.`,
66
+ severity: "warn",
67
+ };
68
+ };
69
+ export const run = async (options) => {
70
+ const { prompt, promptFile, maxIterations = DEFAULT_MAX_ITERATIONS, hooks, branch, model, agent, } = options;
71
+ const hostRepoDir = process.cwd();
72
+ // Resolve prompt
73
+ const rawPrompt = await Effect.runPromise(resolvePrompt({ prompt, promptFile }).pipe(Effect.provide(NodeContext.layer)));
74
+ // Resolve model: explicit option > default
75
+ const resolvedModel = model;
76
+ // Resolve agent provider: explicit option > default
77
+ const agentName = agent ?? "claude-code";
78
+ const provider = getAgentProvider(agentName);
79
+ // Resolve image name: explicit option > default
80
+ const resolvedImageName = options.imageName ?? defaultImageName(hostRepoDir);
81
+ // Resolve env vars
82
+ const env = await Effect.runPromise(resolveEnv(hostRepoDir).pipe(Effect.provide(NodeContext.layer)));
83
+ // When no branch is provided, generate a temporary branch name.
84
+ // This names the log file after the temp branch and also directs
85
+ // the sandbox to work on that branch (instead of the current host branch).
86
+ const resolvedBranch = branch ?? generateTempBranchName(agentName);
87
+ // When using a temp branch, prefix the log filename with the target branch
88
+ // (the host's current branch) so developers can tell which branch was targeted.
89
+ const targetBranch = branch === undefined
90
+ ? await Effect.runPromise(getCurrentBranch(hostRepoDir))
91
+ : undefined;
92
+ // Resolve logging option
93
+ const resolvedLogging = options.logging ?? {
94
+ type: "file",
95
+ path: join(hostRepoDir, ".sandcastle", "logs", buildLogFilename(resolvedBranch, targetBranch)),
96
+ };
97
+ const displayLayer = resolvedLogging.type === "file"
98
+ ? (() => {
99
+ printFileDisplayStartup({
100
+ logPath: resolvedLogging.path,
101
+ agentName: options.name,
102
+ branch: resolvedBranch,
103
+ });
104
+ return Layer.provide(FileDisplay.layer(resolvedLogging.path), NodeFileSystem.layer);
105
+ })()
106
+ : ClackDisplay.layer;
107
+ const factoryLayer = Layer.provide(WorktreeDockerSandboxFactory.layer, Layer.mergeAll(Layer.succeed(WorktreeSandboxConfig, {
108
+ imageName: resolvedImageName,
109
+ env,
110
+ hostRepoDir,
111
+ // Pass explicit branch only — when undefined, WorktreeManager creates a temp branch
112
+ // and SandboxLifecycle cherry-picks commits onto the host's current branch
113
+ branch,
114
+ copyToSandbox: options.copyToSandbox,
115
+ agentName,
116
+ }), NodeFileSystem.layer, displayLayer));
117
+ const runLayer = Layer.merge(factoryLayer, displayLayer);
118
+ const result = await Effect.runPromise(Effect.gen(function* () {
119
+ const d = yield* Display;
120
+ yield* d.intro(options.name ?? "sandcastle");
121
+ const rows = {
122
+ Agent: agentName,
123
+ Image: resolvedImageName,
124
+ "Max iterations": String(maxIterations),
125
+ };
126
+ rows["Branch"] = resolvedBranch;
127
+ if (resolvedModel)
128
+ rows["Model"] = resolvedModel;
129
+ yield* d.summary("Sandcastle Run", rows);
130
+ // Substitute prompt arguments ({{KEY}} placeholders) before orchestration
131
+ const resolvedPrompt = options.promptArgs
132
+ ? yield* substitutePromptArgs(rawPrompt, options.promptArgs)
133
+ : rawPrompt;
134
+ const orchestrateResult = yield* orchestrate({
135
+ hostRepoDir,
136
+ sandboxRepoDir: SANDBOX_WORKSPACE_DIR,
137
+ iterations: maxIterations,
138
+ hooks,
139
+ prompt: resolvedPrompt,
140
+ branch,
141
+ model: resolvedModel,
142
+ completionSignal: options.completionSignal,
143
+ timeoutSeconds: options.timeoutSeconds,
144
+ name: options.name,
145
+ });
146
+ const completion = buildCompletionMessage(orchestrateResult.wasCompletionSignalDetected, orchestrateResult.iterationsRun);
147
+ yield* d.status(completion.message, completion.severity);
148
+ return orchestrateResult;
149
+ }).pipe(Effect.provide(runLayer)));
150
+ return {
151
+ ...result,
152
+ logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : undefined,
153
+ };
154
+ };
155
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { run } from "@ai-hero/sandcastle";
2
+
3
+ // Blank template: customize this to build your own orchestration.
4
+ // Run this with: npx tsx .sandcastle/main.ts
5
+ // Or add to package.json scripts: "sandcastle": "npx tsx .sandcastle/main.ts"
6
+
7
+ await run({
8
+ promptFile: "./.sandcastle/prompt.md",
9
+ });
@@ -0,0 +1,12 @@
1
+ # Context
2
+
3
+ <!-- Use !`command` to pull in dynamic context. Commands run inside the sandbox. -->
4
+ <!-- Example: !`git log --oneline -10` or !`gh issue list --json number,title` -->
5
+
6
+ # Task
7
+
8
+ <!-- Describe what the agent should do. -->
9
+
10
+ # Done
11
+
12
+ <!-- When the task is complete, output <promise>COMPLETE</promise> to signal early termination. -->
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "blank",
3
+ "description": "Bare scaffold — write your own prompt and orchestration"
4
+ }
@@ -0,0 +1,62 @@
1
+ # TASK
2
+
3
+ Fix issue #{{ISSUE_NUMBER}}: {{ISSUE_TITLE}}
4
+
5
+ Pull in the issue using `gh issue view`. If it has a parent PRD, pull that in too.
6
+
7
+ Only work on the issue specified.
8
+
9
+ Work on branch {{BRANCH}}. Make commits, run tests, and close the issue when done.
10
+
11
+ # CONTEXT
12
+
13
+ Here are the last 10 commits:
14
+
15
+ <recent-commits>
16
+
17
+ !`git log -n 10 --format="%H%n%ad%n%B---" --date=short`
18
+
19
+ </recent-commits>
20
+
21
+ # EXPLORATION
22
+
23
+ Explore the repo and fill your context window with relevant information that will allow you to complete the task.
24
+
25
+ Pay extra attention to test files that touch the relevant parts of the code.
26
+
27
+ # EXECUTION
28
+
29
+ If applicable, use RGR to complete the task.
30
+
31
+ 1. RED: write one test
32
+ 2. GREEN: write the implementation to pass that test
33
+ 3. REPEAT until done
34
+ 4. REFACTOR the code
35
+
36
+ # FEEDBACK LOOPS
37
+
38
+ Before committing, run `npm run typecheck` and `npm run test` to ensure the tests pass.
39
+
40
+ # COMMIT
41
+
42
+ Make a git commit. The commit message must:
43
+
44
+ 1. Start with `RALPH:` prefix
45
+ 2. Include task completed + PRD reference
46
+ 3. Key decisions made
47
+ 4. Files changed
48
+ 5. Blockers or notes for next iteration
49
+
50
+ Keep it concise.
51
+
52
+ # THE ISSUE
53
+
54
+ If the task is not complete, leave a comment on the GitHub issue with what was done.
55
+
56
+ Do not close the issue - this will be done later.
57
+
58
+ Once complete, output <promise>COMPLETE</promise>.
59
+
60
+ # FINAL RULES
61
+
62
+ ONLY WORK ON A SINGLE TASK.
@@ -0,0 +1,200 @@
1
+ // Parallel Planner — three-phase orchestration loop
2
+ //
3
+ // This template drives a multi-phase workflow:
4
+ // Phase 1 (Plan): An opus agent analyzes open issues, builds a dependency
5
+ // graph, and outputs a <plan> JSON listing unblocked issues
6
+ // with their target branch names.
7
+ // Phase 2 (Execute): N sonnet agents run in parallel via Promise.allSettled,
8
+ // each working a single issue on its own branch.
9
+ // Phase 3 (Merge): A sonnet agent merges all branches that produced commits.
10
+ //
11
+ // The outer loop repeats up to MAX_ITERATIONS times so that newly unblocked
12
+ // issues are picked up after each round of merges.
13
+ //
14
+ // Usage:
15
+ // npx tsx .sandcastle/main.ts
16
+ // Or add to package.json:
17
+ // "scripts": { "sandcastle": "npx tsx .sandcastle/main.ts" }
18
+
19
+ import * as sandcastle from "@ai-hero/sandcastle";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Configuration
23
+ // ---------------------------------------------------------------------------
24
+
25
+ // Maximum number of plan→execute→merge cycles before stopping.
26
+ // Raise this if your backlog is large; lower it for a quick smoke-test run.
27
+ const MAX_ITERATIONS = 10;
28
+
29
+ // Hooks run inside the sandbox before the agent starts each iteration.
30
+ // npm install ensures the sandbox always has fresh dependencies.
31
+ const hooks = {
32
+ onSandboxReady: [{ command: "npm install" }],
33
+ };
34
+
35
+ // Copy node_modules from the host into the worktree before each sandbox
36
+ // starts. Avoids a full npm install from scratch; the hook above handles
37
+ // platform-specific binaries and any packages added since the last copy.
38
+ const copyToSandbox = ["node_modules"];
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Main loop
42
+ // ---------------------------------------------------------------------------
43
+
44
+ for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
45
+ console.log(`\n=== Iteration ${iteration}/${MAX_ITERATIONS} ===\n`);
46
+
47
+ // -------------------------------------------------------------------------
48
+ // Phase 1: Plan
49
+ //
50
+ // The planning agent (opus, for deeper reasoning) reads the open issue list,
51
+ // builds a dependency graph, and selects the issues that can be worked in
52
+ // parallel right now (i.e., no blocking dependencies on other open issues).
53
+ //
54
+ // It outputs a <plan> JSON block — we parse that to drive Phase 2.
55
+ // -------------------------------------------------------------------------
56
+ const plan = await sandcastle.run({
57
+ hooks,
58
+ copyToSandbox,
59
+ name: "planner",
60
+ // One iteration is enough: the planner just needs to read and reason,
61
+ // not write code.
62
+ maxIterations: 1,
63
+ // Opus for planning: dependency analysis benefits from deeper reasoning.
64
+ model: "claude-opus-4-6",
65
+ promptFile: "./.sandcastle/plan-prompt.md",
66
+ });
67
+
68
+ // Extract the <plan>…</plan> block from the agent's stdout.
69
+ const planMatch = plan.stdout.match(/<plan>([\s\S]*?)<\/plan>/);
70
+ if (!planMatch) {
71
+ throw new Error(
72
+ "Planning agent did not produce a <plan> tag.\n\n" + plan.stdout,
73
+ );
74
+ }
75
+
76
+ // The plan JSON contains an array of issues, each with number, title, branch.
77
+ const { issues } = JSON.parse(planMatch[1]!) as {
78
+ issues: { number: number; title: string; branch: string }[];
79
+ };
80
+
81
+ if (issues.length === 0) {
82
+ // No unblocked work — either everything is done or everything is blocked.
83
+ console.log("No unblocked issues to work on. Exiting.");
84
+ break;
85
+ }
86
+
87
+ console.log(
88
+ `Planning complete. ${issues.length} issue(s) to work in parallel:`,
89
+ );
90
+ for (const issue of issues) {
91
+ console.log(` #${issue.number}: ${issue.title} → ${issue.branch}`);
92
+ }
93
+
94
+ // -------------------------------------------------------------------------
95
+ // Phase 2: Execute
96
+ //
97
+ // Spawn one sonnet agent per issue, all running concurrently.
98
+ // Each agent works on its own branch so there are no conflicts during
99
+ // execution — merging happens in Phase 3.
100
+ //
101
+ // Promise.allSettled means one failing agent doesn't cancel the others.
102
+ // -------------------------------------------------------------------------
103
+ const settled = await Promise.allSettled(
104
+ issues.map((issue) =>
105
+ sandcastle.run({
106
+ hooks,
107
+ copyToSandbox,
108
+ name: "implementer",
109
+ // Give each agent plenty of room to implement and iterate on tests.
110
+ maxIterations: 100,
111
+ // Sonnet for execution: fast and capable enough for typical issue work.
112
+ model: "claude-sonnet-4-6",
113
+ promptFile: "./.sandcastle/implement-prompt.md",
114
+ // Prompt arguments substitute {{ISSUE_NUMBER}}, {{ISSUE_TITLE}},
115
+ // and {{BRANCH}} placeholders in implement-prompt.md before the
116
+ // agent sees the prompt.
117
+ promptArgs: {
118
+ ISSUE_NUMBER: String(issue.number),
119
+ ISSUE_TITLE: issue.title,
120
+ BRANCH: issue.branch,
121
+ },
122
+ // Each agent starts on its own branch.
123
+ branch: issue.branch,
124
+ }),
125
+ ),
126
+ );
127
+
128
+ // Log any agents that threw (network error, sandbox crash, etc.).
129
+ for (const [i, outcome] of settled.entries()) {
130
+ if (outcome.status === "rejected") {
131
+ console.error(
132
+ ` ✗ #${issues[i]!.number} (${issues[i]!.branch}) failed: ${outcome.reason}`,
133
+ );
134
+ }
135
+ }
136
+
137
+ // Only pass branches that actually produced commits to the merge phase.
138
+ // An agent that ran successfully but made no commits has nothing to merge.
139
+ const completedIssues = settled
140
+ .map((outcome, i) => ({ outcome, issue: issues[i]! }))
141
+ .filter(
142
+ (
143
+ entry,
144
+ ): entry is {
145
+ outcome: PromiseFulfilledResult<
146
+ Awaited<ReturnType<typeof sandcastle.run>>
147
+ >;
148
+ issue: (typeof issues)[number];
149
+ } =>
150
+ entry.outcome.status === "fulfilled" &&
151
+ entry.outcome.value.commits.length > 0,
152
+ )
153
+ .map((entry) => entry.issue);
154
+
155
+ const completedBranches = completedIssues.map((i) => i.branch);
156
+
157
+ console.log(
158
+ `\nExecution complete. ${completedBranches.length} branch(es) with commits:`,
159
+ );
160
+ for (const branch of completedBranches) {
161
+ console.log(` ${branch}`);
162
+ }
163
+
164
+ if (completedBranches.length === 0) {
165
+ // All agents ran but none made commits — nothing to merge this cycle.
166
+ console.log("No commits produced. Nothing to merge.");
167
+ continue;
168
+ }
169
+
170
+ // -------------------------------------------------------------------------
171
+ // Phase 3: Merge
172
+ //
173
+ // One sonnet agent merges all completed branches into the current branch,
174
+ // resolving any conflicts and running tests to confirm everything still works.
175
+ //
176
+ // The {{BRANCHES}} and {{ISSUES}} prompt arguments are lists that the agent
177
+ // uses to know which branches to merge and which issues to close.
178
+ // -------------------------------------------------------------------------
179
+ await sandcastle.run({
180
+ hooks,
181
+ copyToSandbox,
182
+ name: "merger",
183
+ maxIterations: 10,
184
+ // Sonnet is sufficient for merge conflict resolution.
185
+ model: "claude-sonnet-4-6",
186
+ promptFile: "./.sandcastle/merge-prompt.md",
187
+ promptArgs: {
188
+ // A markdown list of branch names, one per line.
189
+ BRANCHES: completedBranches.map((b) => `- ${b}`).join("\n"),
190
+ // A markdown list of issue numbers and titles, one per line.
191
+ ISSUES: completedIssues
192
+ .map((i) => `- #${i.number}: ${i.title}`)
193
+ .join("\n"),
194
+ },
195
+ });
196
+
197
+ console.log("\nBranches merged.");
198
+ }
199
+
200
+ console.log("\nAll done.");
@@ -0,0 +1,22 @@
1
+ # TASK
2
+
3
+ Merge the following branches into the current branch:
4
+
5
+ {{BRANCHES}}
6
+
7
+ For each branch:
8
+
9
+ 1. Run `git merge <branch> --no-edit`
10
+ 2. If there are merge conflicts, resolve them intelligently by reading both sides and choosing the correct resolution
11
+ 3. After resolving conflicts, run `npm run typecheck` and `npx vitest run` to verify everything works
12
+ 4. If tests fail, fix the issues before proceeding to the next branch
13
+
14
+ After all branches are merged, make a single commit summarizing the merge.
15
+
16
+ # CLOSE ISSUES
17
+
18
+ For each branch that was merged, close its issue. Here are all the issues:
19
+
20
+ {{ISSUES}}
21
+
22
+ Once you've merged everything you can, output <promise>COMPLETE</promise>.
@@ -0,0 +1,33 @@
1
+ # ISSUES
2
+
3
+ Here are the open issues in the repo:
4
+
5
+ <issues-json>
6
+
7
+ !`gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'`
8
+
9
+ </issues-json>
10
+
11
+ # TASK
12
+
13
+ Analyze the open issues and build a dependency graph. For each issue, determine whether it **blocks** or **is blocked by** any other open issue.
14
+
15
+ An issue B is **blocked by** issue A if:
16
+
17
+ - B requires code or infrastructure that A introduces
18
+ - B and A modify overlapping files or modules, making concurrent work likely to produce merge conflicts
19
+ - B's requirements depend on a decision or API shape that A will establish
20
+
21
+ An issue is **unblocked** if it has zero blocking dependencies on other open issues.
22
+
23
+ For each unblocked issue, assign a branch name using the format `sandcastle/issue-{number}-{slug}`.
24
+
25
+ # OUTPUT
26
+
27
+ Output your plan as a JSON object wrapped in `<plan>` tags:
28
+
29
+ <plan>
30
+ {"issues": [{"number": 42, "title": "Fix auth bug", "branch": "sandcastle/issue-42-fix-auth-bug"}]}
31
+ </plan>
32
+
33
+ Include only unblocked issues. If every issue is blocked, include the single highest-priority candidate (the one with the fewest or weakest dependencies).
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "parallel-planner",
3
+ "description": "Plans parallelizable issues, executes on separate branches, merges"
4
+ }