ralphflow 0.3.0 → 0.4.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/README.md CHANGED
@@ -11,26 +11,27 @@ Define pipelines as loops, coordinate parallel agents via file-based trackers, a
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
14
- # 1. Make sure you have a CLAUDE.md in your project
15
- # (or let Claude create one: claude "Initialize CLAUDE.md for this project")
14
+ # In your project with a CLAUDE.md
15
+ npx ralphflow
16
+ ```
16
17
 
17
- # 2. Initialize a flow
18
- npx ralphflow init --template code-implementation --name my-app
18
+ The interactive menu lets you initialize apps, run individual loops, run all loops in sequence, launch the web dashboard, or check status — no flags needed.
19
19
 
20
- # 3. Run the story loop — describe what you want to build
21
- npx ralphflow run story
20
+ ### Advanced Usage
22
21
 
23
- # 4. Run the tasks loop — agents implement your stories
22
+ ```bash
23
+ # Non-interactive commands (same as before)
24
+ npx ralphflow init --template code-implementation --name my-app
25
+ npx ralphflow run story
24
26
  npx ralphflow run tasks
25
-
26
- # 5. Run with multiple agents for parallel execution
27
- npx ralphflow run tasks --agents 3
28
-
29
- # 6. Deliver — review, get feedback, resolve
27
+ npx ralphflow run tasks --multi-agent # Multi-agent — one terminal per agent
30
28
  npx ralphflow run delivery
31
-
32
- # 7. Check progress anytime
33
29
  npx ralphflow status
30
+
31
+ # Web dashboard
32
+ npx ralphflow dashboard # Start dashboard at http://localhost:4242
33
+ npx ralphflow run tasks --ui # Run with live dashboard alongside
34
+ npx ralphflow e2e --ui # E2E with live dashboard
34
35
  ```
35
36
 
36
37
  ## How It Works
@@ -41,9 +42,9 @@ The default `code-implementation` template ships with three loops:
41
42
 
42
43
  | Loop | Purpose | Mode |
43
44
  |------|---------|------|
44
- | **Story Loop** | Break features into stories and tasks | Interactive Claude asks you questions |
45
- | **Tasks Loop** | Implement tasks, commit code, update CLAUDE.md | Single or multi-agent (autonomous) |
46
- | **Delivery Loop** | Review completed work, gather feedback | Interactive Claude presents deliverables |
45
+ | **Story Loop** | Break features into stories and tasks | Interactive Claude Code session |
46
+ | **Tasks Loop** | Implement tasks, commit code, update CLAUDE.md | Single or multi-agent (`--multi-agent`) |
47
+ | **Delivery Loop** | Review completed work, gather feedback | Interactive Claude Code session |
47
48
 
48
49
  ### Pipeline Flow
49
50
 
@@ -83,20 +84,61 @@ Requires `CLAUDE.md` to exist in your project root. If it doesn't, you'll be pro
83
84
  Runs a loop. Handles the iteration cycle — spawning Claude, detecting completion signals, and restarting on `kill -INT $PPID`.
84
85
 
85
86
  ```bash
86
- npx ralphflow run story # Run story loop
87
+ npx ralphflow run story # Run story loop (interactive Claude session)
87
88
  npx ralphflow run tasks # Run tasks loop (single agent)
88
- npx ralphflow run tasks --agents 3 # Run with 3 parallel agents
89
+ npx ralphflow run tasks --multi-agent # Run as a multi-agent instance (auto-assigns agent ID)
89
90
  npx ralphflow run delivery # Run delivery loop
90
91
  npx ralphflow run story --flow my-app # Specify which flow (when multiple exist)
91
92
  npx ralphflow run tasks --max-iterations 5 # Limit iterations
