ralphflow 0.3.0 → 0.5.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 +103 -73
- package/dist/chunk-TCCMQDVT.js +505 -0
- package/dist/ralphflow.js +282 -282
- package/dist/server-DOSLU36L.js +821 -0
- package/package.json +6 -2
- package/src/dashboard/ui/index.html +3248 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +28 -14
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +7 -5
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +4 -2
- package/src/templates/code-implementation/ralphflow.yaml +3 -0
- package/src/templates/research/loops/00-discovery-loop/prompt.md +7 -5
- package/src/templates/research/loops/01-research-loop/prompt.md +7 -5
- package/src/templates/research/loops/02-story-loop/prompt.md +4 -2
- package/src/templates/research/loops/03-document-loop/prompt.md +4 -2
- package/src/templates/research/ralphflow.yaml +4 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// src/core/template.ts
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, readdirSync, rmSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
6
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
var BUILT_IN_TEMPLATES = ["code-implementation", "research"];
|
|
8
|
+
function resolveTemplatePath(templateName) {
|
|
9
|
+
const candidates = [
|
|
10
|
+
join(__dirname, "..", "templates", templateName),
|
|
11
|
+
// dev: src/core/ -> src/templates/
|
|
12
|
+
join(__dirname, "..", "src", "templates", templateName)
|
|
13
|
+
// bundled: dist/ -> src/templates/
|
|
14
|
+
];
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
if (existsSync(candidate)) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Template "${templateName}" not found. Searched:
|
|
22
|
+
${candidates.join("\n")}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
function resolveTemplatePathWithCustom(templateName, cwd) {
|
|
26
|
+
const customPath = join(cwd, ".ralph-flow", ".templates", templateName);
|
|
27
|
+
if (existsSync(customPath) && existsSync(join(customPath, "ralphflow.yaml"))) {
|
|
28
|
+
return customPath;
|
|
29
|
+
}
|
|
30
|
+
return resolveTemplatePath(templateName);
|
|
31
|
+
}
|
|
32
|
+
function copyTemplate(templateName, targetDir, cwd) {
|
|
33
|
+
const templatePath = cwd ? resolveTemplatePathWithCustom(templateName, cwd) : resolveTemplatePath(templateName);
|
|
34
|
+
const loopsDir = join(templatePath, "loops");
|
|
35
|
+
if (!existsSync(loopsDir)) {
|
|
36
|
+
throw new Error(`Template "${templateName}" has no loops/ directory`);
|
|
37
|
+
}
|
|
38
|
+
mkdirSync(targetDir, { recursive: true });
|
|
39
|
+
cpSync(loopsDir, targetDir, { recursive: true });
|
|
40
|
+
const yamlSrc = join(templatePath, "ralphflow.yaml");
|
|
41
|
+
if (existsSync(yamlSrc)) {
|
|
42
|
+
const yamlDest = join(targetDir, "ralphflow.yaml");
|
|
43
|
+
cpSync(yamlSrc, yamlDest);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function validateTemplateName(name) {
|
|
47
|
+
if (!name || name.trim().length === 0) {
|
|
48
|
+
return { valid: false, error: "Template name is required" };
|
|
49
|
+
}
|
|
50
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
51
|
+
return { valid: false, error: 'Invalid name: must not contain "..", "/", or "\\"' };
|
|
52
|
+
}
|
|
53
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
54
|
+
return { valid: false, error: "Only alphanumeric characters, hyphens, and underscores allowed" };
|
|
55
|
+
}
|
|
56
|
+
if (name.length > 50) {
|
|
57
|
+
return { valid: false, error: "Template name must be 50 characters or fewer" };
|
|
58
|
+
}
|
|
59
|
+
if (BUILT_IN_TEMPLATES.includes(name)) {
|
|
60
|
+
return { valid: false, error: `"${name}" is a reserved built-in template name` };
|
|
61
|
+
}
|
|
62
|
+
return { valid: true };
|
|
63
|
+
}
|
|
64
|
+
function listCustomTemplates(cwd) {
|
|
65
|
+
const customDir = join(cwd, ".ralph-flow", ".templates");
|
|
66
|
+
if (!existsSync(customDir)) return [];
|
|
67
|
+
return readdirSync(customDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).filter((d) => existsSync(join(customDir, d.name, "ralphflow.yaml"))).map((d) => d.name);
|
|
68
|
+
}
|
|
69
|
+
function getAvailableTemplates(cwd) {
|
|
70
|
+
const results = [];
|
|
71
|
+
for (const name of BUILT_IN_TEMPLATES) {
|
|
72
|
+
try {
|
|
73
|
+
const templatePath = resolveTemplatePath(name);
|
|
74
|
+
const yamlPath = join(templatePath, "ralphflow.yaml");
|
|
75
|
+
const raw = readFileSync(yamlPath, "utf-8");
|
|
76
|
+
const descMatch = raw.match(/^description:\s*"?([^"\n]+)"?/m);
|
|
77
|
+
const loopMatches = raw.match(/^\s{2}\S+-loop:/gm);
|
|
78
|
+
results.push({
|
|
79
|
+
name,
|
|
80
|
+
type: "built-in",
|
|
81
|
+
description: descMatch?.[1] || "",
|
|
82
|
+
loopCount: loopMatches?.length || 0
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
results.push({ name, type: "built-in", description: "", loopCount: 0 });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const customDir = join(cwd, ".ralph-flow", ".templates");
|
|
89
|
+
for (const tplName of listCustomTemplates(cwd)) {
|
|
90
|
+
try {
|
|
91
|
+
const yamlPath = join(customDir, tplName, "ralphflow.yaml");
|
|
92
|
+
const raw = readFileSync(yamlPath, "utf-8");
|
|
93
|
+
const descMatch = raw.match(/^description:\s*"?([^"\n]+)"?/m);
|
|
94
|
+
const loopMatches = raw.match(/^\s{2}\S+-loop:/gm);
|
|
95
|
+
results.push({
|
|
96
|
+
name: tplName,
|
|
97
|
+
type: "custom",
|
|
98
|
+
description: descMatch?.[1] || "",
|
|
99
|
+
loopCount: loopMatches?.length || 0
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
results.push({ name: tplName, type: "custom", description: "", loopCount: 0 });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
function createCustomTemplate(cwd, definition) {
|
|
108
|
+
const validation = validateTemplateName(definition.name);
|
|
109
|
+
if (!validation.valid) {
|
|
110
|
+
throw new Error(validation.error);
|
|
111
|
+
}
|
|
112
|
+
const customDir = join(cwd, ".ralph-flow", ".templates", definition.name);
|
|
113
|
+
if (existsSync(customDir)) {
|
|
114
|
+
throw new Error(`Template "${definition.name}" already exists`);
|
|
115
|
+
}
|
|
116
|
+
const config = {
|
|
117
|
+
name: definition.name,
|
|
118
|
+
description: definition.description || "",
|
|
119
|
+
version: 1,
|
|
120
|
+
dir: ".ralph-flow",
|
|
121
|
+
entities: {},
|
|
122
|
+
loops: {}
|
|
123
|
+
};
|
|
124
|
+
const loops = config.loops;
|
|
125
|
+
definition.loops.forEach((loopDef, index) => {
|
|
126
|
+
const baseKey = loopDef.name.toLowerCase().replace(/\s+/g, "-");
|
|
127
|
+
const loopKey = baseKey.endsWith("-loop") ? baseKey : `${baseKey}-loop`;
|
|
128
|
+
const dirPrefix = String(index).padStart(2, "0");
|
|
129
|
+
const loopDirName = `${dirPrefix}-${loopKey}`;
|
|
130
|
+
const loopConfig = {
|
|
131
|
+
order: index,
|
|
132
|
+
name: loopDef.name,
|
|
133
|
+
prompt: `${loopDirName}/prompt.md`,
|
|
134
|
+
tracker: `${loopDirName}/tracker.md`,
|
|
135
|
+
stages: loopDef.stages,
|
|
136
|
+
completion: loopDef.completion,
|
|
137
|
+
multi_agent: loopDef.multi_agent || false,
|
|
138
|
+
model: loopDef.model || "claude-sonnet-4-6",
|
|
139
|
+
cadence: loopDef.cadence ?? 0
|
|
140
|
+
};
|
|
141
|
+
if (loopDef.data_files && loopDef.data_files.length > 0) {
|
|
142
|
+
loopConfig.data_files = loopDef.data_files.map((f) => `${loopDirName}/${f}`);
|
|
143
|
+
}
|
|
144
|
+
if (loopDef.directories && loopDef.directories.length > 0) {
|
|
145
|
+
loopConfig.directories = loopDef.directories.map((d) => `${loopDirName}/${d}`);
|
|
146
|
+
}
|
|
147
|
+
if (loopDef.entities && loopDef.entities.length > 0) {
|
|
148
|
+
loopConfig.entities = loopDef.entities;
|
|
149
|
+
}
|
|
150
|
+
if (loopDef.feeds) loopConfig.feeds = loopDef.feeds;
|
|
151
|
+
if (loopDef.fed_by) loopConfig.fed_by = loopDef.fed_by;
|
|
152
|
+
if (loopDef.multi_agent && typeof loopDef.multi_agent === "object" && loopDef.multi_agent.enabled) {
|
|
153
|
+
loopConfig.lock = {
|
|
154
|
+
file: `${loopDirName}/.tracker-lock`,
|
|
155
|
+
type: "echo",
|
|
156
|
+
stale_seconds: 60
|
|
157
|
+
};
|
|
158
|
+
loopConfig.worktree = {
|
|
159
|
+
strategy: "shared",
|
|
160
|
+
auto_merge: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
loops[loopKey] = loopConfig;
|
|
164
|
+
});
|
|
165
|
+
mkdirSync(customDir, { recursive: true });
|
|
166
|
+
const loopsDir = join(customDir, "loops");
|
|
167
|
+
mkdirSync(loopsDir, { recursive: true });
|
|
168
|
+
writeFileSync(join(customDir, "ralphflow.yaml"), stringifyYaml(config, { lineWidth: 0 }), "utf-8");
|
|
169
|
+
definition.loops.forEach((loopDef, index) => {
|
|
170
|
+
const baseKey = loopDef.name.toLowerCase().replace(/\s+/g, "-");
|
|
171
|
+
const loopKey = baseKey.endsWith("-loop") ? baseKey : `${baseKey}-loop`;
|
|
172
|
+
const dirPrefix = String(index).padStart(2, "0");
|
|
173
|
+
const loopDirName = `${dirPrefix}-${loopKey}`;
|
|
174
|
+
const loopDir = join(loopsDir, loopDirName);
|
|
175
|
+
mkdirSync(loopDir, { recursive: true });
|
|
176
|
+
writeFileSync(join(loopDir, "prompt.md"), `# ${loopDef.name} \u2014 Prompt
|
|
177
|
+
|
|
178
|
+
<!-- Add your prompt here -->
|
|
179
|
+
`, "utf-8");
|
|
180
|
+
writeFileSync(join(loopDir, "tracker.md"), `# ${loopDef.name} \u2014 Tracker
|
|
181
|
+
|
|
182
|
+
- stage: ${loopDef.stages[0] || "init"}
|
|
183
|
+
`, "utf-8");
|
|
184
|
+
if (loopDef.data_files) {
|
|
185
|
+
for (const dataFile of loopDef.data_files) {
|
|
186
|
+
writeFileSync(join(loopDir, dataFile), `# ${dataFile}
|
|
187
|
+
|
|
188
|
+
<!-- Add content here -->
|
|
189
|
+
`, "utf-8");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (loopDef.directories) {
|
|
193
|
+
for (const dir of loopDef.directories) {
|
|
194
|
+
const dirPath = join(loopDir, dir);
|
|
195
|
+
mkdirSync(dirPath, { recursive: true });
|
|
196
|
+
writeFileSync(join(dirPath, ".gitkeep"), "", "utf-8");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function deleteCustomTemplate(cwd, name) {
|
|
202
|
+
if (BUILT_IN_TEMPLATES.includes(name)) {
|
|
203
|
+
throw new Error("Cannot delete built-in templates");
|
|
204
|
+
}
|
|
205
|
+
const validation = validateTemplateName(name);
|
|
206
|
+
if (!validation.valid) {
|
|
207
|
+
throw new Error(validation.error);
|
|
208
|
+
}
|
|
209
|
+
const customDir = join(cwd, ".ralph-flow", ".templates", name);
|
|
210
|
+
if (!existsSync(customDir)) {
|
|
211
|
+
throw new Error(`Template "${name}" not found`);
|
|
212
|
+
}
|
|
213
|
+
rmSync(customDir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/core/config.ts
|
|
217
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
218
|
+
import { join as join2 } from "path";
|
|
219
|
+
import { parse as parseYaml } from "yaml";
|
|
220
|
+
var LOOP_ALIASES = {
|
|
221
|
+
// code-implementation aliases
|
|
222
|
+
story: "story-loop",
|
|
223
|
+
stories: "story-loop",
|
|
224
|
+
tasks: "tasks-loop",
|
|
225
|
+
task: "tasks-loop",
|
|
226
|
+
delivery: "delivery-loop",
|
|
227
|
+
deliver: "delivery-loop",
|
|
228
|
+
// research aliases
|
|
229
|
+
discovery: "discovery-loop",
|
|
230
|
+
discover: "discovery-loop",
|
|
231
|
+
research: "research-loop",
|
|
232
|
+
document: "document-loop",
|
|
233
|
+
doc: "document-loop"
|
|
234
|
+
};
|
|
235
|
+
function listFlows(cwd) {
|
|
236
|
+
const baseDir = join2(cwd, ".ralph-flow");
|
|
237
|
+
if (!existsSync2(baseDir)) return [];
|
|
238
|
+
return readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).filter((d) => existsSync2(join2(baseDir, d.name, "ralphflow.yaml"))).map((d) => d.name);
|
|
239
|
+
}
|
|
240
|
+
function resolveFlowDir(cwd, flowName) {
|
|
241
|
+
const baseDir = join2(cwd, ".ralph-flow");
|
|
242
|
+
if (!existsSync2(baseDir)) {
|
|
243
|
+
throw new Error("No .ralph-flow/ found. Run `npx ralphflow init` first.");
|
|
244
|
+
}
|
|
245
|
+
const flows = listFlows(cwd);
|
|
246
|
+
if (flows.length === 0) {
|
|
247
|
+
throw new Error("No flows found in .ralph-flow/. Run `npx ralphflow init` first.");
|
|
248
|
+
}
|
|
249
|
+
if (flowName) {
|
|
250
|
+
if (!flows.includes(flowName)) {
|
|
251
|
+
throw new Error(`Flow "${flowName}" not found. Available: ${flows.join(", ")}`);
|
|
252
|
+
}
|
|
253
|
+
return join2(baseDir, flowName);
|
|
254
|
+
}
|
|
255
|
+
if (flows.length === 1) {
|
|
256
|
+
return join2(baseDir, flows[0]);
|
|
257
|
+
}
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Multiple flows found: ${flows.join(", ")}. Use --flow <name> to specify which one.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
function loadConfig(flowDir) {
|
|
263
|
+
const configPath = join2(flowDir, "ralphflow.yaml");
|
|
264
|
+
if (!existsSync2(configPath)) {
|
|
265
|
+
throw new Error(`No ralphflow.yaml found in ${flowDir}`);
|
|
266
|
+
}
|
|
267
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
268
|
+
const config = parseYaml(raw);
|
|
269
|
+
if (!config.name) {
|
|
270
|
+
throw new Error('ralphflow.yaml: missing required field "name"');
|
|
271
|
+
}
|
|
272
|
+
if (!config.loops || Object.keys(config.loops).length === 0) {
|
|
273
|
+
throw new Error('ralphflow.yaml: missing required field "loops"');
|
|
274
|
+
}
|
|
275
|
+
if (!config.dir) {
|
|
276
|
+
config.dir = ".ralph-flow";
|
|
277
|
+
}
|
|
278
|
+
return config;
|
|
279
|
+
}
|
|
280
|
+
function resolveLoop(config, name) {
|
|
281
|
+
if (config.loops[name]) {
|
|
282
|
+
return { key: name, loop: config.loops[name] };
|
|
283
|
+
}
|
|
284
|
+
const aliased = LOOP_ALIASES[name.toLowerCase()];
|
|
285
|
+
if (aliased && config.loops[aliased]) {
|
|
286
|
+
return { key: aliased, loop: config.loops[aliased] };
|
|
287
|
+
}
|
|
288
|
+
for (const [key, loop] of Object.entries(config.loops)) {
|
|
289
|
+
if (key.startsWith(name) || loop.name.toLowerCase().includes(name.toLowerCase())) {
|
|
290
|
+
return { key, loop };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const available = Object.keys(config.loops).join(", ");
|
|
294
|
+
throw new Error(`Unknown loop "${name}". Available: ${available}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/core/db.ts
|
|
298
|
+
import Database from "better-sqlite3";
|
|
299
|
+
import { join as join3 } from "path";
|
|
300
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
301
|
+
var SCHEMA = `
|
|
302
|
+
CREATE TABLE IF NOT EXISTS loop_state (
|
|
303
|
+
flow_name TEXT NOT NULL,
|
|
304
|
+
loop_key TEXT NOT NULL,
|
|
305
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
306
|
+
iterations_run INTEGER NOT NULL DEFAULT 0,
|
|
307
|
+
completed_at TEXT,
|
|
308
|
+
PRIMARY KEY (flow_name, loop_key)
|
|
309
|
+
);
|
|
310
|
+
`;
|
|
311
|
+
var _db = null;
|
|
312
|
+
function getDb(cwd) {
|
|
313
|
+
if (_db) return _db;
|
|
314
|
+
const dir = join3(cwd, ".ralph-flow");
|
|
315
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
316
|
+
_db = new Database(join3(dir, ".ralphflow.db"));
|
|
317
|
+
_db.pragma("journal_mode = WAL");
|
|
318
|
+
_db.exec(SCHEMA);
|
|
319
|
+
return _db;
|
|
320
|
+
}
|
|
321
|
+
function getLoopStatus(db, flow, loopKey) {
|
|
322
|
+
const row = db.prepare("SELECT status FROM loop_state WHERE flow_name = ? AND loop_key = ?").get(flow, loopKey);
|
|
323
|
+
return row ? row.status : "pending";
|
|
324
|
+
}
|
|
325
|
+
function isLoopComplete(db, flow, loopKey) {
|
|
326
|
+
return getLoopStatus(db, flow, loopKey) === "complete";
|
|
327
|
+
}
|
|
328
|
+
function markLoopRunning(db, flow, loopKey) {
|
|
329
|
+
db.prepare(`
|
|
330
|
+
INSERT INTO loop_state (flow_name, loop_key, status, iterations_run)
|
|
331
|
+
VALUES (?, ?, 'running', 0)
|
|
332
|
+
ON CONFLICT(flow_name, loop_key) DO UPDATE SET status = 'running'
|
|
333
|
+
`).run(flow, loopKey);
|
|
334
|
+
}
|
|
335
|
+
function incrementIteration(db, flow, loopKey) {
|
|
336
|
+
db.prepare(`
|
|
337
|
+
UPDATE loop_state SET iterations_run = iterations_run + 1
|
|
338
|
+
WHERE flow_name = ? AND loop_key = ?
|
|
339
|
+
`).run(flow, loopKey);
|
|
340
|
+
}
|
|
341
|
+
function markLoopComplete(db, flow, loopKey) {
|
|
342
|
+
db.prepare(`
|
|
343
|
+
UPDATE loop_state SET status = 'complete', completed_at = datetime('now')
|
|
344
|
+
WHERE flow_name = ? AND loop_key = ?
|
|
345
|
+
`).run(flow, loopKey);
|
|
346
|
+
}
|
|
347
|
+
function resetLoopState(db, flow, loopKey) {
|
|
348
|
+
db.prepare("DELETE FROM loop_state WHERE flow_name = ? AND loop_key = ?").run(flow, loopKey);
|
|
349
|
+
}
|
|
350
|
+
function deleteFlowState(db, flow) {
|
|
351
|
+
db.prepare("DELETE FROM loop_state WHERE flow_name = ?").run(flow);
|
|
352
|
+
}
|
|
353
|
+
function getAllLoopStates(db, flow) {
|
|
354
|
+
return db.prepare("SELECT * FROM loop_state WHERE flow_name = ?").all(flow);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/core/status.ts
|
|
358
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
|
|
359
|
+
import { join as join4 } from "path";
|
|
360
|
+
import chalk from "chalk";
|
|
361
|
+
import Table from "cli-table3";
|
|
362
|
+
async function showStatus(cwd, flowName) {
|
|
363
|
+
const flows = flowName ? [flowName] : listFlows(cwd);
|
|
364
|
+
if (flows.length === 0) {
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(chalk.yellow(" No flows found. Run `npx ralphflow init` first."));
|
|
367
|
+
console.log();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
for (const flow of flows) {
|
|
371
|
+
const flowDir = resolveFlowDir(cwd, flow);
|
|
372
|
+
const config = loadConfig(flowDir);
|
|
373
|
+
console.log();
|
|
374
|
+
console.log(chalk.bold(` RalphFlow \u2014 ${flow}`));
|
|
375
|
+
console.log();
|
|
376
|
+
const table = new Table({
|
|
377
|
+
chars: {
|
|
378
|
+
top: "",
|
|
379
|
+
"top-mid": "",
|
|
380
|
+
"top-left": "",
|
|
381
|
+
"top-right": "",
|
|
382
|
+
bottom: "",
|
|
383
|
+
"bottom-mid": "",
|
|
384
|
+
"bottom-left": "",
|
|
385
|
+
"bottom-right": "",
|
|
386
|
+
left: " ",
|
|
387
|
+
"left-mid": "",
|
|
388
|
+
mid: "",
|
|
389
|
+
"mid-mid": "",
|
|
390
|
+
right: "",
|
|
391
|
+
"right-mid": "",
|
|
392
|
+
middle: " "
|
|
393
|
+
},
|
|
394
|
+
style: { "padding-left": 0, "padding-right": 1 },
|
|
395
|
+
head: [
|
|
396
|
+
chalk.dim("Loop"),
|
|
397
|
+
chalk.dim("Stage"),
|
|
398
|
+
chalk.dim("Active"),
|
|
399
|
+
chalk.dim("Progress")
|
|
400
|
+
]
|
|
401
|
+
});
|
|
402
|
+
const sortedLoops = Object.entries(config.loops).sort(([, a], [, b]) => a.order - b.order);
|
|
403
|
+
for (const [key, loop] of sortedLoops) {
|
|
404
|
+
const status = parseTracker(loop.tracker, flowDir, loop.name);
|
|
405
|
+
table.push([
|
|
406
|
+
loop.name,
|
|
407
|
+
status.stage,
|
|
408
|
+
status.active,
|
|
409
|
+
`${status.completed}/${status.total}`
|
|
410
|
+
]);
|
|
411
|
+
if (status.agents && status.agents.length > 0) {
|
|
412
|
+
for (const agent of status.agents) {
|
|
413
|
+
table.push([
|
|
414
|
+
chalk.dim(` ${agent.name}`),
|
|
415
|
+
chalk.dim(agent.stage),
|
|
416
|
+
chalk.dim(agent.activeTask),
|
|
417
|
+
chalk.dim(agent.lastHeartbeat)
|
|
418
|
+
]);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
console.log(table.toString());
|
|
423
|
+
}
|
|
424
|
+
console.log();
|
|
425
|
+
}
|
|
426
|
+
function parseTracker(trackerPath, flowDir, loopName) {
|
|
427
|
+
const fullPath = join4(flowDir, trackerPath);
|
|
428
|
+
const status = {
|
|
429
|
+
loop: loopName,
|
|
430
|
+
stage: "\u2014",
|
|
431
|
+
active: "none",
|
|
432
|
+
completed: 0,
|
|
433
|
+
total: 0
|
|
434
|
+
};
|
|
435
|
+
if (!existsSync4(fullPath)) {
|
|
436
|
+
return status;
|
|
437
|
+
}
|
|
438
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
439
|
+
const lines = content.split("\n");
|
|
440
|
+
for (const line of lines) {
|
|
441
|
+
const metaMatch = line.match(/^- (\w[\w_]*): (.+)$/);
|
|
442
|
+
if (metaMatch) {
|
|
443
|
+
const [, key, value] = metaMatch;
|
|
444
|
+
if (key === "stage") status.stage = value.trim();
|
|
445
|
+
if (key === "active_story" || key === "active_task") status.active = value.trim();
|
|
446
|
+
if (key === "completed_stories" || key === "completed_tasks") {
|
|
447
|
+
const arrayMatch = value.match(/\[(.+)\]/);
|
|
448
|
+
if (arrayMatch) {
|
|
449
|
+
status.completed = arrayMatch[1].split(",").filter((s) => s.trim()).length;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
455
|
+
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
456
|
+
if (unchecked + checked > 0) {
|
|
457
|
+
status.total = unchecked + checked;
|
|
458
|
+
status.completed = checked;
|
|
459
|
+
}
|
|
460
|
+
const agentTableMatch = content.match(/\| agent \|.*\n\|[-|]+\n((?:\|.*\n)*)/);
|
|
461
|
+
if (agentTableMatch) {
|
|
462
|
+
const agentRows = agentTableMatch[1].trim().split("\n");
|
|
463
|
+
status.agents = [];
|
|
464
|
+
for (const row of agentRows) {
|
|
465
|
+
const cells = row.split("|").map((s) => s.trim()).filter(Boolean);
|
|
466
|
+
if (cells.length >= 4) {
|
|
467
|
+
status.agents.push({
|
|
468
|
+
name: cells[0],
|
|
469
|
+
activeTask: cells[1],
|
|
470
|
+
stage: cells[2],
|
|
471
|
+
lastHeartbeat: cells[3]
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (status.agents.length === 0) {
|
|
476
|
+
status.agents = void 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return status;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export {
|
|
483
|
+
BUILT_IN_TEMPLATES,
|
|
484
|
+
resolveTemplatePathWithCustom,
|
|
485
|
+
copyTemplate,
|
|
486
|
+
validateTemplateName,
|
|
487
|
+
listCustomTemplates,
|
|
488
|
+
getAvailableTemplates,
|
|
489
|
+
createCustomTemplate,
|
|
490
|
+
deleteCustomTemplate,
|
|
491
|
+
listFlows,
|
|
492
|
+
resolveFlowDir,
|
|
493
|
+
loadConfig,
|
|
494
|
+
resolveLoop,
|
|
495
|
+
getDb,
|
|
496
|
+
isLoopComplete,
|
|
497
|
+
markLoopRunning,
|
|
498
|
+
incrementIteration,
|
|
499
|
+
markLoopComplete,
|
|
500
|
+
resetLoopState,
|
|
501
|
+
deleteFlowState,
|
|
502
|
+
getAllLoopStates,
|
|
503
|
+
showStatus,
|
|
504
|
+
parseTracker
|
|
505
|
+
};
|