@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.
- package/README.md +284 -0
- package/dist/AgentProvider.d.ts +8 -0
- package/dist/AgentProvider.d.ts.map +1 -0
- package/dist/AgentProvider.js +60 -0
- package/dist/AgentProvider.js.map +1 -0
- package/dist/Config.d.ts +35 -0
- package/dist/Config.d.ts.map +1 -0
- package/dist/Config.js +56 -0
- package/dist/Config.js.map +1 -0
- package/dist/CopyToSandbox.d.ts +8 -0
- package/dist/CopyToSandbox.d.ts.map +1 -0
- package/dist/CopyToSandbox.js +32 -0
- package/dist/CopyToSandbox.js.map +1 -0
- package/dist/Display.d.ts +52 -0
- package/dist/Display.d.ts.map +1 -0
- package/dist/Display.js +132 -0
- package/dist/Display.js.map +1 -0
- package/dist/DockerLifecycle.d.ts +37 -0
- package/dist/DockerLifecycle.d.ts.map +1 -0
- package/dist/DockerLifecycle.js +109 -0
- package/dist/DockerLifecycle.js.map +1 -0
- package/dist/DockerSandbox.d.ts +6 -0
- package/dist/DockerSandbox.d.ts.map +1 -0
- package/dist/DockerSandbox.js +122 -0
- package/dist/DockerSandbox.js.map +1 -0
- package/dist/EnvResolver.d.ts +11 -0
- package/dist/EnvResolver.d.ts.map +1 -0
- package/dist/EnvResolver.js +43 -0
- package/dist/EnvResolver.js.map +1 -0
- package/dist/ErrorHandler.d.ts +15 -0
- package/dist/ErrorHandler.d.ts.map +1 -0
- package/dist/ErrorHandler.js +58 -0
- package/dist/ErrorHandler.js.map +1 -0
- package/dist/FilesystemSandbox.d.ts +6 -0
- package/dist/FilesystemSandbox.d.ts.map +1 -0
- package/dist/FilesystemSandbox.js +83 -0
- package/dist/FilesystemSandbox.js.map +1 -0
- package/dist/InitService.d.ts +11 -0
- package/dist/InitService.d.ts.map +1 -0
- package/dist/InitService.js +111 -0
- package/dist/InitService.js.map +1 -0
- package/dist/Orchestrator.d.ts +49 -0
- package/dist/Orchestrator.d.ts.map +1 -0
- package/dist/Orchestrator.js +155 -0
- package/dist/Orchestrator.js.map +1 -0
- package/dist/PromptArgumentSubstitution.d.ts +6 -0
- package/dist/PromptArgumentSubstitution.d.ts.map +1 -0
- package/dist/PromptArgumentSubstitution.js +33 -0
- package/dist/PromptArgumentSubstitution.js.map +1 -0
- package/dist/PromptPreprocessor.d.ts +7 -0
- package/dist/PromptPreprocessor.d.ts.map +1 -0
- package/dist/PromptPreprocessor.js +34 -0
- package/dist/PromptPreprocessor.js.map +1 -0
- package/dist/PromptResolver.d.ts +9 -0
- package/dist/PromptResolver.d.ts.map +1 -0
- package/dist/PromptResolver.js +26 -0
- package/dist/PromptResolver.js.map +1 -0
- package/dist/RecoveryMessage.d.ts +15 -0
- package/dist/RecoveryMessage.d.ts.map +1 -0
- package/dist/RecoveryMessage.js +81 -0
- package/dist/RecoveryMessage.js.map +1 -0
- package/dist/Sandbox.d.ts +23 -0
- package/dist/Sandbox.d.ts.map +1 -0
- package/dist/Sandbox.js +5 -0
- package/dist/Sandbox.js.map +1 -0
- package/dist/SandboxFactory.d.ts +56 -0
- package/dist/SandboxFactory.d.ts.map +1 -0
- package/dist/SandboxFactory.js +219 -0
- package/dist/SandboxFactory.js.map +1 -0
- package/dist/SandboxLifecycle.d.ts +32 -0
- package/dist/SandboxLifecycle.d.ts.map +1 -0
- package/dist/SandboxLifecycle.js +152 -0
- package/dist/SandboxLifecycle.js.map +1 -0
- package/dist/SyncService.d.ts +20 -0
- package/dist/SyncService.d.ts.map +1 -0
- package/dist/SyncService.js +504 -0
- package/dist/SyncService.js.map +1 -0
- package/dist/TokenResolver.d.ts +6 -0
- package/dist/TokenResolver.d.ts.map +1 -0
- package/dist/TokenResolver.js +43 -0
- package/dist/TokenResolver.js.map +1 -0
- package/dist/WorktreeManager.d.ts +42 -0
- package/dist/WorktreeManager.d.ts.map +1 -0
- package/dist/WorktreeManager.js +170 -0
- package/dist/WorktreeManager.js.map +1 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +217 -0
- package/dist/cli.js.map +1 -0
- package/dist/errors.d.ts +95 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +16 -0
- package/dist/main.js.map +1 -0
- package/dist/run.d.ts +91 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +155 -0
- package/dist/run.js.map +1 -0
- package/dist/templates/blank/main.ts +9 -0
- package/dist/templates/blank/prompt.md +12 -0
- package/dist/templates/blank/template.json +4 -0
- package/dist/templates/parallel-planner/implement-prompt.md +62 -0
- package/dist/templates/parallel-planner/main.ts +200 -0
- package/dist/templates/parallel-planner/merge-prompt.md +22 -0
- package/dist/templates/parallel-planner/plan-prompt.md +33 -0
- package/dist/templates/parallel-planner/template.json +4 -0
- package/dist/templates/sequential-reviewer/implement-prompt.md +62 -0
- package/dist/templates/sequential-reviewer/main.ts +102 -0
- package/dist/templates/sequential-reviewer/review-prompt.md +43 -0
- package/dist/templates/sequential-reviewer/template.json +4 -0
- package/dist/templates/simple-loop/main.ts +37 -0
- package/dist/templates/simple-loop/prompt.md +51 -0
- package/dist/templates/simple-loop/template.json +4 -0
- package/dist/templates.d.ts +2 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +26 -0
- package/dist/templates.js.map +1 -0
- package/dist/terminalCleanup.d.ts +30 -0
- package/dist/terminalCleanup.d.ts.map +1 -0
- package/dist/terminalCleanup.js +37 -0
- package/dist/terminalCleanup.js.map +1 -0
- package/dist/testSandbox.d.ts +8 -0
- package/dist/testSandbox.d.ts.map +1 -0
- package/dist/testSandbox.js +101 -0
- package/dist/testSandbox.js.map +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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
|
package/dist/main.js.map
ADDED
|
@@ -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
|
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,YAAY,EACZ,OAAO,EACP,WAAW,GAEZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAEL,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAEzC,sDAAsD;AACtD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC,oFAAoF;AACpF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAAc,EAAU,EAAE,CAClE,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;AAQvC;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,OAAkC,EAC5B,EAAE;IACR,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC;IAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,IAAI,GAAG,CAAC,CAAC;IAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,MAAM,eAAe,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,WAAW,UAAU,EAAE,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,aAAa,eAAe,EAAE,CAAC,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAe,EAAU,EAAE;IAC1D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC;IACxE,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;IACtE,OAAO,cAAc,SAAS,EAAE,CAAC;AACnC,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,cAAsB,EACtB,YAAqB,EACb,EAAE;IACV,MAAM,SAAS,GAAG,yBAAyB,CAAC,cAAc,CAAC,CAAC;IAC5D,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,GAAG,yBAAyB,CAAC,YAAY,CAAC,IAAI,SAAS,MAAM,CAAC;IACvE,CAAC;IACD,OAAO,GAAG,SAAS,MAAM,CAAC;AAC5B,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,2BAAoC,EACpC,aAAqB,EACsC,EAAE;IAC7D,IAAI,2BAA2B,EAAE,CAAC;QAChC,OAAO;YACL,OAAO,EAAE,sCAAsC,aAAa,gBAAgB;YAC5E,QAAQ,EAAE,SAAS;SACpB,CAAC;IACJ,CAAC;IACD,OAAO;QACL,OAAO,EAAE,yBAAyB,aAAa,0CAA0C;QACzF,QAAQ,EAAE,MAAM;KACjB,CAAC;AACJ,CAAC,CAAC;AAiDF,MAAM,CAAC,MAAM,GAAG,GAAG,KAAK,EAAE,OAAmB,EAAsB,EAAE;IACnE,MAAM,EACJ,MAAM,EACN,UAAU,EACV,aAAa,GAAG,sBAAsB,EACtC,KAAK,EACL,MAAM,EACN,KAAK,EACL,KAAK,GACN,GAAG,OAAO,CAAC;IAEZ,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAElC,iBAAiB;IACjB,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,UAAU,CACvC,aAAa,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,IAAI,CACxC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAClC,CACF,CAAC;IAEF,2CAA2C;IAC3C,MAAM,aAAa,GAAG,KAAK,CAAC;IAE5B,oDAAoD;IACpD,MAAM,SAAS,GAAG,KAAK,IAAI,aAAa,CAAC;IACzC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAE7C,gDAAgD;IAChD,MAAM,iBAAiB,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAE7E,mBAAmB;IACnB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,UAAU,CACjC,UAAU,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAChE,CAAC;IAEF,gEAAgE;IAChE,iEAAiE;IACjE,2EAA2E;IAC3E,MAAM,cAAc,GAAG,MAAM,IAAI,sBAAsB,CAAC,SAAS,CAAC,CAAC;IAEnE,2EAA2E;IAC3E,gFAAgF;IAChF,MAAM,YAAY,GAChB,MAAM,KAAK,SAAS;QAClB,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC;QACxD,CAAC,CAAC,SAAS,CAAC;IAEhB,yBAAyB;IACzB,MAAM,eAAe,GAAkB,OAAO,CAAC,OAAO,IAAI;QACxD,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,IAAI,CACR,WAAW,EACX,aAAa,EACb,MAAM,EACN,gBAAgB,CAAC,cAAc,EAAE,YAAY,CAAC,CAC/C;KACF,CAAC;IACF,MAAM,YAAY,GAChB,eAAe,CAAC,IAAI,KAAK,MAAM;QAC7B,CAAC,CAAC,CAAC,GAAG,EAAE;YACJ,uBAAuB,CAAC;gBACtB,OAAO,EAAE,eAAe,CAAC,IAAI;gBAC7B,SAAS,EAAE,OAAO,CAAC,IAAI;gBACvB,MAAM,EAAE,cAAc;aACvB,CAAC,CAAC;YACH,OAAO,KAAK,CAAC,OAAO,CAClB,WAAW,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,EACvC,cAAc,CAAC,KAAK,CACrB,CAAC;QACJ,CAAC,CAAC,EAAE;QACN,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC;IAEzB,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAChC,4BAA4B,CAAC,KAAK,EAClC,KAAK,CAAC,QAAQ,CACZ,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE;QACnC,SAAS,EAAE,iBAAiB;QAC5B,GAAG;QACH,WAAW;QACX,oFAAoF;QACpF,2EAA2E;QAC3E,MAAM;QACN,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,SAAS;KACV,CAAC,EACF,cAAc,CAAC,KAAK,EACpB,YAAY,CACb,CACF,CAAC;IAEF,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IAEzD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CACpC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC;QACzB,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC,CAAC;QAC7C,MAAM,IAAI,GAA2B;YACnC,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,iBAAiB;YACxB,gBAAgB,EAAE,MAAM,CAAC,aAAa,CAAC;SACxC,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,GAAG,cAAc,CAAC;QAChC,IAAI,aAAa;YAAE,IAAI,CAAC,OAAO,CAAC,GAAG,aAAa,CAAC;QACjD,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;QAEzC,0EAA0E;QAC1E,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU;YACvC,CAAC,CAAC,KAAK,CAAC,CAAC,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC;YAC5D,CAAC,CAAC,SAAS,CAAC;QAEd,MAAM,iBAAiB,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC;YAC3C,WAAW;YACX,cAAc,EAAE,qBAAqB;YACrC,UAAU,EAAE,aAAa;YACzB,KAAK;YACL,MAAM,EAAE,cAAc;YACtB,MAAM;YACN,KAAK,EAAE,aAAa;YACpB,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;YAC1C,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,IAAI,EAAE,OAAO,CAAC,IAAI;SACnB,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,sBAAsB,CACvC,iBAAiB,CAAC,2BAA2B,EAC7C,iBAAiB,CAAC,aAAa,CAChC,CAAC;QACF,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;QAEzD,OAAO,iBAAiB,CAAC;IAC3B,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAClC,CAAC;IAEF,OAAO;QACL,GAAG,MAAM;QACT,WAAW,EACT,eAAe,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;KACrE,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -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,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).
|