92
93
  ```
93
94
 
95
+ Each `run` command opens a full interactive Claude Code session. Claude owns the terminal — you see everything it does in real time.
96
+
97
+ **Multi-agent mode:** Instead of spawning N agents from one process, each terminal is one agent. Open multiple terminals, run `--multi-agent` in each, and they auto-assign sequential agent IDs (`agent-1`, `agent-2`, ...) via PID-based lock files. Stale agents are automatically cleaned up.
98
+
94
99
  **Options:**
95
- - `-a, --agents <n>` Number of parallel agents (default: 1)
100
+ - `--multi-agent` Run as a multi-agent instance (auto-assigns agent ID)
101
+ - `--ui` — Start the web dashboard alongside execution
96
102
  - `-m, --model <model>` — Claude model to use
97
103
  - `-n, --max-iterations <n>` — Maximum iterations (default: 30)
98
104
  - `-f, --flow <name>` — Which flow to run (auto-detected if only one exists)
99
105
 
106
+ ### `npx ralphflow dashboard`
107
+
108
+ Starts a real-time web dashboard at `http://localhost:4242`. Shows all apps, loop pipelines, tracker status, and lets you edit prompt files — all updated live via WebSocket as agents work.
109
+
110
+ ```bash
111
+ npx ralphflow dashboard # Default port 4242
112
+ npx ralphflow dashboard -p 3000 # Custom port
113
+ npx ralphflow ui # Alias
114
+ ```
115
+
116
+ **Features:**
117
+ - Live pipeline view with color-coded status (complete/running/pending)
118
+ - Per-loop detail: stage, active item, progress bar, agent table
119
+ - Prompt editor with Cmd+S save and dirty indicator
120
+ - Live tracker viewer (auto-updates as agents write)
121
+ - WebSocket auto-reconnect with exponential backoff
122
+
123
+ **Options:**
124
+ - `-p, --port <port>` — Port number (default: 4242)
125
+
126
+ ### `npx ralphflow e2e`
127
+
128
+ Runs all loops end-to-end with SQLite orchestration. Skips loops already completed.
129
+
130
+ ```bash
131
+ npx ralphflow e2e # Run all loops
132
+ npx ralphflow e2e --ui # With live dashboard
133
+ npx ralphflow e2e --flow my-app # Specific flow
134
+ ```
135
+
136
+ **Options:**
137
+ - `--ui` — Start the web dashboard alongside execution
138
+ - `-m, --model <model>` — Claude model to use
139
+ - `-n, --max-iterations <n>` — Maximum iterations per loop (default: 30)
140
+ - `-f, --flow <name>` — Which flow to run (auto-detected if only one exists)
141
+
100
142
  ### `npx ralphflow status`
101
143
 
102
144
  Shows the current state of all loops across all flows.
@@ -109,12 +151,10 @@ npx ralphflow status --flow my-app # Specific flow
109
151
  ```
110
152
  RalphFlow — my-app
111
153
 
112
- Loop Stage Active Progress
113
- Story Loop analyze none 0/0
114
- Tasks Loop — none 3/6
115
- agent-1 understand-execute TASK-4 2026-03-08T10:00
116
- agent-2 verify-document TASK-5 2026-03-08T10:01
117
- Delivery Loop idle none 0/0
154
+ Loop Stage Active Progress
155
+ Story Loop analyze none 0/0
156
+ Tasks Loop — none 3/6
157
+ Delivery Loop idle none 0/0
118
158
  ```
119
159
 
120
160
  **Options:**
@@ -142,7 +182,7 @@ Story → Tasks → Delivery pipeline for code projects. Battle-tested across 28
142
182
 
143
183
  ### `research`
144
184
 
145
- Multi-loop research pipeline with discovery, research, story, evolution, issue, datapoints, and merge loops. Tested across 467 places and 94 roads in a Kashi/Varanasi research project.
185
+ Discovery Research Story Document pipeline for research projects. Four loops: discovery (decompose topics), research (investigate with multi-agent), story (synthesize narratives), and document (compile final output).
146
186
 
147
187
  ## Project Structure
148
188
 
