@canivel/ralph 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.agents/ralph/PROMPT_build.md +126 -0
  2. package/.agents/ralph/agents.sh +15 -0
  3. package/.agents/ralph/config.sh +25 -0
  4. package/.agents/ralph/log-activity.sh +15 -0
  5. package/.agents/ralph/loop.sh +1001 -0
  6. package/.agents/ralph/references/CONTEXT_ENGINEERING.md +126 -0
  7. package/.agents/ralph/references/GUARDRAILS.md +174 -0
  8. package/AGENTS.md +20 -0
  9. package/README.md +266 -0
  10. package/bin/ralph +766 -0
  11. package/diagram.svg +55 -0
  12. package/examples/commands.md +46 -0
  13. package/package.json +39 -0
  14. package/ralph.webp +0 -0
  15. package/skills/commit/SKILL.md +219 -0
  16. package/skills/commit/references/commit_examples.md +292 -0
  17. package/skills/dev-browser/SKILL.md +211 -0
  18. package/skills/dev-browser/bun.lock +443 -0
  19. package/skills/dev-browser/package-lock.json +2988 -0
  20. package/skills/dev-browser/package.json +31 -0
  21. package/skills/dev-browser/references/scraping.md +155 -0
  22. package/skills/dev-browser/scripts/start-relay.ts +32 -0
  23. package/skills/dev-browser/scripts/start-server.ts +117 -0
  24. package/skills/dev-browser/server.sh +24 -0
  25. package/skills/dev-browser/src/client.ts +474 -0
  26. package/skills/dev-browser/src/index.ts +287 -0
  27. package/skills/dev-browser/src/relay.ts +731 -0
  28. package/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts +223 -0
  29. package/skills/dev-browser/src/snapshot/browser-script.ts +877 -0
  30. package/skills/dev-browser/src/snapshot/index.ts +14 -0
  31. package/skills/dev-browser/src/snapshot/inject.ts +13 -0
  32. package/skills/dev-browser/src/types.ts +34 -0
  33. package/skills/dev-browser/tsconfig.json +36 -0
  34. package/skills/dev-browser/vitest.config.ts +12 -0
  35. package/skills/prd/SKILL.md +235 -0
  36. package/tests/agent-loops.mjs +79 -0
  37. package/tests/agent-ping.mjs +39 -0
  38. package/tests/audit.md +56 -0
  39. package/tests/cli-smoke.mjs +47 -0
  40. package/tests/real-agents.mjs +127 -0
