@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.
- package/.agents/ralph/PROMPT_build.md +126 -0
- package/.agents/ralph/agents.sh +15 -0
- package/.agents/ralph/config.sh +25 -0
- package/.agents/ralph/log-activity.sh +15 -0
- package/.agents/ralph/loop.sh +1001 -0
- package/.agents/ralph/references/CONTEXT_ENGINEERING.md +126 -0
- package/.agents/ralph/references/GUARDRAILS.md +174 -0
- package/AGENTS.md +20 -0
- package/README.md +266 -0
- package/bin/ralph +766 -0
- package/diagram.svg +55 -0
- package/examples/commands.md +46 -0
- package/package.json +39 -0
- package/ralph.webp +0 -0
- package/skills/commit/SKILL.md +219 -0
- package/skills/commit/references/commit_examples.md +292 -0
- package/skills/dev-browser/SKILL.md +211 -0
- package/skills/dev-browser/bun.lock +443 -0
- package/skills/dev-browser/package-lock.json +2988 -0
- package/skills/dev-browser/package.json +31 -0
- package/skills/dev-browser/references/scraping.md +155 -0
- package/skills/dev-browser/scripts/start-relay.ts +32 -0
- package/skills/dev-browser/scripts/start-server.ts +117 -0
- package/skills/dev-browser/server.sh +24 -0
- package/skills/dev-browser/src/client.ts +474 -0
- package/skills/dev-browser/src/index.ts +287 -0
- package/skills/dev-browser/src/relay.ts +731 -0
- package/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts +223 -0
- package/skills/dev-browser/src/snapshot/browser-script.ts +877 -0
- package/skills/dev-browser/src/snapshot/index.ts +14 -0
- package/skills/dev-browser/src/snapshot/inject.ts +13 -0
- package/skills/dev-browser/src/types.ts +34 -0
- package/skills/dev-browser/tsconfig.json +36 -0
- package/skills/dev-browser/vitest.config.ts +12 -0
- package/skills/prd/SKILL.md +235 -0
- package/tests/agent-loops.mjs +79 -0
- package/tests/agent-ping.mjs +39 -0
- package/tests/audit.md +56 -0
- package/tests/cli-smoke.mjs +47 -0
- 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
|
+
});
|