@@ -0,0 +1,274 @@
1
+ // src/core/config.ts
2
+ import { readFileSync, existsSync, readdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { parse as parseYaml } from "yaml";
5
+ var LOOP_ALIASES = {
6
+ // code-implementation aliases
7
+ story: "story-loop",
8
+ stories: "story-loop",
9
+ tasks: "tasks-loop",
10
+ task: "tasks-loop",
11
+ delivery: "delivery-loop",
12
+ deliver: "delivery-loop",
13
+ // research aliases
14
+ discovery: "discovery-loop",
15
+ discover: "discovery-loop",
16
+ research: "research-loop",
17
+ document: "document-loop",
18
+ doc: "document-loop"
19
+ };
20
+ function listFlows(cwd) {
21
+ const baseDir = join(cwd, ".ralph-flow");
22
+ if (!existsSync(baseDir)) return [];
23
+ return readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).filter((d) => existsSync(join(baseDir, d.name, "ralphflow.yaml"))).map((d) => d.name);
24
+ }
25
+ function resolveFlowDir(cwd, flowName) {
26
+ const baseDir = join(cwd, ".ralph-flow");
27
+ if (!existsSync(baseDir)) {
28
+ throw new Error("No .ralph-flow/ found. Run `npx ralphflow init` first.");
29
+ }
30
+ const flows = listFlows(cwd);
31
+ if (flows.length === 0) {
32
+ throw new Error("No flows found in .ralph-flow/. Run `npx ralphflow init` first.");
33
+ }
34
+ if (flowName) {
35
+ if (!flows.includes(flowName)) {
36
+ throw new Error(`Flow "${flowName}" not found. Available: ${flows.join(", ")}`);
37
+ }
38
+ return join(baseDir, flowName);
39
+ }
40
+ if (flows.length === 1) {
41
+ return join(baseDir, flows[0]);
42
+ }
43
+ throw new Error(
44
+ `Multiple flows found: ${flows.join(", ")}. Use --flow <name> to specify which one.`
45
+ );
46
+ }
47
+ function loadConfig(flowDir) {
48
+ const configPath = join(flowDir, "ralphflow.yaml");
49
+ if (!existsSync(configPath)) {
50
+ throw new Error(`No ralphflow.yaml found in ${flowDir}`);
51
+ }
52
+ const raw = readFileSync(configPath, "utf-8");
53
+ const config = parseYaml(raw);
54
+ if (!config.name) {
55
+ throw new Error('ralphflow.yaml: missing required field "name"');
56
+ }
57
+ if (!config.loops || Object.keys(config.loops).length === 0) {
58
+ throw new Error('ralphflow.yaml: missing required field "loops"');
59
+ }
60
+ if (!config.dir) {
61
+ config.dir = ".ralph-flow";
62
+ }
63
+ return config;
64
+ }
65
+ function resolveLoop(config, name) {
66
+ if (config.loops[name]) {
67
+ return { key: name, loop: config.loops[name] };
68
+ }
69
+ const aliased = LOOP_ALIASES[name.toLowerCase()];
70
+ if (aliased && config.loops[aliased]) {
71
+ return { key: aliased, loop: config.loops[aliased] };
72
+ }
73
+ for (const [key, loop] of Object.entries(config.loops)) {
74
+ if (key.startsWith(name) || loop.name.toLowerCase().includes(name.toLowerCase())) {
75
+ return { key, loop };
76
+ }
77
+ }
78
+ const available = Object.keys(config.loops).join(", ");
79
+ throw new Error(`Unknown loop "${name}". Available: ${available}`);
80
+ }
81
+
82
+ // src/core/db.ts
83
+ import Database from "better-sqlite3";
84
+ import { join as join2 } from "path";
85
+ import { existsSync as existsSync2, mkdirSync } from "fs";
86
+ var SCHEMA = `
87
+ CREATE TABLE IF NOT EXISTS loop_state (
88
+ flow_name TEXT NOT NULL,
89
+ loop_key TEXT NOT NULL,
90
+ status TEXT NOT NULL DEFAULT 'pending',
91
+ iterations_run INTEGER NOT NULL DEFAULT 0,
92
+ completed_at TEXT,
93
+ PRIMARY KEY (flow_name, loop_key)
94
+ );
95
+ `;
96
+ var _db = null;
97
+ function getDb(cwd) {
98
+ if (_db) return _db;
99
+ const dir = join2(cwd, ".ralph-flow");
100
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
101
+ _db = new Database(join2(dir, ".ralphflow.db"));
102
+ _db.pragma("journal_mode = WAL");
103
+ _db.exec(SCHEMA);
104
+ return _db;
105
+ }
106
+ function getLoopStatus(db, flow, loopKey) {
107
+ const row = db.prepare("SELECT status FROM loop_state WHERE flow_name = ? AND loop_key = ?").get(flow, loopKey);
108
+ return row ? row.status : "pending";
109
+ }
110
+ function isLoopComplete(db, flow, loopKey) {
111
+ return getLoopStatus(db, flow, loopKey) === "complete";
112
+ }
113
+ function markLoopRunning(db, flow, loopKey) {
114
+ db.prepare(`
115
+ INSERT INTO loop_state (flow_name, loop_key, status, iterations_run)
116
+ VALUES (?, ?, 'running', 0)
117
+ ON CONFLICT(flow_name, loop_key) DO UPDATE SET status = 'running'
118
+ `).run(flow, loopKey);
119
+ }
120
+ function incrementIteration(db, flow, loopKey) {
121
+ db.prepare(`
122
+ UPDATE loop_state SET iterations_run = iterations_run + 1
123
+ WHERE flow_name = ? AND loop_key = ?
124
+ `).run(flow, loopKey);
125
+ }
126
+ function markLoopComplete(db, flow, loopKey) {
127
+ db.prepare(`
128
+ UPDATE loop_state SET status = 'complete', completed_at = datetime('now')
129
+ WHERE flow_name = ? AND loop_key = ?
130
+ `).run(flow, loopKey);
131
+ }
132
+ function getAllLoopStates(db, flow) {
133
+ return db.prepare("SELECT * FROM loop_state WHERE flow_name = ?").all(flow);
134
+ }
135
+
136
+ // src/core/status.ts
137
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
138
+ import { join as join3 } from "path";
139
+ import chalk from "chalk";
140
+ import Table from "cli-table3";
141
+ async function showStatus(cwd, flowName) {
142
+ const flows = flowName ? [flowName] : listFlows(cwd);
143
+ if (flows.length === 0) {
144
+ console.log();
145
+ console.log(chalk.yellow(" No flows found. Run `npx ralphflow init` first."));
146
+ console.log();
147
+ return;
148
+ }
149
+ for (const flow of flows) {
150
+ const flowDir = resolveFlowDir(cwd, flow);
151
+ const config = loadConfig(flowDir);
152
+ console.log();
153
+ console.log(chalk.bold(` RalphFlow \u2014 ${flow}`));
154
+ console.log();
155
+ const table = new Table({
156
+ chars: {
157
+ top: "",
158
+ "top-mid": "",
159
+ "top-left": "",
160
+ "top-right": "",
161
+ bottom: "",
162
+ "bottom-mid": "",
163
+ "bottom-left": "",
164
+ "bottom-right": "",
165
+ left: " ",
166
+ "left-mid": "",
167
+ mid: "",
168
+ "mid-mid": "",
169
+ right: "",
170
+ "right-mid": "",
171
+ middle: " "
172
+ },
173
+ style: { "padding-left": 0, "padding-right": 1 },
174
+ head: [
175
+ chalk.dim("Loop"),
176
+ chalk.dim("Stage"),
177
+ chalk.dim("Active"),
178
+ chalk.dim("Progress")
179
+ ]
180
+ });
181
+ const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
182
+ for (const [key, loop] of sortedLoops) {
183
+ const status = parseTracker(loop.tracker, flowDir, loop.name);
184
+ table.push([
185
+ loop.name,
186
+ status.stage,
187
+ status.active,
188
+ `${status.completed}/${status.total}`
189
+ ]);
190
+ if (status.agents && status.agents.length > 0) {
191
+ for (const agent of status.agents) {
192
+ table.push([
193
+ chalk.dim(` ${agent.name}`),
194
+ chalk.dim(agent.stage),
195
+ chalk.dim(agent.activeTask),
196
+ chalk.dim(agent.lastHeartbeat)
197
+ ]);
198
+ }
199
+ }
200
+ }
201
+ console.log(table.toString());
202
+ }
203
+ console.log();
204
+ }
205
+ function parseTracker(trackerPath, flowDir, loopName) {
206
+ const fullPath = join3(flowDir, trackerPath);
207
+ const status = {
208
+ loop: loopName,
209
+ stage: "\u2014",
210
+ active: "none",
211
+ completed: 0,
212
+ total: 0
213
+ };
214
+ if (!existsSync3(fullPath)) {
215
+ return status;
216
+ }
217
+ const content = readFileSync2(fullPath, "utf-8");
218
+ const lines = content.split("\n");
219
+ for (const line of lines) {
220
+ const metaMatch = line.match(/^- (\w[\w_]*): (.+)$/);
221
+ if (metaMatch) {
222
+ const [, key, value] = metaMatch;
223
+ if (key === "stage") status.stage = value.trim();
224
+ if (key === "active_story" || key === "active_task") status.active = value.trim();
225
+ if (key === "completed_stories" || key === "completed_tasks") {
226
+ const arrayMatch = value.match(/\[(.+)\]/);
227
+ if (arrayMatch) {
228
+ status.completed = arrayMatch[1].split(",").filter((s) => s.trim()).length;
229
+ }
230
+ }
231
+ }
232
+ }
233
+ const unchecked = (content.match(/- \[ \]/g) || []).length;
234
+ const checked = (content.match(/- \[x\]/gi) || []).length;
235
+ if (unchecked + checked > 0) {
236
+ status.total = unchecked + checked;
237
+ status.completed = checked;
238
+ }
239
+ const agentTableMatch = content.match(/\| agent \|.*\n\|[-|]+\n((?:\|.*\n)*)/);
240
+ if (agentTableMatch) {
241
+ const agentRows = agentTableMatch[1].trim().split("\n");
242
+ status.agents = [];
243
+ for (const row of agentRows) {
244
+ const cells = row.split("|").map((s) => s.trim()).filter(Boolean);
245
+ if (cells.length >= 4) {
246
+ status.agents.push({
247
+ name: cells[0],
248
+ activeTask: cells[1],
249
+ stage: cells[2],
250
+ lastHeartbeat: cells[3]
251
+ });
252
+ }
253
+ }
254
+ if (status.agents.length === 0) {
255
+ status.agents = void 0;
256
+ }
257
+ }
258
+ return status;
259
+ }
260
+
261
+ export {
262
+ listFlows,
263
+ resolveFlowDir,
264
+ loadConfig,
265
+ resolveLoop,
266
+ getDb,
267
+ isLoopComplete,
268
+ markLoopRunning,
269
+ incrementIteration,
270
+ markLoopComplete,
271
+ getAllLoopStates,
272
+ showStatus,
273
+ parseTracker
274
+ };