package/bin/ralph ADDED
@@ -0,0 +1,766 @@
1
+ #!/usr/bin/env node
2
+ const { spawnSync } = require("child_process");
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ const rawArgs = process.argv.slice(2);
8
+ const args = [];
9
+ const cwd = process.cwd();
10
+ const repoRoot = path.resolve(__dirname, "..");
11
+ const globalDir = path.join(repoRoot, ".agents", "ralph");
12
+ const localDir = path.join(cwd, ".agents", "ralph");
13
+ const skillsRoot = path.join(repoRoot, "skills");
14
+ let agentOverride = null;
15
+ let installSkills = false;
16
+ let installForce = false;
17
+ let prdPath = null;
18
+ let progressPath = null;
19
+ let prdOutPath = null;
20
+
21
+ function exists(p) {
22
+ try {
23
+ fs.accessSync(p);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function readPackageMeta() {
31
+ try {
32
+ const pkgPath = path.join(repoRoot, "package.json");
33
+ const data = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
34
+ return { name: data.name || "ralph", version: data.version || "0.0.0" };
35
+ } catch {
36
+ return { name: "ralph", version: "0.0.0" };
37
+ }
38
+ }
39
+
40
+ function compareVersions(a, b) {
41
+ const parse = (value) =>
42
+ String(value || "")
43
+ .split(".")
44
+ .map((part) => parseInt(part, 10))
45
+ .map((n) => (Number.isNaN(n) ? 0 : n));
46
+ const pa = parse(a);
47
+ const pb = parse(b);
48
+ const len = Math.max(pa.length, pb.length);
49
+ for (let i = 0; i < len; i += 1) {
50
+ const da = pa[i] || 0;
51
+ const db = pb[i] || 0;
52
+ if (da > db) return 1;
53
+ if (da < db) return -1;
54
+ }
55
+ return 0;
56
+ }
57
+
58
+ function checkForUpdate() {
59
+ if (!process.stdout.isTTY || !process.stdin.isTTY) return null;
60
+ if (process.env.RALPH_SKIP_UPDATE_CHECK === "1") return null;
61
+ const { name, version } = readPackageMeta();
62
+ const result = spawnSync("npm", ["view", name, "version"], { encoding: "utf-8" });
63
+ if (result.status !== 0) return null;
64
+ const latest = String(result.stdout || "").trim();
65
+ if (!latest) return null;
66
+ if (compareVersions(latest, version) <= 0) return null;
67
+ return { name, current: version, latest };
68
+ }
69
+
70
+ function usage() {
71
+ console.log(`ralph <command>
72
+
73
+ Commands:
74
+ install [--skills] [--force] Copy .agents/ralph into the current repo
75
+ prd ["<request>"] [--out path] Generate a PRD (JSON) via agent
76
+ ping Minimal agent health check
77
+ log "<message>" Append to .ralph/activity.log
78
+ build [n] [--no-commit] Run build loop (default)
79
+ overview Render a human overview from PRD JSON
80
+ config Configure default agent
81
+ help Show this message
82
+
83
+ Options:
84
+ --prd <path> Override PRD path
85
+ --out <path> Override PRD output path (prd command)
86
+ --progress <path> Override progress log path
87
+ --agent <codex|claude|droid|opencode> Override agent runner
88
+
89
+ Notes:
90
+ - Uses local .agents/ralph if present; otherwise uses bundled defaults.
91
+ - State and logs are written to .ralph/ in the project.
92
+ - On first run, you'll be prompted to select your default agent.
93
+ `);
94
+ }
95
+
96
+ function getGlobalConfigPath() {
97
+ return path.join(os.homedir(), ".ralph", "config.json");
98
+ }
99
+
100
+ function loadGlobalConfig() {
101
+ const configPath = getGlobalConfigPath();
102
+ try {
103
+ if (exists(configPath)) {
104
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
105
+ }
106
+ } catch {
107
+ // ignore parse errors
108
+ }
109
+ return {};
110
+ }
111
+
112
+ function saveGlobalConfig(config) {
113
+ const configPath = getGlobalConfigPath();
114
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
115
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
116
+ }
117
+
118
+ async function promptForDefaultAgent(currentDefault) {
119
+ const { intro, outro, select, isCancel } = await import("@clack/prompts");
120
+ intro("Ralph Configuration");
121
+
122
+ const agent = await select({
123
+ message: "Select your default agent",
124
+ options: [
125
+ { value: "claude", label: "claude (Anthropic Claude CLI)" },
126
+ { value: "codex", label: "codex (OpenAI Codex CLI)" },
127
+ { value: "droid", label: "droid (Factory Droid CLI)" },
128
+ { value: "opencode", label: "opencode (OpenCode CLI)" },
129
+ ],
130
+ initialValue: currentDefault || "claude",
131
+ });
132
+
133
+ if (isCancel(agent)) {
134
+ outro("Cancelled. Using default: codex");
135
+ return null;
136
+ }
137
+
138
+ outro(`Default agent set to: ${agent}`);
139
+ return agent;
140
+ }
141
+
142
+ async function ensureFirstRunConfig() {
143
+ // Skip if not interactive TTY
144
+ if (!process.stdout.isTTY || !process.stdin.isTTY) return null;
145
+
146
+ const config = loadGlobalConfig();
147
+ if (config.defaultAgent) {
148
+ return config.defaultAgent;
149
+ }
150
+
151
+ // First run - prompt for agent selection
152
+ const agent = await promptForDefaultAgent("claude");
153
+ if (agent) {
154
+ config.defaultAgent = agent;
155
+ config.configuredAt = new Date().toISOString();
156
+ saveGlobalConfig(config);
157
+ }
158
+ return agent;
159
+ }
160
+
161
+ function findPrdCandidates(baseDir) {
162
+ const tasksDir = path.join(baseDir, ".agents", "tasks");
163
+ if (!exists(tasksDir)) {
164
+ return [];
165
+ }
166
+ return fs
167
+ .readdirSync(tasksDir)
168
+ .filter((name) => name.toLowerCase().endsWith(".json"))
169
+ .map((name) => path.join(tasksDir, name));
170
+ }
171
+
172
+ function defaultPrdOutputPath(baseDir, request) {
173
+ return path.join(baseDir, ".agents", "tasks");
174
+ }
175
+
176
+ async function resolvePrdPath({ cwd, explicitPath, label }) {
177
+ if (explicitPath) {
178
+ return path.resolve(cwd, explicitPath);
179
+ }
180
+ const candidates = findPrdCandidates(cwd);
181
+ if (candidates.length === 0) {
182
+ return null;
183
+ }
184
+ if (candidates.length === 1) {
185
+ return candidates[0];
186
+ }
187
+ const { intro, select, outro, isCancel } = await import("@clack/prompts");
188
+ intro(label || "Select PRD");
189
+ const selection = await select({
190
+ message: "Pick a PRD JSON file",
191
+ options: candidates.map((file) => ({
192
+ value: file,
193
+ label: path.relative(cwd, file),
194
+ })),
195
+ });
196
+ if (isCancel(selection)) {
197
+ outro("Cancelled.");
198
+ process.exit(0);
199
+ }
200
+ outro("Selected.");
201
+ return selection;
202
+ }
203
+
204
+ for (let i = 0; i < rawArgs.length; i += 1) {
205
+ const arg = rawArgs[i];
206
+ if (arg.startsWith("--agent=")) {
207
+ agentOverride = arg.split("=").slice(1).join("=");
208
+ continue;
209
+ }
210
+ if (arg === "--agent") {
211
+ agentOverride = rawArgs[i + 1];
212
+ i += 1;
213
+ continue;
214
+ }
215
+ if (arg === "--skills") {
216
+ installSkills = true;
217
+ continue;
218
+ }
219
+ if (arg === "--force") {
220
+ installForce = true;
221
+ continue;
222
+ }
223
+ if (arg.startsWith("--prd=")) {
224
+ prdPath = arg.split("=").slice(1).join("=");
225
+ continue;
226
+ }
227
+ if (arg.startsWith("--out=")) {
228
+ prdOutPath = arg.split("=").slice(1).join("=");
229
+ continue;
230
+ }
231
+ if (arg === "--prd") {
232
+ prdPath = rawArgs[i + 1];
233
+ i += 1;
234
+ continue;
235
+ }
236
+ if (arg === "--out") {
237
+ prdOutPath = rawArgs[i + 1];
238
+ i += 1;
239
+ continue;
240
+ }
241
+ if (arg.startsWith("--progress=")) {
242
+ progressPath = arg.split("=").slice(1).join("=");
243
+ continue;
244
+ }
245
+ if (arg === "--progress") {
246
+ progressPath = rawArgs[i + 1];
247
+ i += 1;
248
+ continue;
249
+ }
250
+ args.push(arg);
251
+ }
252
+
253
+ const cmd = args[0];
254
+ if (cmd === "help" || cmd === "-h" || cmd === "--help") {
255
+ usage();
256
+ process.exit(0);
257
+ }
258
+
259
+ async function runInstallSkills() {
260
+ const { intro, outro, select, isCancel } = await import("@clack/prompts");
261
+ intro("Ralph skills install");
262
+
263
+ const agent = await select({
264
+ message: "Which agent are you using?",
265
+ options: [
266
+ { value: "codex", label: "codex" },
267
+ { value: "claude", label: "claude" },
268
+ { value: "droid", label: "droid" },
269
+ { value: "opencode", label: "opencode" },
270
+ ],
271
+ initialValue: agentOverride || "codex",
272
+ });
273
+ if (isCancel(agent)) {
274
+ outro("Cancelled.");
275
+ process.exit(0);
276
+ }
277
+
278
+ const scope = await select({
279
+ message: "Where should skills be installed?",
280
+ options: [
281
+ { value: "local", label: "Local (project)" },
282
+ { value: "global", label: "Global (home directory)" },
283
+ ],
284
+ initialValue: "local",
285
+ });
286
+ if (isCancel(scope)) {
287
+ outro("Cancelled.");
288
+ process.exit(0);
289
+ }
290
+
291
+ const home = os.homedir();
292
+ const targetRoot =
293
+ agent === "codex"
294
+ ? scope === "global"
295
+ ? path.join(home, ".codex", "skills")
296
+ : path.join(cwd, ".codex", "skills")
297
+ : agent === "claude"
298
+ ? scope === "global"
299
+ ? path.join(home, ".claude", "skills")
300
+ : path.join(cwd, ".claude", "skills")
301
+ : agent === "droid"
302
+ ? scope === "global"
303
+ ? path.join(home, ".factory", "skills")
304
+ : path.join(cwd, ".factory", "skills")
305
+ : scope === "global"
306
+ ? path.join(home, ".local", "share", "opencode", "skills")
307
+ : path.join(cwd, ".opencode", "skills");
308
+
309
+ const skillsToInstall = ["commit", "dev-browser", "prd"];
310
+ fs.mkdirSync(targetRoot, { recursive: true });
311
+ const installed = [];
312
+ const skipped = [];
313
+
314
+ for (const skill of skillsToInstall) {
315
+ const source = path.join(skillsRoot, skill);
316
+ const target = path.join(targetRoot, skill);
317
+ if (!exists(source)) {
318
+ skipped.push(`${skill} (missing in repo)`);
319
+ continue;
320
+ }
321
+ if (exists(target) && !installForce) {
322
+ skipped.push(`${skill} (already exists)`);
323
+ continue;
324
+ }
325
+ fs.cpSync(source, target, { recursive: true, force: true });
326
+ installed.push(skill);
327
+ }
328
+
329
+ if (installed.length) {
330
+ console.log(`Installed skills to ${targetRoot}: ${installed.join(", ")}`);
331
+ }
332
+ if (skipped.length) {
333
+ console.log(`Skipped: ${skipped.join(", ")}`);
334
+ }
335
+ outro("Done.");
336
+ }
337
+
338
+ async function main() {
339
+ // Handle config command first
340
+ if (cmd === "config") {
341
+ const config = loadGlobalConfig();
342
+ const agent = await promptForDefaultAgent(config.defaultAgent || "claude");
343
+ if (agent) {
344
+ config.defaultAgent = agent;
345
+ config.configuredAt = new Date().toISOString();
346
+ saveGlobalConfig(config);
347
+ console.log(`Configuration saved to ${getGlobalConfigPath()}`);
348
+ }
349
+ process.exit(0);
350
+ }
351
+
352
+ // Check for first-run configuration (skip for help/install commands)
353
+ if (cmd !== "help" && cmd !== "-h" && cmd !== "--help" && cmd !== "install") {
354
+ const configuredAgent = await ensureFirstRunConfig();
355
+ if (configuredAgent && !agentOverride) {
356
+ // Use configured agent as default if no override specified
357
+ process.env.DEFAULT_AGENT = configuredAgent;
358
+ }
359
+ }
360
+
361
+ const updateInfo = checkForUpdate();
362
+ if (updateInfo) {
363
+ const { intro, confirm, outro, isCancel } = await import("@clack/prompts");
364
+ intro("Ralph update available");
365
+ const shouldUpdate = await confirm({
366
+ message: `Update ${updateInfo.name} from ${updateInfo.current} to ${updateInfo.latest}?`,
367
+ initialValue: true,
368
+ });
369
+ if (isCancel(shouldUpdate)) {
370
+ outro("Skipped update.");
371
+ } else if (shouldUpdate) {
372
+ outro("Updating...");
373
+ const result = spawnSync("npm", ["i", "-g", `${updateInfo.name}@latest`], {
374
+ stdio: "inherit",
375
+ });
376
+ process.exit(result.status ?? 1);
377
+ } else {
378
+ outro("Skipped update.");
379
+ }
380
+ }
381
+
382
+ if (cmd === "install") {
383
+ if (exists(localDir) && !installForce) {
384
+ console.log(`.agents/ralph already exists at ${localDir}. Skipping templates.`);
385
+ } else {
386
+ if (!exists(globalDir)) {
387
+ console.error(`Bundled templates not found at ${globalDir}.`);
388
+ process.exit(1);
389
+ }
390
+ fs.mkdirSync(path.dirname(localDir), { recursive: true });
391
+ fs.cpSync(globalDir, localDir, { recursive: true, force: true });
392
+ console.log(`Installed .agents/ralph to ${localDir}`);
393
+ }
394
+ const readmeSrc = path.join(repoRoot, "README.md");
395
+ const diagramSrc = path.join(repoRoot, "diagram.svg");
396
+ const imageSrc = path.join(repoRoot, "ralph.webp");
397
+ if (exists(localDir)) {
398
+ const readmeDst = path.join(localDir, "README.md");
399
+ const diagramDst = path.join(localDir, "diagram.svg");
400
+ const imageDst = path.join(localDir, "ralph.webp");
401
+ if (exists(readmeSrc)) fs.copyFileSync(readmeSrc, readmeDst);
402
+ if (exists(diagramSrc)) fs.copyFileSync(diagramSrc, diagramDst);
403
+ if (exists(imageSrc)) fs.copyFileSync(imageSrc, imageDst);
404
+ }
405
+ if (installSkills) {
406
+ await runInstallSkills();
407
+ } else {
408
+ const { confirm, isCancel } = await import("@clack/prompts");
409
+ const wantsSkills = await confirm({
410
+ message: "Install skills (commit + dev-browser + prd)?",
411
+ initialValue: true,
412
+ });
413
+ if (isCancel(wantsSkills)) {
414
+ console.log("Skipped skills install.");
415
+ } else if (wantsSkills) {
416
+ await runInstallSkills();
417
+ }
418
+ }
419
+ process.exit(0);
420
+ }
421
+
422
+ const templateDir = exists(localDir) ? localDir : globalDir;
423
+ if (!exists(templateDir)) {
424
+ console.error("No .agents/ralph found locally and no bundled defaults available.");
425
+ process.exit(1);
426
+ }
427
+ if (templateDir === localDir) {
428
+ console.log(`Using local templates: ${localDir}`);
429
+ } else {
430
+ console.log(`Using bundled templates: ${globalDir}`);
431
+ }
432
+
433
+ const loopPath = path.join(templateDir, "loop.sh");
434
+ if (!exists(loopPath)) {
435
+ console.error(`loop.sh not found at ${loopPath}`);
436
+ process.exit(1);
437
+ }
438
+
439
+ const loopArgs = args.length ? args : ["build"];
440
+ const loadAgentMap = () => {
441
+ const defaults = {
442
+ codex: "codex exec --yolo --skip-git-repo-check -",
443
+ claude: "claude -p --dangerously-skip-permissions \"$(cat {prompt})\"",
444
+ droid: "droid exec --skip-permissions-unsafe -f {prompt}",
445
+ opencode: "opencode run \"$(cat {prompt})\"",
446
+ };
447
+ const interactiveDefaults = {
448
+ codex: "codex --yolo {prompt}",
449
+ claude: "claude --dangerously-skip-permissions {prompt}",
450
+ droid: "droid --skip-permissions-unsafe {prompt}",
451
+ opencode: "opencode --prompt {prompt}",
452
+ };
453
+ const agentsPath = path.join(templateDir, "agents.sh");
454
+ if (!exists(agentsPath)) {
455
+ return {
456
+ headless: defaults,
457
+ interactive: interactiveDefaults,
458
+ defaultAgent: "codex",
459
+ };
460
+ }
461
+ const safePath = agentsPath.replace(/'/g, "'\\''");
462
+ const bashCmd = [
463
+ `source '${safePath}'`,
464
+ 'printf "DEFAULT_AGENT=%s\\n" "${DEFAULT_AGENT:-}"',
465
+ 'printf "AGENT_CODEX_CMD=%s\\n" "${AGENT_CODEX_CMD:-}"',
466
+ 'printf "AGENT_CLAUDE_CMD=%s\\n" "${AGENT_CLAUDE_CMD:-}"',
467
+ 'printf "AGENT_DROID_CMD=%s\\n" "${AGENT_DROID_CMD:-}"',
468
+ 'printf "AGENT_OPENCODE_CMD=%s\\n" "${AGENT_OPENCODE_CMD:-}"',
469
+ 'printf "AGENT_CODEX_INTERACTIVE_CMD=%s\\n" "${AGENT_CODEX_INTERACTIVE_CMD:-}"',
470
+ 'printf "AGENT_CLAUDE_INTERACTIVE_CMD=%s\\n" "${AGENT_CLAUDE_INTERACTIVE_CMD:-}"',
471
+ 'printf "AGENT_DROID_INTERACTIVE_CMD=%s\\n" "${AGENT_DROID_INTERACTIVE_CMD:-}"',
472
+ 'printf "AGENT_OPENCODE_INTERACTIVE_CMD=%s\\n" "${AGENT_OPENCODE_INTERACTIVE_CMD:-}"',
473
+ ].join("; ");
474
+ const result = spawnSync("bash", ["-lc", bashCmd], { encoding: "utf-8" });
475
+ if (result.status !== 0) {
476
+ return {
477
+ headless: defaults,
478
+ interactive: interactiveDefaults,
479
+ defaultAgent: "codex",
480
+ };
481
+ }
482
+ const parsed = new Map();
483
+ for (const line of String(result.stdout || "").split("\n")) {
484
+ if (!line) continue;
485
+ const idx = line.indexOf("=");
486
+ if (idx === -1) continue;
487
+ parsed.set(line.slice(0, idx), line.slice(idx + 1));
488
+ }
489
+ const readVar = (name) => parsed.get(name) || null;
490
+ return {
491
+ headless: {
492
+ codex: readVar("AGENT_CODEX_CMD") || defaults.codex,
493
+ claude: readVar("AGENT_CLAUDE_CMD") || defaults.claude,
494
+ droid: readVar("AGENT_DROID_CMD") || defaults.droid,
495
+ opencode: readVar("AGENT_OPENCODE_CMD") || defaults.opencode,
496
+ },
497
+ interactive: {
498
+ codex: readVar("AGENT_CODEX_INTERACTIVE_CMD") || interactiveDefaults.codex,
499
+ claude: readVar("AGENT_CLAUDE_INTERACTIVE_CMD") || interactiveDefaults.claude,
500
+ droid: readVar("AGENT_DROID_INTERACTIVE_CMD") || interactiveDefaults.droid,
501
+ opencode: readVar("AGENT_OPENCODE_INTERACTIVE_CMD") || interactiveDefaults.opencode,
502
+ },
503
+ defaultAgent: readVar("DEFAULT_AGENT") || "codex",
504
+ };
505
+ };
506
+ const agentDefs = loadAgentMap();
507
+ const agentMap = agentDefs.headless;
508
+ const agentInteractiveMap = agentDefs.interactive;
509
+ const defaultAgent = agentDefs.defaultAgent || "codex";
510
+ if (agentOverride) {
511
+ const mapped = agentMap[agentOverride];
512
+ if (!mapped) {
513
+ console.error(`Unknown agent: ${agentOverride}`);
514
+ console.error("Valid values: codex, claude, droid, opencode");
515
+ process.exit(1);
516
+ }
517
+ process.env.AGENT_CMD = mapped;
518
+ }
519
+ const env = { ...process.env, RALPH_ROOT: cwd };
520
+ if (!env.ACTIVITY_CMD) {
521
+ env.ACTIVITY_CMD = "ralph log";
522
+ }
523
+ if (progressPath) env.PROGRESS_PATH = progressPath;
524
+ if (templateDir === globalDir) {
525
+ env.PROMPT_BUILD = path.join(templateDir, "PROMPT_build.md");
526
+ env.GUARDRAILS_REF = path.join(templateDir, "references", "GUARDRAILS.md");
527
+ env.CONTEXT_REF = path.join(templateDir, "references", "CONTEXT_ENGINEERING.md");
528
+ }
529
+
530
+ if (cmd === "ping") {
531
+ const agentName = agentOverride || defaultAgent;
532
+ const agentCmd = agentMap[agentName];
533
+ if (!agentCmd) {
534
+ console.error("Unknown agent for ping.");
535
+ process.exit(1);
536
+ }
537
+ const agentBin = agentCmd.split(" ")[0];
538
+ const existsResult = spawnSync(`command -v ${agentBin}`, { shell: true, stdio: "ignore" });
539
+ if (existsResult.status !== 0) {
540
+ console.error(`Agent command not found: ${agentBin}`);
541
+ process.exit(1);
542
+ }
543
+ const promptFile = path.join(os.tmpdir(), `ralph-ping-${Date.now()}.txt`);
544
+ fs.writeFileSync(promptFile, "Reply with <end>pong</end> only.");
545
+ const escapePath = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
546
+ const rendered = agentCmd.includes("{prompt}")
547
+ ? agentCmd.replace(/\{prompt\}/g, escapePath(promptFile))
548
+ : `cat ${escapePath(promptFile)} | ${agentCmd}`;
549
+ const result = spawnSync(rendered, { shell: true, encoding: "utf-8" });
550
+ const output = `${result.stdout || ""}${result.stderr || ""}`;
551
+ if (!output.includes("<end>pong</end>")) {
552
+ console.error("Ping failed: missing <end>pong</end>.");
553
+ process.exit(1);
554
+ }
555
+ console.log("Ping OK.");
556
+ process.exit(0);
557
+ }
558
+
559
+ if (cmd === "plan") {
560
+ console.error("The plan command has been removed. Use `ralph build` with a JSON PRD.");
561
+ process.exit(1);
562
+ }
563
+
564
+ if (cmd === "log") {
565
+ const message = args.slice(1).join(" ").trim();
566
+ if (!message) {
567
+ console.error('Usage: ralph log "message"');
568
+ process.exit(1);
569
+ }
570
+ const activityLog = path.join(cwd, ".ralph", "activity.log");
571
+ fs.mkdirSync(path.dirname(activityLog), { recursive: true });
572
+ const now = new Date();
573
+ const pad = (n) => String(n).padStart(2, "0");
574
+ const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
575
+ fs.appendFileSync(activityLog, `[${stamp}] ${message}\n`);
576
+ process.exit(0);
577
+ }
578
+
579
+ if (cmd === "prd") {
580
+ let request = args.slice(1).join(" ").trim();
581
+ if (!request) {
582
+ const { intro, outro, text, isCancel } = await import("@clack/prompts");
583
+ intro("Ralph PRD");
584
+ const response = await text({
585
+ message: "Describe the feature you want a PRD for",
586
+ placeholder: "Example: A lightweight uptime monitor with email alerts",
587
+ });
588
+ if (isCancel(response)) {
589
+ outro("Cancelled.");
590
+ process.exit(0);
591
+ }
592
+ request = String(response || "").trim();
593
+ if (!request) {
594
+ outro("No description provided.");
595
+ process.exit(1);
596
+ }
597
+ outro("Generating PRD...");
598
+ }
599
+
600
+ // Determine output path
601
+ const outputPath = prdOutPath || prdPath || path.join(cwd, ".agents", "tasks");
602
+ const prdOutputFile = outputPath.endsWith(".json")
603
+ ? outputPath
604
+ : path.join(outputPath, "prd.json");
605
+ fs.mkdirSync(path.dirname(prdOutputFile), { recursive: true });
606
+
607
+ // Find PRD skill file
608
+ let skillContent = "";
609
+ const skillPaths = [
610
+ path.join(cwd, ".claude", "skills", "prd", "SKILL.md"),
611
+ path.join(cwd, "skills", "prd", "SKILL.md"),
612
+ path.join(repoRoot, "skills", "prd", "SKILL.md"),
613
+ ];
614
+ for (const sp of skillPaths) {
615
+ if (exists(sp)) {
616
+ skillContent = fs.readFileSync(sp, "utf-8");
617
+ break;
618
+ }
619
+ }
620
+
621
+ // Build the prompt
622
+ const promptLines = [
623
+ "You are an autonomous coding agent.",
624
+ "",
625
+ ];
626
+ if (skillContent) {
627
+ promptLines.push(skillContent);
628
+ promptLines.push("");
629
+ promptLines.push("---");
630
+ promptLines.push("");
631
+ }
632
+ promptLines.push("# Output Requirements");
633
+ promptLines.push("");
634
+ promptLines.push(`Save the PRD JSON to: ${prdOutputFile}`);
635
+ promptLines.push("");
636
+ promptLines.push("Do NOT implement anything. Only generate the JSON PRD file.");
637
+ promptLines.push("");
638
+ promptLines.push("After saving the PRD, end your response with:");
639
+ promptLines.push("PRD JSON saved to <path>. Close this chat and run `ralph build`.");
640
+ promptLines.push("");
641
+ promptLines.push("---");
642
+ promptLines.push("");
643
+ promptLines.push("# User Request");
644
+ promptLines.push("");
645
+ promptLines.push(request);
646
+
647
+ const promptFile = path.join(os.tmpdir(), `ralph-prd-${Date.now()}.md`);
648
+ fs.writeFileSync(promptFile, promptLines.join("\n"));
649
+
650
+ // Spawn the agent directly
651
+ const agentName = agentOverride || defaultAgent;
652
+ console.log(`Using agent: ${agentName}`);
653
+ console.log(`PRD will be saved to: ${prdOutputFile}`);
654
+ console.log("");
655
+
656
+ let result;
657
+ if (agentName === "claude") {
658
+ // Spawn claude directly with proper stdio inheritance
659
+ result = spawnSync("claude", ["--dangerously-skip-permissions", promptFile], {
660
+ stdio: "inherit",
661
+ cwd,
662
+ env: { ...process.env },
663
+ shell: true,
664
+ });
665
+ } else {
666
+ // For other agents, use loop.sh
667
+ const requestFile = path.join(os.tmpdir(), `ralph-prd-req-${Date.now()}.md`);
668
+ fs.writeFileSync(requestFile, `${request}\n`);
669
+ env.PRD_PATH = prdOutputFile;
670
+ const prdAgent = agentInteractiveMap[agentName] || agentMap[agentName];
671
+ if (prdAgent) {
672
+ env.PRD_AGENT_CMD = prdAgent;
673
+ }
674
+ const prdArgs = ["prd", "--prompt", requestFile];
675
+ result = spawnSync(loopPath, prdArgs, {
676
+ stdio: "inherit",
677
+ env,
678
+ });
679
+ }
680
+ process.exit(result.status ?? 1);
681
+ }
682
+
683
+ if (cmd === "overview") {
684
+ const resolvedPrdPath = await resolvePrdPath({
685
+ cwd,
686
+ explicitPath: prdPath,
687
+ label: "Ralph overview",
688
+ });
689
+ if (!resolvedPrdPath) {
690
+ console.error("No PRD JSON files found. Run `ralph prd` first.");
691
+ process.exit(1);
692
+ }
693
+ const prdText = fs.readFileSync(resolvedPrdPath, "utf-8");
694
+ let prd;
695
+ try {
696
+ prd = JSON.parse(prdText);
697
+ } catch (err) {
698
+ const message = err instanceof Error ? err.message : String(err);
699
+ console.error(`Invalid PRD JSON: ${message}`);
700
+ process.exit(1);
701
+ }
702
+ const stories = Array.isArray(prd.stories) ? prd.stories : [];
703
+ const total = stories.length;
704
+ const done = stories.filter((s) => String(s.status || "open").toLowerCase() === "done").length;
705
+ const inProgress = stories.filter((s) => String(s.status || "open").toLowerCase() === "in_progress").length;
706
+ const open = total - done - inProgress;
707
+ const gates = Array.isArray(prd.qualityGates) ? prd.qualityGates : [];
708
+ const parsed = path.parse(resolvedPrdPath);
709
+ const overviewPath = path.join(parsed.dir, `${parsed.name}.overview.md`);
710
+
711
+ const lines = [];
712
+ lines.push(`# PRD Overview${prd.project ? `: ${prd.project}` : ""}`);
713
+ lines.push("");
714
+ lines.push(`- File: ${path.relative(cwd, resolvedPrdPath)}`);
715
+ lines.push(`- Stories: ${total} total (${open} open, ${inProgress} in_progress, ${done} done)`);
716
+ lines.push("");
717
+ lines.push("## Quality Gates");
718
+ if (gates.length === 0) {
719
+ lines.push("- (none)");
720
+ } else {
721
+ for (const gate of gates) lines.push(`- ${gate}`);
722
+ }
723
+ lines.push("");
724
+ lines.push("## Stories");
725
+ if (total === 0) {
726
+ lines.push("- (none)");
727
+ } else {
728
+ for (const story of stories) {
729
+ const status = String(story.status || "open").toLowerCase();
730
+ const depends = Array.isArray(story.dependsOn) && story.dependsOn.length
731
+ ? ` (depends on: ${story.dependsOn.join(", ")})`
732
+ : "";
733
+ lines.push(`- [${status}] ${story.id || "US-???"}: ${story.title || ""}${depends}`);
734
+ }
735
+ }
736
+ fs.writeFileSync(overviewPath, `${lines.join("\n")}\n`);
737
+ console.log(`Overview written to ${path.relative(cwd, overviewPath)}`);
738
+ process.exit(0);
739
+ }
740
+
741
+ const needsPrd = cmd === "build" || !cmd;
742
+ if (needsPrd) {
743
+ const resolvedPrdPath = await resolvePrdPath({
744
+ cwd,
745
+ explicitPath: prdPath,
746
+ label: "Ralph build",
747
+ });
748
+ if (!resolvedPrdPath) {
749
+ console.error("No PRD JSON files found. Run `ralph prd` first.");
750
+ process.exit(1);
751
+ }
752
+ env.PRD_PATH = resolvedPrdPath;
753
+ }
754
+
755
+ const result = spawnSync(loopPath, loopArgs, {
756
+ stdio: "inherit",
757
+ env,
758
+ });
759
+
760
+ process.exit(result.status ?? 1);
761
+ }
762
+
763
+ main().catch((err) => {
764
+ console.error(err);
765
+ process.exit(1);
766
+ });