ralph-tool 1.0.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.
package/Dockerfile ADDED
@@ -0,0 +1,40 @@
1
+ FROM node:20-bookworm
2
+
3
+ # System deps
4
+ USER root
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ git bash ca-certificates curl ripgrep openssh-client \
7
+ jq bsdutils bc gosu \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Install GitHub CLI
11
+ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
12
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
13
+ && apt-get update && apt-get install -y gh \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Install Claude Code globally
17
+ RUN npm install -g @anthropic-ai/claude-code
18
+
19
+ # Install ralph-tool globally from source
20
+ COPY . /tmp/ralph-tool
21
+ RUN npm install -g /tmp/ralph-tool && rm -rf /tmp/ralph-tool \
22
+ && ln -sf $(which ralph) /usr/local/bin/ralph
23
+
24
+ # Prepare writable config dirs for the non-root user
25
+ RUN mkdir -p /home/node/.config /home/node/.cache /home/node/.ssh \
26
+ && chown -R node:node /home/node/.config /home/node/.cache /home/node/.ssh
27
+
28
+ # Add GitHub's SSH host key to known_hosts
29
+ RUN ssh-keyscan github.com >> /home/node/.ssh/known_hosts \
30
+ && chown node:node /home/node/.ssh/known_hosts
31
+
32
+ # Add entrypoint script
33
+ COPY entrypoint.sh /entrypoint.sh
34
+ RUN chmod +x /entrypoint.sh
35
+
36
+ WORKDIR /workspace
37
+ ENV TERM=xterm-256color
38
+
39
+ ENTRYPOINT ["/entrypoint.sh"]
40
+ CMD ["bash"]
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Ralph
2
+
3
+ AI-powered task automation for software development. Ralph orchestrates [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to implement features task-by-task — one task per iteration, fresh context every time, automatic PRs when done.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g ralph-tool
9
+ ```
10
+
11
+ > Ralph auto-installs required dependencies (`jq`, `bc`, `claude`) during setup. Docker and `gh` are optional.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ cd my-project
17
+ ralph init
18
+ ```
19
+
20
+ This creates a `.ralph/` directory with all config, templates, and task files. Then choose your workflow:
21
+
22
+ ### Workflow A: Plan from Vision
23
+
24
+ Best for greenfield projects or large feature sets. Define what you want, let Ralph plan the tasks.
25
+
26
+ ```bash
27
+ # 1. Write your project vision (or use the Claude Code role to help)
28
+ # Edit .ralph/config/vision.md
29
+
30
+ # 2. Ralph plans features + creates task specs from your vision
31
+ ralph plan --max 20
32
+
33
+ # 3. Ralph implements everything
34
+ ralph start --max 50
35
+
36
+ # 4. Review & merge PRs as they come
37
+ ralph review
38
+ ```
39
+
40
+ ### Workflow B: Define Tasks Directly
41
+
42
+ Best for adding specific features to an existing project.
43
+
44
+ ```bash
45
+ # 1. Edit .ralph/tasks/prd.json with your features and tasks
46
+ # Optionally create spec files in .ralph/tasks/1_new_tasks/
47
+
48
+ # 2. Configure build/test commands
49
+ # Edit .ralph/tasks/testing-harness.md
50
+
51
+ # 3. Run it
52
+ ralph start --max 10
53
+ ```
54
+
55
+ ## Commands
56
+
57
+ | Command | Description |
58
+ |---------|-------------|
59
+ | `ralph init` | Initialize Ralph in current directory |
60
+ | `ralph start [options]` | Run loop in Docker sandbox (recommended) |
61
+ | `ralph loop [options]` | Run loop directly on your machine |
62
+ | `ralph plan [options]` | Plan features from `vision.md` into tasks |
63
+ | `ralph sandbox` | Open interactive Docker shell |
64
+ | `ralph status` | Show current feature and task progress |
65
+ | `ralph review` | Review latest PR and merge to main |
66
+ | `ralph cleanup [feature]` | Archive completed features |
67
+ | `ralph clear` | Clear iteration logs |
68
+
69
+ ### Options
70
+
71
+ | Flag | Default | Description |
72
+ |------|---------|-------------|
73
+ | `--max N` | 6 (start/loop), 10 (plan) | Maximum iterations |
74
+ | `--model MODEL` | sonnet | Model to use: `sonnet`, `opus`, `haiku` |
75
+ | `--local` | off | Commit locally only, skip branch/push/PR |
76
+
77
+ ## How It Works
78
+
79
+ **Loop mode** (`start` / `loop`):
80
+
81
+ 1. Ralph picks the next incomplete task from `prd.json`
82
+ 2. Reads the task spec (if one exists) and delegates code exploration to subagents
83
+ 3. Implements the code changes, then runs your test harness
84
+ 4. On pass: marks task complete, updates progress. On fail: fixes and retests
85
+ 5. When all tasks in a feature are done → creates a git branch and PR automatically
86
+ 6. Repeat. Each iteration gets fresh context — state lives in files, not memory
87
+
88
+ **Plan mode** (`plan`):
89
+
90
+ 1. Reads your `vision.md` and existing `prd.json`
91
+ 2. Identifies one unplanned area, explores the codebase
92
+ 3. Creates a detailed spec file and adds the feature + tasks to `prd.json`
93
+ 4. Every 5 iterations: runs a cleanup pass to consolidate and reorder
94
+ 5. Repeat until the vision is fully covered
95
+
96
+ ## Claude Code Roles
97
+
98
+ Ralph ships with roles you can use directly in Claude Code (`@` mention the file):
99
+
100
+ | Role | File | Purpose |
101
+ |------|------|---------|
102
+ | Vision Creator | `.ralph/roles/ralph-plan-vision.md` | Helps you write `vision.md` interactively |
103
+ | Feature Planner | `.ralph/roles/ralph-plan-feature.md` | Plans a single feature with you |
104
+ | Helper | `.ralph/roles/ralph-helper.md` | Explains Ralph concepts and usage |
105
+
106
+ ## Project Structure
107
+
108
+ After `ralph init`:
109
+
110
+ ```
111
+ .ralph/
112
+ ├── config/
113
+ │ ├── prompt.md # Loop orchestrator prompt
114
+ │ ├── plan-prompt.md # Planning orchestrator prompt
115
+ │ ├── plan-cleanup-prompt.md # Planning cleanup prompt
116
+ │ ├── AGENT.md # Persistent learnings (accumulates over time)
117
+ │ ├── vision.md # Your project vision (for plan mode)
118
+ │ └── pr-review-prompt.md # PR review template
119
+ ├── tasks/
120
+ │ ├── prd.json # Features and tasks definition
121
+ │ ├── progress.txt # Current iteration progress
122
+ │ ├── testing-harness.md # Build/test commands
123
+ │ ├── feature-spec-template.md
124
+ │ ├── 1_new_tasks/ # Active task spec files
125
+ │ └── 2_done_tasks/ # Archived completed features
126
+ ├── roles/ # Claude Code role files
127
+ └── logs/ # Iteration logs and cost tracking
128
+ ```
129
+
130
+ ## Key Concepts
131
+
132
+ - **One task per iteration** — Ralph completes exactly one task, then exits. The loop script starts a new iteration with fresh context.
133
+ - **File-based state** — All state lives in `prd.json`, `progress.txt`, and `AGENT.md`. No memory between iterations.
134
+ - **AGENT.md** — Persistent knowledge base that accumulates learnings across all iterations and features. Never cleared automatically.
135
+ - **Spec files** — Detailed implementation specs in `1_new_tasks/`. Created by `ralph plan` or manually.
136
+ - **Testing harness** — Your build/test commands in `testing-harness.md`. Ralph runs these after every implementation.
137
+
138
+ ## Requirements
139
+
140
+ - **Node.js** 18+
141
+ - **Claude Code CLI** — `npm install -g @anthropic-ai/claude-code`
142
+ - **Docker** — for `ralph start` / `ralph sandbox` (optional if using `ralph loop`)
143
+ - **GitHub CLI** (`gh`) — for PR creation and `ralph review` (optional with `--local`)
144
+ - **jq**, **bc** — auto-installed during `npm install`
145
+
146
+ ## License
147
+
148
+ MIT
package/bin/ralph.js ADDED
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const { spawn, spawnSync } = require('child_process');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const chalk = require('chalk');
8
+
9
+ // Colors matching culinary cloud style
10
+ const cmd = chalk.bold.magenta;
11
+ const title = chalk.bold.cyan;
12
+ const dim = chalk.dim;
13
+
14
+ // Resolve paths
15
+ const RALPH_HOME = path.resolve(__dirname, '..');
16
+ const PROJECT_DIR = process.cwd();
17
+ const PROJECT_RALPH_DIR = path.join(PROJECT_DIR, '.ralph');
18
+
19
+ // Set environment variables for child scripts
20
+ process.env.RALPH_HOME = RALPH_HOME;
21
+ process.env.PROJECT_DIR = PROJECT_DIR;
22
+ process.env.PROJECT_RALPH_DIR = PROJECT_RALPH_DIR;
23
+
24
+ // Helper: Check if .ralph exists
25
+ function requireInit() {
26
+ if (!fs.existsSync(PROJECT_RALPH_DIR)) {
27
+ console.error(chalk.red('Error: .ralph/ not found in current directory'));
28
+ console.log('');
29
+ console.log("Run 'ralph init' first to initialize Ralph in this project.");
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ // Helper: Run a bash script (async — for non-interactive commands)
35
+ function runScript(scriptName, args = []) {
36
+ const scriptPath = path.join(RALPH_HOME, 'scripts', scriptName);
37
+
38
+ if (!fs.existsSync(scriptPath)) {
39
+ console.error(chalk.red(`Error: Script not found: ${scriptPath}`));
40
+ process.exit(1);
41
+ }
42
+
43
+ const child = spawn('bash', [scriptPath, ...args], {
44
+ stdio: 'inherit',
45
+ env: { ...process.env, RALPH_HOME, PROJECT_DIR, PROJECT_RALPH_DIR }
46
+ });
47
+
48
+ child.on('close', (code) => {
49
+ process.exit(code || 0);
50
+ });
51
+ }
52
+
53
+ // Helper: Run a bash script synchronously (for interactive/TTY commands like sandbox)
54
+ function runScriptSync(scriptName, args = []) {
55
+ const scriptPath = path.join(RALPH_HOME, 'scripts', scriptName);
56
+
57
+ if (!fs.existsSync(scriptPath)) {
58
+ console.error(chalk.red(`Error: Script not found: ${scriptPath}`));
59
+ process.exit(1);
60
+ }
61
+
62
+ const result = spawnSync('bash', [scriptPath, ...args], {
63
+ stdio: 'inherit',
64
+ env: { ...process.env, RALPH_HOME, PROJECT_DIR, PROJECT_RALPH_DIR }
65
+ });
66
+
67
+ process.exit(result.status || 0);
68
+ }
69
+
70
+ // Helper: Copy directory recursively
71
+ function copyDir(src, dest) {
72
+ fs.mkdirSync(dest, { recursive: true });
73
+ const entries = fs.readdirSync(src, { withFileTypes: true });
74
+
75
+ for (const entry of entries) {
76
+ const srcPath = path.join(src, entry.name);
77
+ const destPath = path.join(dest, entry.name);
78
+
79
+ if (entry.isDirectory()) {
80
+ copyDir(srcPath, destPath);
81
+ } else {
82
+ fs.copyFileSync(srcPath, destPath);
83
+ }
84
+ }
85
+ }
86
+
87
+ // Init command
88
+ function doInit() {
89
+ if (fs.existsSync(PROJECT_RALPH_DIR)) {
90
+ console.log(chalk.yellow('Warning: .ralph/ already exists in this directory'));
91
+ const readline = require('readline');
92
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
93
+
94
+ rl.question('Overwrite? (y/N) ', (answer) => {
95
+ rl.close();
96
+ if (answer.toLowerCase() !== 'y') {
97
+ console.log('Aborted.');
98
+ process.exit(0);
99
+ }
100
+ performInit();
101
+ });
102
+ } else {
103
+ performInit();
104
+ }
105
+ }
106
+
107
+ function performInit() {
108
+ console.log(chalk.green(`Initializing Ralph in ${PROJECT_DIR}`));
109
+ console.log('');
110
+
111
+ // Create directory structure
112
+ console.log('[1/3] Creating directory structure...');
113
+ fs.mkdirSync(path.join(PROJECT_RALPH_DIR, 'config'), { recursive: true });
114
+ fs.mkdirSync(path.join(PROJECT_RALPH_DIR, 'tasks', '1_new_tasks'), { recursive: true });
115
+ fs.mkdirSync(path.join(PROJECT_RALPH_DIR, 'tasks', '2_done_tasks'), { recursive: true });
116
+ fs.mkdirSync(path.join(PROJECT_RALPH_DIR, 'logs', 'progress'), { recursive: true });
117
+ fs.mkdirSync(path.join(PROJECT_RALPH_DIR, 'roles'), { recursive: true });
118
+ console.log(chalk.green(' ✓') + ' Directories created');
119
+
120
+ // Copy templates
121
+ console.log('[2/3] Copying templates...');
122
+ const templatesDir = path.join(RALPH_HOME, 'templates');
123
+
124
+ // Config files
125
+ fs.copyFileSync(
126
+ path.join(templatesDir, 'prompt.md'),
127
+ path.join(PROJECT_RALPH_DIR, 'config', 'prompt.md')
128
+ );
129
+ fs.copyFileSync(
130
+ path.join(templatesDir, 'AGENT.md'),
131
+ path.join(PROJECT_RALPH_DIR, 'config', 'AGENT.md')
132
+ );
133
+ fs.copyFileSync(
134
+ path.join(templatesDir, 'pr-review-prompt.md'),
135
+ path.join(PROJECT_RALPH_DIR, 'config', 'pr-review-prompt.md')
136
+ );
137
+
138
+ // Task files
139
+ fs.copyFileSync(
140
+ path.join(templatesDir, 'testing-harness.md'),
141
+ path.join(PROJECT_RALPH_DIR, 'tasks', 'testing-harness.md')
142
+ );
143
+ fs.copyFileSync(
144
+ path.join(templatesDir, 'feature-spec-template.md'),
145
+ path.join(PROJECT_RALPH_DIR, 'tasks', 'feature-spec-template.md')
146
+ );
147
+ fs.copyFileSync(
148
+ path.join(templatesDir, 'prd.json'),
149
+ path.join(PROJECT_RALPH_DIR, 'tasks', 'prd.json')
150
+ );
151
+
152
+ // Plan mode files
153
+ fs.copyFileSync(
154
+ path.join(templatesDir, 'vision.md'),
155
+ path.join(PROJECT_RALPH_DIR, 'config', 'vision.md')
156
+ );
157
+ fs.copyFileSync(
158
+ path.join(templatesDir, 'plan-prompt.md'),
159
+ path.join(PROJECT_RALPH_DIR, 'config', 'plan-prompt.md')
160
+ );
161
+ fs.copyFileSync(
162
+ path.join(templatesDir, 'plan-cleanup-prompt.md'),
163
+ path.join(PROJECT_RALPH_DIR, 'config', 'plan-cleanup-prompt.md')
164
+ );
165
+
166
+ // Role files
167
+ copyDir(
168
+ path.join(templatesDir, 'roles'),
169
+ path.join(PROJECT_RALPH_DIR, 'roles')
170
+ );
171
+
172
+ // Create empty progress file
173
+ fs.writeFileSync(path.join(PROJECT_RALPH_DIR, 'tasks', 'progress.txt'), '');
174
+
175
+ console.log(chalk.green(' ✓') + ' Templates copied');
176
+
177
+ // Done
178
+ console.log('[3/3] Done!');
179
+ console.log('');
180
+ console.log(chalk.green('Ralph initialized successfully!'));
181
+ console.log('');
182
+ console.log('Next steps:');
183
+ console.log(' 1. Plan a feature: @.ralph/roles/ralph-plan-feature.md in Claude Code');
184
+ console.log(' 2. Edit .ralph/tasks/testing-harness.md for your build/test commands');
185
+ console.log(' 3. Run \'ralph start\' to begin');
186
+ console.log('');
187
+ }
188
+
189
+ // Custom help display
190
+ function showHelp() {
191
+ console.log('');
192
+ console.log(title('Ralph - AI Task Automation CLI'));
193
+ console.log('');
194
+ console.log(`Usage: ralph ${dim('{command} [options]')}`);
195
+ console.log('');
196
+ console.log('Commands:');
197
+ console.log(` ${cmd('init')}`);
198
+ console.log(' Initialize Ralph in current directory (creates .ralph/)');
199
+ console.log('');
200
+ console.log(` ${cmd('start')} ${dim('[--max N] [--model MODEL] [--local]')}`);
201
+ console.log(' Open Docker sandbox and start loop inside container');
202
+ console.log('');
203
+ console.log(` ${cmd('sandbox')}`);
204
+ console.log(' Open interactive Docker shell');
205
+ console.log('');
206
+ console.log(` ${cmd('loop')} ${dim('[--max N] [--model MODEL] [--local]')}`);
207
+ console.log(' Start Ralph loop directly (default: 6 iterations, sonnet-4-5)');
208
+ console.log('');
209
+ console.log(` ${cmd('plan')} ${dim('[--max N] [--model MODEL]')}`);
210
+ console.log(' Plan features from vision.md (default: 10 iterations, cleanup every 5)');
211
+ console.log('');
212
+ console.log(` ${cmd('status')}`);
213
+ console.log(' Check current PRD task status');
214
+ console.log('');
215
+ console.log(` ${cmd('clear')}`);
216
+ console.log(' Clear all log files (keeps progress)');
217
+ console.log('');
218
+ console.log(` ${cmd('review')}`);
219
+ console.log(' Review latest PR, merge to main');
220
+ console.log('');
221
+ console.log(` ${cmd('cleanup')} ${dim('[feature]')}`);
222
+ console.log(' Archive completed features');
223
+ console.log('');
224
+ console.log(` ${cmd('help')}`);
225
+ console.log(' Show this help message');
226
+ console.log('');
227
+ console.log('Options:');
228
+ console.log(` ${cmd('--max N')} Maximum iterations (default: 6)`);
229
+ console.log(` ${cmd('--model MODEL')} AI model: sonnet, opus, haiku (default: sonnet)`);
230
+ console.log(` ${cmd('--local')} Commit locally, skip branch/push/PR creation`);
231
+ console.log('');
232
+ console.log('Examples:');
233
+ console.log(` ralph ${cmd('init')} # Set up Ralph in your project`);
234
+ console.log(` ralph ${cmd('start')} --max 5 --model haiku # Run in Docker with haiku`);
235
+ console.log(` ralph ${cmd('loop')} # Run loop directly`);
236
+ console.log(` ralph ${cmd('loop')} --max 10 --model opus # 10 iterations with opus`);
237
+ console.log(` ralph ${cmd('plan')} # Plan features from vision`);
238
+ console.log(` ralph ${cmd('plan')} --max 20 --model opus # Deep planning with opus`);
239
+ console.log(` ralph ${cmd('status')} # Check task progress`);
240
+ console.log('');
241
+ }
242
+
243
+ // Setup CLI — disable commander's built-in help entirely
244
+ program
245
+ .name('ralph')
246
+ .version('1.0.0')
247
+ .helpOption(false)
248
+ .addHelpCommand(false)
249
+ .configureOutput({
250
+ outputError: () => {}, // suppress commander error output
251
+ });
252
+
253
+ program
254
+ .command('init')
255
+ .description('Initialize Ralph in current directory')
256
+ .action(doInit);
257
+
258
+ program
259
+ .command('start')
260
+ .option('--max <n>', 'Maximum iterations', '6')
261
+ .option('--model <model>', 'AI model: sonnet, opus, haiku')
262
+ .option('--local', 'Local mode: commit but skip branch/push/PR creation')
263
+ .action((options) => {
264
+ requireInit();
265
+ if (options.local) process.env.RALPH_LOCAL = '1';
266
+ console.log(chalk.dim('Starting Docker container...'));
267
+ runScriptSync('start.sh', [options.max, options.model || '']);
268
+ });
269
+
270
+ program
271
+ .command('sandbox')
272
+ .action(() => {
273
+ requireInit();
274
+ console.log(chalk.green('Opening Docker sandbox...'));
275
+ runScriptSync('sandbox.sh');
276
+ });
277
+
278
+ program
279
+ .command('loop')
280
+ .option('--max <n>', 'Maximum iterations', '6')
281
+ .option('--model <model>', 'AI model: sonnet, opus, haiku')
282
+ .option('--local', 'Local mode: commit but skip branch/push/PR creation')
283
+ .action((options) => {
284
+ requireInit();
285
+ if (options.local) process.env.RALPH_LOCAL = '1';
286
+ console.log(chalk.dim('Starting Ralph loop...'));
287
+ const promptFile = path.join(PROJECT_RALPH_DIR, 'config', 'prompt.md');
288
+ runScript('loop.sh', [promptFile, options.max, options.model || '']);
289
+ });
290
+
291
+ program
292
+ .command('plan')
293
+ .option('--max <n>', 'Maximum iterations', '10')
294
+ .option('--model <model>', 'AI model: sonnet, opus, haiku')
295
+ .action((options) => {
296
+ requireInit();
297
+ const visionFile = path.join(PROJECT_RALPH_DIR, 'config', 'vision.md');
298
+ if (!fs.existsSync(visionFile)) {
299
+ console.error(chalk.red('Error: Vision file not found at .ralph/config/vision.md'));
300
+ console.log('');
301
+ console.log('Create your vision first:');
302
+ console.log(' Use @.ralph/roles/ralph-plan-vision.md in Claude Code');
303
+ process.exit(1);
304
+ }
305
+ console.log(chalk.dim('Starting Ralph planning loop...'));
306
+ runScript('plan-loop.sh', [options.max, options.model || '']);
307
+ });
308
+
309
+ program
310
+ .command('status')
311
+ .action(() => {
312
+ requireInit();
313
+ runScript('status.sh');
314
+ });
315
+
316
+ program
317
+ .command('clear')
318
+ .action(() => {
319
+ requireInit();
320
+ runScript('clear.sh');
321
+ });
322
+
323
+ program
324
+ .command('review')
325
+ .action(() => {
326
+ requireInit();
327
+ runScript('review.sh');
328
+ });
329
+
330
+ program
331
+ .command('cleanup [feature]')
332
+ .action((feature) => {
333
+ requireInit();
334
+ runScript('cleanup.sh', feature ? [feature] : []);
335
+ });
336
+
337
+ program
338
+ .command('help')
339
+ .action(() => showHelp());
340
+
341
+ // Intercept before commander parses: handle no args, help, and --help
342
+ const args = process.argv.slice(2);
343
+ const firstArg = args[0];
344
+
345
+ if (!firstArg || firstArg === 'help' || firstArg === '--help' || firstArg === '-h') {
346
+ showHelp();
347
+ process.exit(0);
348
+ }
349
+
350
+ // Unknown commands → show help
351
+ program.on('command:*', () => {
352
+ console.error(chalk.red(`Error: Unknown command '${args[0]}'`));
353
+ showHelp();
354
+ process.exit(1);
355
+ });
356
+
357
+ program.parse();
@@ -0,0 +1,40 @@
1
+ services:
2
+ ai:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ working_dir: /workspace
7
+ environment:
8
+ - TERM=xterm-256color
9
+ - CLAUDE_CONFIG_DIR=/home/node/.config/claude
10
+ - SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock
11
+ - RALPH_HOME=/ralph-tool
12
+ - GIT_USER_NAME=${GIT_USER_NAME:-}
13
+ - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}
14
+ volumes:
15
+ # Project directory mounted to /workspace (set by ralph CLI)
16
+ # This is handled dynamically by the start.sh script
17
+
18
+ # Ralph tool installation
19
+ - .:/ralph-tool:ro
20
+
21
+ # Persistent volumes for configs
22
+ - claude_config:/home/node/.config/claude
23
+ - claude_workspace:/workspace/.claude
24
+ - gh_config:/home/node/.config/gh
25
+
26
+ # SSH agent for git operations
27
+ - type: bind
28
+ source: /run/host-services/ssh-auth.sock
29
+ target: /run/host-services/ssh-auth.sock
30
+
31
+ # Git config (read-only)
32
+ - ~/.gitconfig:/home/node/.gitconfig:ro
33
+
34
+ stdin_open: true
35
+ tty: true
36
+
37
+ volumes:
38
+ claude_config:
39
+ claude_workspace:
40
+ gh_config:
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # Constants for Ralph Loop
3
+
4
+ # Configuration constants
5
+ MAX_CHARS="${MAX_CHARS:-4000}"
6
+ MAX_LINES_FOR_TOOL_RESULT=4
7
+ DEFAULT_MODEL="claude-sonnet-4.5"
8
+
9
+ # Fun names for Ralph
10
+ RALPH_NAMES=(
11
+ "YOLO RALPH COOKING"
12
+ "RALPH IN THE MATRIX"
13
+ "RALPH MODE: ACTIVATED"
14
+ "RALPH.EXE RUNNING"
15
+ "EMPLOYEE OF THE MONTH RALPH"
16
+ "RALPH IS SPEEEEEEEEEED"
17
+ "BUSINESS MONEY GETTING BURNED BY RAAAALLLPPPHHHHH"
18
+ "CRACKHEAD RALPH TO THE RESCUE"
19
+ "BUSINESSSSSSS ISS RAAAAALLLLPPPPHHHH"
20
+ "RALPH: PROCESSING..."
21
+ "RALPH COOKING SOME TOOOKKKEEENNNNN FOR BUSINESSSSSSS"
22
+ )
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # Cost calculation helpers for Ralph loop
3
+
4
+ # Model pricing lookup (bash 3.2 compatible — no associative arrays)
5
+ # Returns: input_base cache_write_5m cache_write_1h cache_hit output (per million tokens)
6
+ get_model_pricing() {
7
+ case "$1" in
8
+ claude-opus-4-5) echo "5.0 6.25 10.0 0.50 25.0" ;;
9
+ claude-opus-4-1) echo "15.0 18.75 30.0 1.50 75.0" ;;
10
+ claude-opus-4) echo "15.0 18.75 30.0 1.50 75.0" ;;
11
+ claude-sonnet-4-5) echo "3.0 3.75 6.0 0.30 15.0" ;;
12
+ claude-sonnet-3-5) echo "3.0 3.75 6.0 0.30 15.0" ;;
13
+ claude-haiku-3) echo "0.25 0.30 1.25 0.03 1.25" ;;
14
+ *) echo "3.0 3.75 6.0 0.30 15.0" ;; # default to sonnet pricing
15
+ esac
16
+ }
17
+
18
+ # Function to normalize model name for pricing lookup
19
+ normalize_model_name() {
20
+ local model="$1"
21
+ echo "$model" | tr '[:upper:]' '[:lower:]' | sed 's/\./-/g' | sed 's/-[0-9]\{8\}$//' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g'
22
+ }
23
+
24
+ # Function to calculate cost based on model and usage
25
+ calculate_cost() {
26
+ local model="$1"
27
+ local input_tokens="$2"
28
+ local output_tokens="$3"
29
+ local cache_read_tokens="$4"
30
+ local cache_write_tokens="$5"
31
+
32
+ local normalized_model
33
+ normalized_model=$(normalize_model_name "$model")
34
+ local pricing
35
+ pricing=$(get_model_pricing "$normalized_model")
36
+
37
+ local input_price cache_write_price cache_hit_price output_price
38
+ input_price=$(echo "$pricing" | awk '{print $1}')
39
+ cache_write_price=$(echo "$pricing" | awk '{print $2}')
40
+ cache_hit_price=$(echo "$pricing" | awk '{print $4}')
41
+ output_price=$(echo "$pricing" | awk '{print $5}')
42
+
43
+ local input_cost output_cost cache_read_cost cache_write_cost total_cost
44
+ input_cost=$(echo "scale=6; ($input_tokens * $input_price) / 1000000" | bc)
45
+ output_cost=$(echo "scale=6; ($output_tokens * $output_price) / 1000000" | bc)
46
+ cache_read_cost=$(echo "scale=6; ($cache_read_tokens * $cache_hit_price) / 1000000" | bc)
47
+ cache_write_cost=$(echo "scale=6; ($cache_write_tokens * $cache_write_price) / 1000000" | bc)
48
+
49
+ total_cost=$(echo "scale=6; $input_cost + $output_cost + $cache_read_cost + $cache_write_cost" | bc)
50
+
51
+ echo "$total_cost"
52
+ }
53
+
54
+ # Function to format token count (add commas)
55
+ format_tokens() {
56
+ local tokens="$1"
57
+ if [[ -z "$tokens" ]] || [[ "$tokens" == "null" ]] || [[ "$tokens" == "0" ]]; then
58
+ echo "0"
59
+ else
60
+ echo "$tokens" | awk '{printf "%'"'"'d\n", $1}'
61
+ fi
62
+ }
63
+
64
+ # Function to format cost
65
+ format_cost() {
66
+ local cost="$1"
67
+ if [[ -z "$cost" ]] || [[ "$cost" == "null" ]] || [[ "$cost" == "0" ]]; then
68
+ echo "\$0.00"
69
+ else
70
+ local formatted=$(printf "%.4f" "$cost" | sed 's/0*$//' | sed 's/\.$//')
71
+ printf "\$%s\n" "$formatted"
72
+ fi
73
+ }