researchloop 0.1.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/researchloop.js +900 -0
- package/docs/getting-started.md +283 -0
- package/package.json +37 -0
- package/templates/adapters/generic.md +18 -0
- package/templates/adapters/huggingface.md +15 -0
- package/templates/adapters/llm-research-kit.md +20 -0
- package/templates/adapters/pytorch.md +26 -0
- package/templates/base/AGENTS.md +47 -0
- package/templates/base/goal.md +22 -0
- package/templates/base/plan.md +22 -0
- package/templates/base/scratchpad/THREAD.md +13 -0
- package/templates/base/scratchpad/audits.md +14 -0
- package/templates/base/scratchpad/ideas/.gitkeep +1 -0
- package/templates/base/scratchpad/papers/.gitkeep +1 -0
- package/templates/base/scratchpad/picklist.md +15 -0
- package/templates/base/scratchpad/runs.jsonl +1 -0
- package/templates/base/scratchpad/sweeps/.gitkeep +1 -0
- package/templates/base/scratchpad/variants/.gitkeep +1 -0
- package/templates/dashboard/index.html +627 -0
- package/templates/prompts/claude-code.md +30 -0
- package/templates/prompts/codex.md +29 -0
- package/templates/prompts/focus/architecture.md +30 -0
- package/templates/prompts/focus/attention.md +27 -0
- package/templates/prompts/focus/hyperparameters.md +32 -0
- package/templates/prompts/generic.md +8 -0
- package/templates/prompts/hermes.md +26 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const packageRoot = path.resolve(path.dirname(__filename), "..");
|
|
11
|
+
const templatesRoot = path.join(packageRoot, "templates");
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args.find((arg) => !arg.startsWith("-")) || "help";
|
|
15
|
+
|
|
16
|
+
function option(name, fallback = undefined) {
|
|
17
|
+
const idx = args.indexOf(name);
|
|
18
|
+
if (idx === -1) return fallback;
|
|
19
|
+
const value = args[idx + 1];
|
|
20
|
+
if (!value || value.startsWith("--")) return true;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasFlag(name) {
|
|
25
|
+
return args.includes(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function optionsAll(name) {
|
|
29
|
+
const values = [];
|
|
30
|
+
for (let idx = 0; idx < args.length; idx += 1) {
|
|
31
|
+
if (args[idx] === name) {
|
|
32
|
+
const value = args[idx + 1];
|
|
33
|
+
if (value && !value.startsWith("--")) {
|
|
34
|
+
values.push(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function positionalText(excludedFlags = []) {
|
|
42
|
+
const idx = args.findIndex((arg) => !arg.startsWith("-"));
|
|
43
|
+
if (idx === -1) {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
const skip = new Set(excludedFlags);
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (let i = idx + 1; i < args.length; i += 1) {
|
|
49
|
+
const arg = args[i];
|
|
50
|
+
if (skip.has(arg)) {
|
|
51
|
+
i += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg.startsWith("-")) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
parts.push(arg);
|
|
58
|
+
}
|
|
59
|
+
return parts.join(" ").trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function targetDir() {
|
|
63
|
+
return path.resolve(String(option("--dir", process.cwd())));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ensureDir(dir) {
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeFileSafe(file, content, force = false) {
|
|
71
|
+
ensureDir(path.dirname(file));
|
|
72
|
+
if (fs.existsSync(file) && !force) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
fs.writeFileSync(file, content);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function copyDir(src, dest, force = false) {
|
|
80
|
+
ensureDir(dest);
|
|
81
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
82
|
+
const srcPath = path.join(src, entry.name);
|
|
83
|
+
const destPath = path.join(dest, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
copyDir(srcPath, destPath, force);
|
|
86
|
+
} else {
|
|
87
|
+
writeFileSafe(destPath, fs.readFileSync(srcPath), force);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function run(commandText, cwd) {
|
|
93
|
+
try {
|
|
94
|
+
return execSync(commandText, {
|
|
95
|
+
cwd,
|
|
96
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
timeout: 5000,
|
|
99
|
+
}).trim();
|
|
100
|
+
} catch {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function existsAny(cwd, candidates) {
|
|
106
|
+
return candidates.filter((candidate) => fs.existsSync(path.join(cwd, candidate)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function walkFiles(cwd, maxDepth = 3) {
|
|
110
|
+
const out = [];
|
|
111
|
+
function walk(dir, depth) {
|
|
112
|
+
if (depth > maxDepth) return;
|
|
113
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
114
|
+
if (
|
|
115
|
+
entry.name === ".git" ||
|
|
116
|
+
entry.name === ".researchloop" ||
|
|
117
|
+
entry.name === "node_modules" ||
|
|
118
|
+
entry.name === "__pycache__"
|
|
119
|
+
) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const full = path.join(dir, entry.name);
|
|
123
|
+
const rel = path.relative(cwd, full);
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
walk(full, depth + 1);
|
|
126
|
+
} else {
|
|
127
|
+
out.push(rel);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
walk(cwd, 0);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function detectRepo(cwd) {
|
|
136
|
+
const files = walkFiles(cwd, 3);
|
|
137
|
+
const lower = files.map((file) => file.toLowerCase());
|
|
138
|
+
const has = (pattern) => lower.some((file) => file.includes(pattern));
|
|
139
|
+
|
|
140
|
+
const adapters = ["generic"];
|
|
141
|
+
if (has("train.py") || has("train_") || has("pytorch") || has("torch")) {
|
|
142
|
+
adapters.push("pytorch");
|
|
143
|
+
}
|
|
144
|
+
if (has("trainer") || has("transformers") || has("huggingface")) {
|
|
145
|
+
adapters.push("huggingface");
|
|
146
|
+
}
|
|
147
|
+
if (files.includes("train_llm.py") && files.includes("configs/llm_config.py")) {
|
|
148
|
+
adapters.push("llm-research-kit");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
cwd,
|
|
153
|
+
generated_at: new Date().toISOString(),
|
|
154
|
+
git_branch: run("git branch --show-current", cwd) || null,
|
|
155
|
+
git_status_short: run("git status --short", cwd) || null,
|
|
156
|
+
package_files: existsAny(cwd, ["package.json", "pyproject.toml", "requirements.txt", "uv.lock"]),
|
|
157
|
+
candidate_train_files: files.filter((file) => /(^|\/)(train|finetune|pretrain).*\.py$/i.test(file)).slice(0, 30),
|
|
158
|
+
candidate_eval_files: files.filter((file) => /(^|\/)(eval|evaluate|benchmark).*\.py$/i.test(file)).slice(0, 30),
|
|
159
|
+
candidate_config_files: files.filter((file) => /(^|\/|_)(config|cfg)[^/]*\.(py|js|ts|json|yaml|yml|toml)$|\.ya?ml$|\.toml$|\.json$/i.test(file)).slice(0, 40),
|
|
160
|
+
candidate_log_dirs: existsAny(cwd, ["logs", "runs", "wandb", "mlruns", "checkpoints", "plots"]),
|
|
161
|
+
adapters: [...new Set(adapters)],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function installAgentFile(cwd, agent, force) {
|
|
166
|
+
const content = [
|
|
167
|
+
"# Research Loop",
|
|
168
|
+
"",
|
|
169
|
+
"Before doing autonomous research, read:",
|
|
170
|
+
"",
|
|
171
|
+
"- `.researchloop/AGENTS.md`",
|
|
172
|
+
"- `.researchloop/goal.md`",
|
|
173
|
+
"- `.researchloop/plan.md`",
|
|
174
|
+
"- `.researchloop/scratchpad/THREAD.md`",
|
|
175
|
+
"",
|
|
176
|
+
"Use `.researchloop/` as durable working memory. Record commands, metrics, decisions, and next experiments.",
|
|
177
|
+
"",
|
|
178
|
+
].join("\n");
|
|
179
|
+
|
|
180
|
+
if (agent === "claude-code") {
|
|
181
|
+
return writeFileSafe(path.join(cwd, "CLAUDE.md"), content, force);
|
|
182
|
+
}
|
|
183
|
+
if (agent === "hermes") {
|
|
184
|
+
return writeFileSafe(path.join(cwd, "HERMES.md"), content, force);
|
|
185
|
+
}
|
|
186
|
+
if (agent === "cursor") {
|
|
187
|
+
return writeFileSafe(path.join(cwd, ".cursor", "rules", "researchloop.mdc"), content, force);
|
|
188
|
+
}
|
|
189
|
+
return writeFileSafe(path.join(cwd, "AGENTS.md"), content, force);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cmdInit() {
|
|
193
|
+
const cwd = targetDir();
|
|
194
|
+
const force = hasFlag("--force");
|
|
195
|
+
const agent = String(option("--agent", "codex"));
|
|
196
|
+
const researchDir = path.join(cwd, ".researchloop");
|
|
197
|
+
|
|
198
|
+
copyDir(path.join(templatesRoot, "base"), researchDir, force);
|
|
199
|
+
copyDir(path.join(templatesRoot, "adapters"), path.join(researchDir, "adapters"), force);
|
|
200
|
+
const wroteAgent = installAgentFile(cwd, agent, force);
|
|
201
|
+
|
|
202
|
+
const profile = detectRepo(cwd);
|
|
203
|
+
writeFileSafe(
|
|
204
|
+
path.join(researchDir, "repo-profile.json"),
|
|
205
|
+
`${JSON.stringify(profile, null, 2)}\n`,
|
|
206
|
+
true
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
console.log(`Research Loop initialized in ${cwd}`);
|
|
210
|
+
console.log(`Harness: ${path.relative(cwd, researchDir)}`);
|
|
211
|
+
console.log(`Agent file: ${wroteAgent ? "written" : "already existed"}`);
|
|
212
|
+
console.log(`Detected adapters: ${profile.adapters.join(", ")}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cmdInspect() {
|
|
216
|
+
const cwd = targetDir();
|
|
217
|
+
const researchDir = path.join(cwd, ".researchloop");
|
|
218
|
+
ensureDir(researchDir);
|
|
219
|
+
const profile = detectRepo(cwd);
|
|
220
|
+
fs.writeFileSync(path.join(researchDir, "repo-profile.json"), `${JSON.stringify(profile, null, 2)}\n`);
|
|
221
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function cmdPrompt() {
|
|
225
|
+
const cwd = targetDir();
|
|
226
|
+
const agent = String(option("--agent", "codex"));
|
|
227
|
+
const explicitGoal = option("--goal", null);
|
|
228
|
+
const savedGoal = readGoalSummary(path.join(cwd, ".researchloop", "goal.md"));
|
|
229
|
+
const focus = String(option("--focus", option("--playbook", ""))).trim();
|
|
230
|
+
const goal =
|
|
231
|
+
explicitGoal ||
|
|
232
|
+
savedGoal ||
|
|
233
|
+
"Improve the target metric through small, documented experiments.";
|
|
234
|
+
const promptFile = path.join(templatesRoot, "prompts", `${agent}.md`);
|
|
235
|
+
const fallback = path.join(templatesRoot, "prompts", "generic.md");
|
|
236
|
+
const template = fs.readFileSync(fs.existsSync(promptFile) ? promptFile : fallback, "utf8");
|
|
237
|
+
let output = template.replaceAll("{{GOAL}}", goal);
|
|
238
|
+
|
|
239
|
+
if (focus) {
|
|
240
|
+
const focusFile = path.join(templatesRoot, "prompts", "focus", `${focus}.md`);
|
|
241
|
+
if (fs.existsSync(focusFile)) {
|
|
242
|
+
output += "\n\n";
|
|
243
|
+
output += fs.readFileSync(focusFile, "utf8").replaceAll("{{GOAL}}", goal);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
process.stdout.write(output);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cmdGoal() {
|
|
251
|
+
const cwd = targetDir();
|
|
252
|
+
const researchDir = path.join(cwd, ".researchloop");
|
|
253
|
+
ensureDir(researchDir);
|
|
254
|
+
const goalText = positionalText(["--dir", "--metric", "--direction", "--baseline", "--evaluation", "--allowed", "--forbidden"]);
|
|
255
|
+
const goalFile = path.join(researchDir, "goal.md");
|
|
256
|
+
|
|
257
|
+
if (!goalText) {
|
|
258
|
+
if (fs.existsSync(goalFile)) {
|
|
259
|
+
process.stdout.write(fs.readFileSync(goalFile, "utf8"));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
console.log("No research goal set yet. Use `researchloop goal \"lower validation loss\"`.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const metric = String(option("--metric", "validation loss"));
|
|
267
|
+
const direction = String(option("--direction", "lower"));
|
|
268
|
+
const baseline = String(option("--baseline", "unknown"));
|
|
269
|
+
const evaluation = String(option("--evaluation", "unknown"));
|
|
270
|
+
const allowed = String(option("--allowed", "optimizer, schedules, initialization, hyperparameters"));
|
|
271
|
+
const forbidden = String(option("--forbidden", "data, architecture, batch size, benchmark definition"));
|
|
272
|
+
|
|
273
|
+
const content = [
|
|
274
|
+
"# Research Goal",
|
|
275
|
+
"",
|
|
276
|
+
"## Goal",
|
|
277
|
+
goalText,
|
|
278
|
+
"",
|
|
279
|
+
"## Target Metric",
|
|
280
|
+
metric,
|
|
281
|
+
"",
|
|
282
|
+
"## Direction",
|
|
283
|
+
direction,
|
|
284
|
+
"",
|
|
285
|
+
"## Baseline Command",
|
|
286
|
+
baseline,
|
|
287
|
+
"",
|
|
288
|
+
"## Evaluation Command",
|
|
289
|
+
evaluation,
|
|
290
|
+
"",
|
|
291
|
+
"## Allowed Changes",
|
|
292
|
+
allowed,
|
|
293
|
+
"",
|
|
294
|
+
"## Forbidden Changes",
|
|
295
|
+
forbidden,
|
|
296
|
+
"",
|
|
297
|
+
"## Current Best",
|
|
298
|
+
"Unknown.",
|
|
299
|
+
"",
|
|
300
|
+
"## Notes",
|
|
301
|
+
"Use `researchloop inspect` to generate a repo profile, then ask an agent to fill in the missing benchmark details.",
|
|
302
|
+
"",
|
|
303
|
+
].join("\n");
|
|
304
|
+
|
|
305
|
+
fs.writeFileSync(goalFile, content);
|
|
306
|
+
console.log(`Research goal saved to ${path.relative(cwd, goalFile)}`);
|
|
307
|
+
console.log(`Goal: ${goalText}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readGoalSummary(goalFile) {
|
|
311
|
+
if (!fs.existsSync(goalFile)) {
|
|
312
|
+
return "";
|
|
313
|
+
}
|
|
314
|
+
const text = fs.readFileSync(goalFile, "utf8");
|
|
315
|
+
const match = text.match(/## Goal\s+([\s\S]*?)(?:\n## |\n# |$)/i);
|
|
316
|
+
if (match) {
|
|
317
|
+
return match[1].trim();
|
|
318
|
+
}
|
|
319
|
+
return text.trim();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function cmdDoctor() {
|
|
323
|
+
const cwd = targetDir();
|
|
324
|
+
const python = String(option("--python", "python3"));
|
|
325
|
+
const nodeVersion = process.version;
|
|
326
|
+
const npmVersion = run("npm --version", cwd) || "not found";
|
|
327
|
+
const gitVersion = run("git --version", cwd) || "not found";
|
|
328
|
+
const pythonVersion = run(`${python} --version`, cwd) || "not found";
|
|
329
|
+
const torchProbe = run(`${python} - <<'PY'\nimport importlib.util\nspec = importlib.util.find_spec('torch')\nif not spec:\n print('torch missing')\nelse:\n import torch\n print(f'torch {torch.__version__}')\n print(f'cuda {torch.cuda.is_available()}')\n print(f'mps {hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available()}')\nPY`, cwd) || "torch unknown";
|
|
330
|
+
|
|
331
|
+
console.log(`cwd: ${cwd}`);
|
|
332
|
+
console.log(`node: ${nodeVersion}`);
|
|
333
|
+
console.log(`npm: ${npmVersion}`);
|
|
334
|
+
console.log(`git: ${gitVersion}`);
|
|
335
|
+
console.log(`python: ${pythonVersion} (${python})`);
|
|
336
|
+
console.log(torchProbe);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function cmdReport() {
|
|
340
|
+
const cwd = targetDir();
|
|
341
|
+
const ledger = path.join(cwd, ".researchloop", "scratchpad", "runs.jsonl");
|
|
342
|
+
if (!fs.existsSync(ledger)) {
|
|
343
|
+
console.log("No run ledger found. Run `researchloop init` first.");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const rows = fs.readFileSync(ledger, "utf8").split("\n").filter(Boolean);
|
|
347
|
+
const parsed = rows.map((row) => {
|
|
348
|
+
try {
|
|
349
|
+
return JSON.parse(row);
|
|
350
|
+
} catch {
|
|
351
|
+
return { parse_error: true, raw: row };
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
const errors = parsed.filter((row) => row.parse_error).length;
|
|
355
|
+
const complete = parsed.filter((row) => row.status === "complete" || row.status === "completed").length;
|
|
356
|
+
console.log(`runs: ${rows.length}`);
|
|
357
|
+
console.log(`complete: ${complete}`);
|
|
358
|
+
console.log(`parse_errors: ${errors}`);
|
|
359
|
+
if (parsed.length) {
|
|
360
|
+
console.log(`last: ${JSON.stringify(parsed[parsed.length - 1], null, 2)}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function readTextIfExists(file) {
|
|
365
|
+
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function parseMarkdownSection(text, heading) {
|
|
369
|
+
if (!text) return "";
|
|
370
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
371
|
+
const match = text.match(new RegExp(`^## ${escaped}\\s+([\\s\\S]*?)(?=\\n## |\\n# |$)`, "mi"));
|
|
372
|
+
return match ? match[1].trim() : "";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseGoalFile(goalFile) {
|
|
376
|
+
const raw = readTextIfExists(goalFile);
|
|
377
|
+
return {
|
|
378
|
+
raw,
|
|
379
|
+
goal: parseMarkdownSection(raw, "Goal") || "",
|
|
380
|
+
metric: parseMarkdownSection(raw, "Target Metric") || "",
|
|
381
|
+
direction: parseMarkdownSection(raw, "Direction") || "",
|
|
382
|
+
baseline: parseMarkdownSection(raw, "Baseline Command") || "",
|
|
383
|
+
evaluation: parseMarkdownSection(raw, "Evaluation Command") || "",
|
|
384
|
+
currentBest: parseMarkdownSection(raw, "Current Best") || "",
|
|
385
|
+
notes: parseMarkdownSection(raw, "Notes") || "",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function parsePlanFile(planFile) {
|
|
390
|
+
const raw = readTextIfExists(planFile);
|
|
391
|
+
return {
|
|
392
|
+
raw,
|
|
393
|
+
currentState: parseMarkdownSection(raw, "Current State") || "",
|
|
394
|
+
picklist: parseMarkdownSection(raw, "Picklist") || "",
|
|
395
|
+
ruledOut: parseMarkdownSection(raw, "Ruled Out") || "",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parseRunsLedger(ledgerFile) {
|
|
400
|
+
if (!fs.existsSync(ledgerFile)) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
return readTextIfExists(ledgerFile)
|
|
404
|
+
.split("\n")
|
|
405
|
+
.filter(Boolean)
|
|
406
|
+
.map((row) => {
|
|
407
|
+
try {
|
|
408
|
+
return JSON.parse(row);
|
|
409
|
+
} catch {
|
|
410
|
+
return { parse_error: true, raw: row };
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isNumericMetric(value) {
|
|
416
|
+
return Number.isFinite(Number(value));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function choosePrimaryMetric(goal, runs) {
|
|
420
|
+
const metricHint = String(goal?.metric || "").trim();
|
|
421
|
+
const metricKeys = new Set();
|
|
422
|
+
for (const run of runs) {
|
|
423
|
+
for (const key of Object.keys(run.metrics || {})) {
|
|
424
|
+
if (isNumericMetric(run.metrics[key])) {
|
|
425
|
+
metricKeys.add(key);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (metricHint && metricKeys.has(metricHint)) {
|
|
431
|
+
return metricHint;
|
|
432
|
+
}
|
|
433
|
+
if (metricKeys.has("val_loss")) {
|
|
434
|
+
return "val_loss";
|
|
435
|
+
}
|
|
436
|
+
if (metricKeys.has("loss")) {
|
|
437
|
+
return "loss";
|
|
438
|
+
}
|
|
439
|
+
return metricKeys.values().next().value || "";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function summarizeDashboardRuns(runs, primaryMetric, preferHigher = false) {
|
|
443
|
+
const completeRuns = runs.filter((run) => run.status === "complete" || run.status === "completed");
|
|
444
|
+
const parseErrors = runs.filter((run) => run.parse_error).length;
|
|
445
|
+
const latestRun = [...runs].reverse().find((run) => !run.parse_error) || null;
|
|
446
|
+
|
|
447
|
+
const metricEntries = runs
|
|
448
|
+
.map((run, index) => ({
|
|
449
|
+
run,
|
|
450
|
+
index,
|
|
451
|
+
value: primaryMetric && isNumericMetric(run.metrics?.[primaryMetric]) ? Number(run.metrics[primaryMetric]) : Number.NaN,
|
|
452
|
+
}))
|
|
453
|
+
.filter((entry) => Number.isFinite(entry.value));
|
|
454
|
+
|
|
455
|
+
metricEntries.sort((a, b) => (preferHigher ? b.value - a.value : a.value - b.value));
|
|
456
|
+
const bestRun = metricEntries[0] || null;
|
|
457
|
+
const worstRun = metricEntries[metricEntries.length - 1] || null;
|
|
458
|
+
const series = metricEntries
|
|
459
|
+
.slice()
|
|
460
|
+
.sort((a, b) => a.index - b.index)
|
|
461
|
+
.map((entry) => ({
|
|
462
|
+
id: entry.run.id,
|
|
463
|
+
value: entry.value,
|
|
464
|
+
timestamp: entry.run.timestamp,
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
totalRuns: runs.length,
|
|
469
|
+
completeRuns: completeRuns.length,
|
|
470
|
+
parseErrors,
|
|
471
|
+
latestRun,
|
|
472
|
+
bestRun,
|
|
473
|
+
worstRun,
|
|
474
|
+
series,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function buildDashboardState(cwd) {
|
|
479
|
+
const goalPath = path.join(cwd, ".researchloop", "goal.md");
|
|
480
|
+
const planPath = path.join(cwd, ".researchloop", "plan.md");
|
|
481
|
+
const profilePath = path.join(cwd, ".researchloop", "repo-profile.json");
|
|
482
|
+
const ledgerPath = path.join(cwd, ".researchloop", "scratchpad", "runs.jsonl");
|
|
483
|
+
|
|
484
|
+
const goal = parseGoalFile(goalPath);
|
|
485
|
+
const plan = parsePlanFile(planPath);
|
|
486
|
+
let repoProfile = detectRepo(cwd);
|
|
487
|
+
if (fs.existsSync(profilePath)) {
|
|
488
|
+
try {
|
|
489
|
+
repoProfile = JSON.parse(readTextIfExists(profilePath));
|
|
490
|
+
} catch {
|
|
491
|
+
repoProfile = detectRepo(cwd);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const runs = parseRunsLedger(ledgerPath);
|
|
495
|
+
const primaryMetric = choosePrimaryMetric(goal, runs);
|
|
496
|
+
const preferHigher = String(goal.direction || "").toLowerCase().includes("high");
|
|
497
|
+
const summary = summarizeDashboardRuns(runs, primaryMetric, preferHigher);
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
cwd,
|
|
501
|
+
generatedAt: new Date().toISOString(),
|
|
502
|
+
goal,
|
|
503
|
+
plan,
|
|
504
|
+
repoProfile,
|
|
505
|
+
runs,
|
|
506
|
+
primaryMetric,
|
|
507
|
+
preferHigher,
|
|
508
|
+
summary,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function cmdDashboard() {
|
|
513
|
+
const cwd = targetDir();
|
|
514
|
+
const host = String(option("--host", "127.0.0.1"));
|
|
515
|
+
const port = Number(option("--port", 8787));
|
|
516
|
+
const dashboardFile = path.join(templatesRoot, "dashboard", "index.html");
|
|
517
|
+
const html = readTextIfExists(dashboardFile);
|
|
518
|
+
|
|
519
|
+
const server = http.createServer((req, res) => {
|
|
520
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
|
|
521
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
522
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
523
|
+
res.end(html);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (url.pathname === "/api/state") {
|
|
527
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
528
|
+
res.end(`${JSON.stringify(buildDashboardState(cwd), null, 2)}\n`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (url.pathname === "/api/runs") {
|
|
532
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
533
|
+
const state = buildDashboardState(cwd);
|
|
534
|
+
res.end(`${JSON.stringify(state.runs, null, 2)}\n`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (url.pathname === "/api/goal") {
|
|
538
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
539
|
+
const state = buildDashboardState(cwd);
|
|
540
|
+
res.end(`${JSON.stringify(state.goal, null, 2)}\n`);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
544
|
+
res.end("Not found");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
server.listen(port, host, () => {
|
|
548
|
+
const address = server.address();
|
|
549
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
550
|
+
console.log(`ResearchLoop dashboard running at http://${host}:${actualPort}`);
|
|
551
|
+
console.log(`Repo: ${cwd}`);
|
|
552
|
+
console.log("No auth. Localhost only.");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
process.on("SIGINT", () => server.close(() => process.exit(0)));
|
|
556
|
+
process.on("SIGTERM", () => server.close(() => process.exit(0)));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function loadRepoProfile(cwd) {
|
|
560
|
+
const profileFile = path.join(cwd, ".researchloop", "repo-profile.json");
|
|
561
|
+
if (fs.existsSync(profileFile)) {
|
|
562
|
+
try {
|
|
563
|
+
return JSON.parse(fs.readFileSync(profileFile, "utf8"));
|
|
564
|
+
} catch {
|
|
565
|
+
return detectRepo(cwd);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return detectRepo(cwd);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function buildIdeaList(profile, goalText) {
|
|
572
|
+
const adapters = Array.isArray(profile?.adapters) ? profile.adapters : ["generic"];
|
|
573
|
+
const adapterSet = new Set(adapters);
|
|
574
|
+
const ideas = [];
|
|
575
|
+
|
|
576
|
+
const add = (rank, title, hypothesis, change, killCriterion, whyNow) => {
|
|
577
|
+
ideas.push({ rank, title, hypothesis, change, killCriterion, whyNow });
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
if (adapterSet.has("llm-research-kit")) {
|
|
581
|
+
add(
|
|
582
|
+
1,
|
|
583
|
+
"Baseline config lock",
|
|
584
|
+
`We need a clean baseline for ${goalText || "the target metric"} before changing architecture.`,
|
|
585
|
+
"Run the current tiny config once with logging fixed, and verify val_loss parsing end to end.",
|
|
586
|
+
"If the baseline cannot be reproduced twice, do not touch architecture yet.",
|
|
587
|
+
"This tells us whether the metric path is trustworthy."
|
|
588
|
+
);
|
|
589
|
+
add(
|
|
590
|
+
2,
|
|
591
|
+
"Learning rate sweep",
|
|
592
|
+
`Learning rate is usually the cheapest knob for ${goalText || "loss reduction"}.`,
|
|
593
|
+
"Sweep a few learning rates while holding d_model, n_layers, batch size, and dataset fixed.",
|
|
594
|
+
"If no setting beats baseline by a meaningful margin, prune the family.",
|
|
595
|
+
"Cheap, high-signal, and likely to matter before architecture changes."
|
|
596
|
+
);
|
|
597
|
+
add(
|
|
598
|
+
3,
|
|
599
|
+
"Tiny architecture sweep",
|
|
600
|
+
"A small architecture change may help after the optimizer path is stable.",
|
|
601
|
+
"Try one change at a time: d_model, n_layers, or d_ff, but never stack them in the first pass.",
|
|
602
|
+
"If the change helps only once and not on reproduction, discard it.",
|
|
603
|
+
"This is the smallest architecture probe that still feels real."
|
|
604
|
+
);
|
|
605
|
+
add(
|
|
606
|
+
4,
|
|
607
|
+
"Second-seed reproduction",
|
|
608
|
+
"Any win should survive a second run before it is promoted.",
|
|
609
|
+
"Re-run the best candidate with a fresh seed and the same config.",
|
|
610
|
+
"If the win vanishes, demote it and keep searching.",
|
|
611
|
+
"This prevents the project from believing one lucky run."
|
|
612
|
+
);
|
|
613
|
+
} else if (adapterSet.has("huggingface")) {
|
|
614
|
+
add(
|
|
615
|
+
1,
|
|
616
|
+
"Baseline reproduction",
|
|
617
|
+
`Confirm the current Hugging Face training path before changing anything for ${goalText || "the target metric"}.`,
|
|
618
|
+
"Run the existing Trainer or training script once and capture the metric extraction path.",
|
|
619
|
+
"If the baseline is unstable, stop and fix reproducibility first.",
|
|
620
|
+
"Cheap signal before any parameter sweeps."
|
|
621
|
+
);
|
|
622
|
+
add(
|
|
623
|
+
2,
|
|
624
|
+
"Learning rate and schedule sweep",
|
|
625
|
+
"Trainer setups often respond first to LR and warmup changes.",
|
|
626
|
+
"Sweep learning rate, warmup ratio, and scheduler while keeping the dataset and model fixed.",
|
|
627
|
+
"If none of the runs improve, prune the family.",
|
|
628
|
+
"Usually the highest-return low-risk ablation."
|
|
629
|
+
);
|
|
630
|
+
add(
|
|
631
|
+
3,
|
|
632
|
+
"Batch and precision check",
|
|
633
|
+
"Throughput or stability may be limiting the current run.",
|
|
634
|
+
"Try a single batch-size or precision change, not both at once.",
|
|
635
|
+
"If validation gets noisier without speed or quality gain, stop.",
|
|
636
|
+
"A small systems-level change can reveal an easy win."
|
|
637
|
+
);
|
|
638
|
+
} else if (adapterSet.has("pytorch")) {
|
|
639
|
+
add(
|
|
640
|
+
1,
|
|
641
|
+
"Baseline reproduction",
|
|
642
|
+
`Confirm the current PyTorch path before changing anything for ${goalText || "the target metric"}.`,
|
|
643
|
+
"Run the existing train script once and capture the evaluation command plus metric path.",
|
|
644
|
+
"If the baseline cannot be reproduced, fix that first.",
|
|
645
|
+
"You need a stable starting point."
|
|
646
|
+
);
|
|
647
|
+
add(
|
|
648
|
+
2,
|
|
649
|
+
"Optimizer sweep",
|
|
650
|
+
"Optimizer choice is often the cheapest first lever.",
|
|
651
|
+
"Compare AdamW against the repo's current optimizer while holding everything else fixed.",
|
|
652
|
+
"If the alternative does not beat baseline, stop the family.",
|
|
653
|
+
"This is a small, interpretable change."
|
|
654
|
+
);
|
|
655
|
+
add(
|
|
656
|
+
3,
|
|
657
|
+
"Learning rate sweep",
|
|
658
|
+
"Learning rate usually matters more than most architectural changes early on.",
|
|
659
|
+
"Sweep a few learning rates around the current default.",
|
|
660
|
+
"If the curve is flat, prune the family.",
|
|
661
|
+
"Fast and often decisive."
|
|
662
|
+
);
|
|
663
|
+
} else {
|
|
664
|
+
add(
|
|
665
|
+
1,
|
|
666
|
+
"Find the baseline",
|
|
667
|
+
`Before optimizing ${goalText || "the target metric"}, identify the exact command that produces the current metric.`,
|
|
668
|
+
"Use inspect to find the training and evaluation commands, then run the smallest proof-of-life command.",
|
|
669
|
+
"If the baseline is unknown, do not guess at improvements yet.",
|
|
670
|
+
"The workflow starts with observability."
|
|
671
|
+
);
|
|
672
|
+
add(
|
|
673
|
+
2,
|
|
674
|
+
"Metric plumbing check",
|
|
675
|
+
"A lot of early failures are just missing metric extraction.",
|
|
676
|
+
"Make sure the repo prints one clear metric that can be compared run to run.",
|
|
677
|
+
"If no reliable metric exists, stop and add logging before tuning.",
|
|
678
|
+
"Without a metric, experiments are theater."
|
|
679
|
+
);
|
|
680
|
+
add(
|
|
681
|
+
3,
|
|
682
|
+
"Smallest config change",
|
|
683
|
+
"Once the metric is stable, try one config change at a time.",
|
|
684
|
+
"Prefer a hyperparameter or schedule change before touching architecture.",
|
|
685
|
+
"If a change is hard to explain or reproduce, discard it.",
|
|
686
|
+
"This keeps the first pass cheap and legible."
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return ideas;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function renderIdeasMarkdown(profile, goalText, ideas) {
|
|
694
|
+
const adapters = (profile?.adapters || ["generic"]).join(", ");
|
|
695
|
+
const lines = [
|
|
696
|
+
"# Research Ideas",
|
|
697
|
+
"",
|
|
698
|
+
`Goal: ${goalText || "Unknown"}`,
|
|
699
|
+
`Adapters: ${adapters}`,
|
|
700
|
+
"",
|
|
701
|
+
];
|
|
702
|
+
|
|
703
|
+
for (const idea of ideas) {
|
|
704
|
+
lines.push(
|
|
705
|
+
`## ${idea.rank}. ${idea.title}`,
|
|
706
|
+
"",
|
|
707
|
+
`Hypothesis: ${idea.hypothesis}`,
|
|
708
|
+
`Change: ${idea.change}`,
|
|
709
|
+
`Kill criterion: ${idea.killCriterion}`,
|
|
710
|
+
`Why now: ${idea.whyNow}`,
|
|
711
|
+
""
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return lines.join("\n");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function cmdIdea() {
|
|
719
|
+
const cwd = targetDir();
|
|
720
|
+
const researchDir = path.join(cwd, ".researchloop");
|
|
721
|
+
ensureDir(researchDir);
|
|
722
|
+
const goalText = option("--goal", "") || readGoalSummary(path.join(researchDir, "goal.md"));
|
|
723
|
+
const profile = loadRepoProfile(cwd);
|
|
724
|
+
const ideas = buildIdeaList(profile, goalText);
|
|
725
|
+
const markdown = renderIdeasMarkdown(profile, goalText, ideas);
|
|
726
|
+
process.stdout.write(`${markdown}\n`);
|
|
727
|
+
|
|
728
|
+
if (hasFlag("--write")) {
|
|
729
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
730
|
+
const file = path.join(researchDir, "scratchpad", "ideas", `${stamp}-ideas.md`);
|
|
731
|
+
ensureDir(path.dirname(file));
|
|
732
|
+
fs.writeFileSync(file, `${markdown}\n`);
|
|
733
|
+
console.log(`\nIdea note written to ${path.relative(cwd, file)}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function cmdCompare() {
|
|
738
|
+
const cwd = targetDir();
|
|
739
|
+
const ledger = path.join(cwd, ".researchloop", "scratchpad", "runs.jsonl");
|
|
740
|
+
const metricName = option("--metric", null);
|
|
741
|
+
const direction = String(option("--direction", "lower")).toLowerCase();
|
|
742
|
+
const preferHigher = direction === "higher" || direction === "max" || direction === "maximize";
|
|
743
|
+
|
|
744
|
+
if (!fs.existsSync(ledger)) {
|
|
745
|
+
console.log("No run ledger found. Run `researchloop init` first.");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const rows = fs
|
|
750
|
+
.readFileSync(ledger, "utf8")
|
|
751
|
+
.split("\n")
|
|
752
|
+
.filter(Boolean)
|
|
753
|
+
.map((row) => {
|
|
754
|
+
try {
|
|
755
|
+
return JSON.parse(row);
|
|
756
|
+
} catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
})
|
|
760
|
+
.filter(Boolean);
|
|
761
|
+
|
|
762
|
+
const candidates = rows.filter((row) => row.metrics && typeof row.metrics === "object");
|
|
763
|
+
let resolvedMetric = metricName;
|
|
764
|
+
if (!resolvedMetric) {
|
|
765
|
+
for (const row of candidates) {
|
|
766
|
+
for (const key of Object.keys(row.metrics)) {
|
|
767
|
+
if (Number.isFinite(rowMetricValue(row, key))) {
|
|
768
|
+
resolvedMetric = key;
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (resolvedMetric) {
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (!resolvedMetric) {
|
|
779
|
+
console.log("No comparable numeric metric found in the run ledger.");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const scored = candidates
|
|
784
|
+
.map((row) => ({
|
|
785
|
+
row,
|
|
786
|
+
value: rowMetricValue(row, resolvedMetric),
|
|
787
|
+
}))
|
|
788
|
+
.filter((entry) => Number.isFinite(entry.value));
|
|
789
|
+
|
|
790
|
+
if (!scored.length) {
|
|
791
|
+
console.log(`No numeric values found for metric: ${resolvedMetric}`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
scored.sort((a, b) => (preferHigher ? b.value - a.value : a.value - b.value));
|
|
796
|
+
const best = scored[0];
|
|
797
|
+
const worst = scored[scored.length - 1];
|
|
798
|
+
|
|
799
|
+
console.log(`metric: ${resolvedMetric}`);
|
|
800
|
+
console.log(`direction: ${preferHigher ? "higher" : "lower"}`);
|
|
801
|
+
console.log(`runs_compared: ${scored.length}`);
|
|
802
|
+
console.log(`best: ${best.row.id} = ${best.value}`);
|
|
803
|
+
console.log(`worst: ${worst.row.id} = ${worst.value}`);
|
|
804
|
+
console.log("top_3:");
|
|
805
|
+
for (const entry of scored.slice(0, 3)) {
|
|
806
|
+
console.log(`- ${entry.row.id}: ${entry.value}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function rowMetricValue(row, key) {
|
|
811
|
+
if (!row || !row.metrics || !(key in row.metrics)) {
|
|
812
|
+
return Number.NaN;
|
|
813
|
+
}
|
|
814
|
+
return Number(row.metrics[key]);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function parseMetric(metricText) {
|
|
818
|
+
const splitAt = metricText.indexOf("=");
|
|
819
|
+
if (splitAt === -1) {
|
|
820
|
+
return [metricText, true];
|
|
821
|
+
}
|
|
822
|
+
const key = metricText.slice(0, splitAt).trim();
|
|
823
|
+
const rawValue = metricText.slice(splitAt + 1).trim();
|
|
824
|
+
const numberValue = Number(rawValue);
|
|
825
|
+
return [key, Number.isNaN(numberValue) ? rawValue : numberValue];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function cmdRecord() {
|
|
829
|
+
const cwd = targetDir();
|
|
830
|
+
const ledger = path.join(cwd, ".researchloop", "scratchpad", "runs.jsonl");
|
|
831
|
+
ensureDir(path.dirname(ledger));
|
|
832
|
+
|
|
833
|
+
const metrics = {};
|
|
834
|
+
for (const metric of optionsAll("--metric")) {
|
|
835
|
+
const [key, value] = parseMetric(metric);
|
|
836
|
+
if (key) {
|
|
837
|
+
metrics[key] = value;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const row = {
|
|
842
|
+
id: String(option("--id", `run-${new Date().toISOString().replace(/[:.]/g, "-")}`)),
|
|
843
|
+
timestamp: new Date().toISOString(),
|
|
844
|
+
status: String(option("--status", "recorded")),
|
|
845
|
+
agent: String(option("--agent", "manual")),
|
|
846
|
+
command: option("--command", null),
|
|
847
|
+
metrics,
|
|
848
|
+
notes: String(option("--note", "")),
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
fs.appendFileSync(ledger, `${JSON.stringify(row)}\n`);
|
|
852
|
+
console.log(`Recorded run: ${row.id}`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function cmdHelp() {
|
|
856
|
+
console.log(`Research Loop
|
|
857
|
+
|
|
858
|
+
Usage:
|
|
859
|
+
researchloop init [--agent codex|claude-code|hermes|cursor] [--dir PATH] [--force]
|
|
860
|
+
researchloop goal [TEXT] [--dir PATH] [--metric NAME] [--direction lower|higher] [--baseline CMD] [--evaluation CMD] [--allowed TEXT] [--forbidden TEXT]
|
|
861
|
+
researchloop inspect [--dir PATH]
|
|
862
|
+
researchloop idea [--dir PATH] [--goal TEXT] [--write]
|
|
863
|
+
researchloop prompt [--agent codex|claude-code|hermes|generic] [--goal TEXT] [--focus hyperparameters|architecture|attention]
|
|
864
|
+
researchloop doctor [--dir PATH] [--python PATH]
|
|
865
|
+
researchloop record [--dir PATH] [--id ID] [--status STATUS] [--metric key=value] [--note TEXT]
|
|
866
|
+
researchloop compare [--dir PATH] [--metric NAME] [--direction lower|higher]
|
|
867
|
+
researchloop dashboard [--dir PATH] [--host HOST] [--port PORT]
|
|
868
|
+
researchloop report [--dir PATH]
|
|
869
|
+
|
|
870
|
+
Research Loop installs docs, prompts, scratchpads, and experiment ledgers for autonomous AI research agents.
|
|
871
|
+
`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (hasFlag("--help") || command === "help") {
|
|
875
|
+
cmdHelp();
|
|
876
|
+
} else if (command === "init") {
|
|
877
|
+
cmdInit();
|
|
878
|
+
} else if (command === "goal") {
|
|
879
|
+
cmdGoal();
|
|
880
|
+
} else if (command === "inspect") {
|
|
881
|
+
cmdInspect();
|
|
882
|
+
} else if (command === "idea") {
|
|
883
|
+
cmdIdea();
|
|
884
|
+
} else if (command === "prompt") {
|
|
885
|
+
cmdPrompt();
|
|
886
|
+
} else if (command === "doctor") {
|
|
887
|
+
cmdDoctor();
|
|
888
|
+
} else if (command === "record") {
|
|
889
|
+
cmdRecord();
|
|
890
|
+
} else if (command === "compare") {
|
|
891
|
+
cmdCompare();
|
|
892
|
+
} else if (command === "dashboard") {
|
|
893
|
+
cmdDashboard();
|
|
894
|
+
} else if (command === "report") {
|
|
895
|
+
cmdReport();
|
|
896
|
+
} else {
|
|
897
|
+
console.error(`Unknown command: ${command}`);
|
|
898
|
+
cmdHelp();
|
|
899
|
+
process.exitCode = 1;
|
|
900
|
+
}
|