From d8d798c5e1450152f870cf600018a82ee0c39563 Mon Sep 17 00:00:00 2001 From: whiteye <62688683+maskjelly@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:04:01 +0530 Subject: [PATCH 1/4] feat: add opencode research command (autoresearch pattern) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new 'opencode research' command that implements Karpathy's autoresearch pattern using opencode as the agent harness. - Scaffolds a research workspace with program.md, eval/eval.sh, target/ - Launches an autonomous research agent that iterates on the target - The agent modifies target/, runs eval, measures score, keeps/discards - Based on Karpathy's autoresearch (modify → experiment → measure → loop) - Uses program.md as the instruction file (human-edited meta-level) - eval/ is read-only; target/ is the agent's sandbox --- packages/opencode/src/cli/cmd/research.ts | 271 ++++++++++++++++++++++ packages/opencode/src/index.ts | 2 + 2 files changed, 273 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/research.ts diff --git a/packages/opencode/src/cli/cmd/research.ts b/packages/opencode/src/cli/cmd/research.ts new file mode 100644 index 000000000000..c8979dc1ba67 --- /dev/null +++ b/packages/opencode/src/cli/cmd/research.ts @@ -0,0 +1,271 @@ +import { EOL } from "os" +import { Effect } from "effect" +import { effectCmd, fail } from "../effect-cmd" +import { UI } from "../ui" + +// --------------------------------------------------------------------------- +// Template for program.md — the instruction file the agent reads +// Based on Karpathy's autoresearch pattern, but generalized for code research. +// --------------------------------------------------------------------------- + +function programMD(goal: string): string { + return `# Autoresearch + +This is an experiment in autonomous research. You (the AI agent) will iterate +on research artifacts to improve a measurable score. The human supervises at +the meta level (this file). You do the actual work. + +## Research Goal + +${goal} + +## Setup + +1. Read this file completely. +2. Read \`eval/eval.sh\` to understand how scoring works. **Do not modify eval/**. +3. Read the current state of \`target/\` — this is what you will iterate on. +4. Read \`results.tsv\` (if it exists) to see prior experiment history. +5. Initialize \`results.tsv\` with the header row if missing. +6. Establish a baseline: run \`bash eval/eval.sh\` and record the score. + +## What You Can Do + +- Modify any file inside \`target/\` — this is your sandbox. Architecture, + implementation, parameters, everything is fair game. +- Create new files inside \`target/\`. +- Run \`bash eval/eval.sh\` at any time to measure performance. +- Run shell commands to test your changes (compile, lint, test, benchmark). + +## What You Cannot Do + +- Modify anything inside \`eval/\`. These files are read-only. +- Modify \`program.md\`. +- Install new system packages without asking. +- Modify \`results.tsv\` directly — the scoring function outputs to it. + +## The Metric + +\`eval/eval.sh\` outputs a single floating-point number to stdout. +LOWER is better (like loss). The first line of stdout is the score. +If the script fails (non-zero exit), the experiment is a crash. + +## Output Format + +Results are logged in \`results.tsv\` (tab-separated). Columns: +- \`commit\` — git commit hash (7 chars) or "none" if not versioned +- \`score\` — the metric value (use 999.0 for crashes) +- \`status\` — \`keep\`, \`discard\`, or \`crash\` +- \`description\` — what this experiment tried + +## The Experiment Loop + +LOOP FOREVER: + +1. Review the current state and past results. +2. Form a hypothesis: what change might improve the score? +3. Implement the change in \`target/\`. +4. (If git is available) \`git commit -am "experiment: "\` +5. Run the evaluation: \`bash eval/eval.sh\` +6. Read the score from stdout. +7. Log the result in \`results.tsv\`. +8. If the score improved (decreased), keep the change and advance. +9. If the score is equal or worse, revert the change. + - If git: \`git reset --hard HEAD~1\` + - If no git: manually undo the change +10. Go to step 1. + +## Guidelines + +- **Never stop**. The loop runs until you are manually interrupted. +- **If you run out of ideas**, read more, try combinations of previous approaches, + or make more radical changes. +- **Crashes**: If eval fails, fix obvious bugs and retry. If the idea is + fundamentally broken, discard and move on. +- **Simplicity**: All else equal, simpler is better. A big improvement that + adds ugly complexity is worth less than a small improvement from clean code. +- **Timeout**: If an experiment takes more than 10 minutes, kill it and treat + as a crash. +- **You are autonomous**. Do not ask the human if you should continue. + Do not ask permission. The human may be asleep. Keep going. +` +} + +// --------------------------------------------------------------------------- +// Template for eval/eval.sh — the scoring harness +// --------------------------------------------------------------------------- + +const evalShTemplate = `#!/bin/bash +# eval/eval.sh — Scoring harness for autoresearch +# +# Output: a single floating-point score to stdout (lower is better). +# Exit 0 on success, non-zero on crash. +# +# IMPORTANT: This file is read-only. The agent must NOT modify it. + +# Default: measure target/ directory size as a proxy for complexity. +# Replace this with your actual evaluation logic. +# Examples: +# - Compile and count warnings +# - Run tests and measure pass rate +# - Benchmark execution time +# - Lint and count issues + +# Example: count lines of code in target/ (lower is better for simplicity) +find target/ -type f \\( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.go" -o -name "*.rs" -o -name "*.md" \\) 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}' + +# Alternative example: run a test suite and extract a score +# cd target && npm test 2>&1 | grep -oP 'tests: \\\\d+' | grep -oP '\\\\d+' +` + +// --------------------------------------------------------------------------- +// Template README for the research workspace +// --------------------------------------------------------------------------- + +const readmeTemplate = (goal: string) => `# Autoresearch Workspace + +**Goal**: ${goal} + +## Structure + +- \`program.md\` — instructions for the AI agent (human-edited) +- \`target/\` — the artifact being optimized (agent modifies this) +- \`eval/eval.sh\` — scoring harness (read-only, do not modify) +- \`results.tsv\` — experiment log + +## Usage + +\`\`\`bash +# Start the research loop +./start.sh +\`\`\` + +Edit \`program.md\` and \`eval/eval.sh\` to customize the research direction. +` + +const startShTemplate = `#!/bin/bash +# start.sh — Enter the autoresearch loop +set -euo pipefail +cd "$(dirname "$0")" +echo "Starting autoresearch loop. Press Ctrl+C to stop." +echo "Goal: $(head -1 program.md | sed 's/^# //')" +echo "" +opencode run --auto --agent build "I am an autonomous researcher. Read program.md and start the experiment loop. Never stop until interrupted." +` + +// --------------------------------------------------------------------------- +// Scaffold a research workspace +// --------------------------------------------------------------------------- + +async function scaffoldWorkspace(dir: string, goal: string): Promise { + const { mkdir, writeFile } = await import("node:fs/promises") + const path = await import("path") + const root = path.resolve(dir) + + await mkdir(path.join(root, "target"), { recursive: true }) + await mkdir(path.join(root, "eval"), { recursive: true }) + + await writeFile(path.join(root, "program.md"), programMD(goal), "utf-8") + await writeFile(path.join(root, "eval", "eval.sh"), evalShTemplate, "utf-8") + await writeFile(path.join(root, "start.sh"), startShTemplate, "utf-8") + await writeFile(path.join(root, "README.md"), readmeTemplate(goal), "utf-8") + + // Create empty results.tsv with header + const tsvHeader = "commit\tscore\tstatus\tdescription" + EOL + await writeFile(path.join(root, "results.tsv"), tsvHeader, "utf-8") + + // Create .gitkeep in target + await writeFile(path.join(root, "target", ".gitkeep"), "", "utf-8") + + // Make scripts executable + const { chmod } = await import("node:fs/promises") + await chmod(path.join(root, "eval", "eval.sh"), 0o755) + await chmod(path.join(root, "start.sh"), 0o755) +} + +// --------------------------------------------------------------------------- +// The opencode research command +// --------------------------------------------------------------------------- + +export const ResearchCommand = effectCmd({ + command: "research [query..]", + describe: "set up and run autonomous research (autoresearch pattern)", + instance: false, + builder: (yargs) => + yargs + .positional("query", { + describe: "research goal or topic", + type: "string", + array: true, + }) + .option("dir", { + type: "string", + describe: "workspace directory (default: ./.autoresearch)", + default: ".autoresearch", + }) + .option("goal", { + type: "string", + describe: "research goal (if not provided via query)", + }), + handler: Effect.fn("Cli.research")(function* (args) { + const goal = args.goal || args.query?.join(" ") || "" + if (!goal) { + return yield* fail( + "Provide a research goal. Usage: opencode research [--goal ] ", + ) + } + + const path = yield* Effect.promise(() => import("path")) + const dir = path.resolve(args.dir) + const { mkdir } = yield* Effect.promise(() => import("node:fs/promises")) + const { existsSync } = yield* Effect.promise(() => import("node:fs")) + + // Scaffold workspace + if (!existsSync(dir)) { + yield* Effect.promise(() => mkdir(dir, { recursive: true })) + yield* Effect.promise(() => scaffoldWorkspace(dir, goal)) + UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✓" + UI.Style.TEXT_NORMAL + " Research workspace created at " + dir) + } else { + UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + " Using existing workspace at " + dir) + } + + UI.empty() + UI.println("Goal: " + goal) + UI.println("Program: " + path.join(dir, "program.md")) + UI.println("Target: " + path.join(dir, "target/")) + UI.println("Eval: " + path.join(dir, "eval/eval.sh")) + UI.empty() + + // Launch the research agent + UI.println("Launching research agent... (Ctrl+C to stop)") + UI.empty() + + // Use opencode run to start the agent with program.md + // We spawn a child process rather than using the SDK directly for simplicity. + const { spawn } = yield* Effect.promise(() => import("node:child_process")) + + yield* Effect.async((resume) => { + const child = spawn("opencode", ["run", "--auto", "--agent", "build", "--dir", dir], { + stdio: "inherit", + env: { + ...process.env, + OPENCODE_AUTORESEARCH_GOAL: goal, + }, + }) + + child.on("exit", (code) => { + if (code === 0 || code === 130 || code === 143) { + // Normal exit, SIGINT, or SIGTERM + resume(Effect.void) + } else { + UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + ` Research agent exited with code ${code}`) + resume(Effect.void) + } + }) + + child.on("error", (err) => { + UI.error("Failed to launch research agent: " + err.message) + resume(Effect.void) + }) + }) + }), +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 13540a73a36f..05c3ec5c8a95 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -25,6 +25,7 @@ import { EOL } from "os" import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" +import { ResearchCommand } from "./cli/cmd/research" import { DbCommand } from "./cli/cmd/db" import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" @@ -99,6 +100,7 @@ const cli = yargs(args) .command(GithubCommand) .command(PrCommand) .command(SessionCommand) + .command(ResearchCommand) .command(PluginCommand) .command(DbCommand) .fail((msg, err) => { From e584aed8734dc15c4b46b27579f1dd4cce8f5b1e Mon Sep 17 00:00:00 2001 From: whiteye <62688683+maskjelly@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:09:35 +0530 Subject: [PATCH 2/4] =?UTF-8?q?chore(opencode):=20clean=20up=20research=20?= =?UTF-8?q?command=20=E2=80=94=20add=20--model=20flag,=20baseline=20eval,?= =?UTF-8?q?=20top-level=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opencode/src/cli/cmd/research.ts | 253 ++++++++++------------ 1 file changed, 113 insertions(+), 140 deletions(-) diff --git a/packages/opencode/src/cli/cmd/research.ts b/packages/opencode/src/cli/cmd/research.ts index c8979dc1ba67..0f771fd323e9 100644 --- a/packages/opencode/src/cli/cmd/research.ts +++ b/packages/opencode/src/cli/cmd/research.ts @@ -1,19 +1,22 @@ import { EOL } from "os" +import path from "path" +import { mkdir, writeFile, chmod } from "node:fs/promises" +import { existsSync } from "node:fs" +import { spawn } from "node:child_process" import { Effect } from "effect" import { effectCmd, fail } from "../effect-cmd" import { UI } from "../ui" // --------------------------------------------------------------------------- -// Template for program.md — the instruction file the agent reads -// Based on Karpathy's autoresearch pattern, but generalized for code research. +// program.md — the instruction file the agent follows +// Based on Karpathy's autoresearch (modify → experiment → measure → loop). // --------------------------------------------------------------------------- function programMD(goal: string): string { return `# Autoresearch -This is an experiment in autonomous research. You (the AI agent) will iterate -on research artifacts to improve a measurable score. The human supervises at -the meta level (this file). You do the actual work. +You are an autonomous researcher. Your job: iterate on \`target/\` to improve +a measurable score. The human edits this file; you do the actual work. ## Research Goal @@ -21,249 +24,219 @@ ${goal} ## Setup -1. Read this file completely. -2. Read \`eval/eval.sh\` to understand how scoring works. **Do not modify eval/**. -3. Read the current state of \`target/\` — this is what you will iterate on. -4. Read \`results.tsv\` (if it exists) to see prior experiment history. -5. Initialize \`results.tsv\` with the header row if missing. -6. Establish a baseline: run \`bash eval/eval.sh\` and record the score. +1. Read this file and \`eval/eval.sh\` completely. +2. Read \`target/\` — this is what you will iterate on. +3. Read \`results.tsv\` if it exists (prior experiment history). +4. **Establish a baseline**: run \`bash eval/eval.sh\` and record the score. +5. You are now ready to experiment. Never stop until interrupted. ## What You Can Do -- Modify any file inside \`target/\` — this is your sandbox. Architecture, - implementation, parameters, everything is fair game. +- Modify any file inside \`target/\` — architecture, implementation, params. - Create new files inside \`target/\`. -- Run \`bash eval/eval.sh\` at any time to measure performance. -- Run shell commands to test your changes (compile, lint, test, benchmark). +- Run \`bash eval/eval.sh\` at any time to get the current score. +- Run any shell command to test your changes (compile, lint, benchmark). ## What You Cannot Do - Modify anything inside \`eval/\`. These files are read-only. - Modify \`program.md\`. -- Install new system packages without asking. -- Modify \`results.tsv\` directly — the scoring function outputs to it. +- Install new system packages. ## The Metric -\`eval/eval.sh\` outputs a single floating-point number to stdout. -LOWER is better (like loss). The first line of stdout is the score. -If the script fails (non-zero exit), the experiment is a crash. +\`eval/eval.sh\` outputs a single float to stdout (first line). **Lower is better**. +If the script exits non-zero, the experiment is a crash (score = 999.0). -## Output Format +## Results -Results are logged in \`results.tsv\` (tab-separated). Columns: -- \`commit\` — git commit hash (7 chars) or "none" if not versioned -- \`score\` — the metric value (use 999.0 for crashes) +Log every experiment in \`results.tsv\` (tab-separated): +- \`commit\` — git hash or "none" +- \`score\` — metric value (999.0 for crashes) - \`status\` — \`keep\`, \`discard\`, or \`crash\` -- \`description\` — what this experiment tried +- \`description\` — what you tried -## The Experiment Loop +## Experiment Loop LOOP FOREVER: -1. Review the current state and past results. +1. Review state + past results. 2. Form a hypothesis: what change might improve the score? -3. Implement the change in \`target/\`. -4. (If git is available) \`git commit -am "experiment: "\` -5. Run the evaluation: \`bash eval/eval.sh\` -6. Read the score from stdout. -7. Log the result in \`results.tsv\`. -8. If the score improved (decreased), keep the change and advance. -9. If the score is equal or worse, revert the change. - - If git: \`git reset --hard HEAD~1\` - - If no git: manually undo the change -10. Go to step 1. +3. Implement it in \`target/\`. +4. (Optional) \`git commit -am "experiment: "\`. +5. Run \`bash eval/eval.sh\` and capture the score. +6. Log the result in \`results.tsv\`. +7. If score improved → keep the change and advance. +8. If score regressed → revert (\`git reset --hard HEAD~1\` or manual undo). +9. Go to step 1. ## Guidelines -- **Never stop**. The loop runs until you are manually interrupted. -- **If you run out of ideas**, read more, try combinations of previous approaches, - or make more radical changes. -- **Crashes**: If eval fails, fix obvious bugs and retry. If the idea is - fundamentally broken, discard and move on. -- **Simplicity**: All else equal, simpler is better. A big improvement that - adds ugly complexity is worth less than a small improvement from clean code. -- **Timeout**: If an experiment takes more than 10 minutes, kill it and treat - as a crash. -- **You are autonomous**. Do not ask the human if you should continue. - Do not ask permission. The human may be asleep. Keep going. +- **Never stop**. You are autonomous. The human may be asleep. +- **Run out of ideas?** Combine near-misses, try radical changes, re-read prior results. +- **Crashes**: fix obvious bugs and retry; if fundamentally broken, discard. +- **Simplicity**: simpler is better. A small improvement with clean code beats a large one with hacky code. +- **Timeout**: kill experiments exceeding 10 minutes, treat as crash. ` } -// --------------------------------------------------------------------------- -// Template for eval/eval.sh — the scoring harness -// --------------------------------------------------------------------------- - const evalShTemplate = `#!/bin/bash -# eval/eval.sh — Scoring harness for autoresearch +# eval/eval.sh — Scoring harness. Outputs a single float to stdout (lower=better). +# Exit 0 on success, non-zero on crash. This file is read-only. # -# Output: a single floating-point score to stdout (lower is better). -# Exit 0 on success, non-zero on crash. -# -# IMPORTANT: This file is read-only. The agent must NOT modify it. - -# Default: measure target/ directory size as a proxy for complexity. -# Replace this with your actual evaluation logic. -# Examples: +# Customize this for your specific research task. Examples: # - Compile and count warnings -# - Run tests and measure pass rate +# - Run tests, measure pass rate # - Benchmark execution time # - Lint and count issues -# Example: count lines of code in target/ (lower is better for simplicity) +# Default: count lines of code in target/ (simplicity metric) find target/ -type f \\( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.go" -o -name "*.rs" -o -name "*.md" \\) 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}' - -# Alternative example: run a test suite and extract a score -# cd target && npm test 2>&1 | grep -oP 'tests: \\\\d+' | grep -oP '\\\\d+' -` - -// --------------------------------------------------------------------------- -// Template README for the research workspace -// --------------------------------------------------------------------------- - -const readmeTemplate = (goal: string) => `# Autoresearch Workspace - -**Goal**: ${goal} - -## Structure - -- \`program.md\` — instructions for the AI agent (human-edited) -- \`target/\` — the artifact being optimized (agent modifies this) -- \`eval/eval.sh\` — scoring harness (read-only, do not modify) -- \`results.tsv\` — experiment log - -## Usage - -\`\`\`bash -# Start the research loop -./start.sh -\`\`\` - -Edit \`program.md\` and \`eval/eval.sh\` to customize the research direction. ` const startShTemplate = `#!/bin/bash -# start.sh — Enter the autoresearch loop set -euo pipefail cd "$(dirname "$0")" -echo "Starting autoresearch loop. Press Ctrl+C to stop." -echo "Goal: $(head -1 program.md | sed 's/^# //')" +echo "Autoresearch loop starting. Ctrl+C to stop." +echo "Goal: $(sed -n '1s/^# //p' program.md)" echo "" opencode run --auto --agent build "I am an autonomous researcher. Read program.md and start the experiment loop. Never stop until interrupted." ` // --------------------------------------------------------------------------- -// Scaffold a research workspace +// Scaffold a research workspace with template files // --------------------------------------------------------------------------- async function scaffoldWorkspace(dir: string, goal: string): Promise { - const { mkdir, writeFile } = await import("node:fs/promises") - const path = await import("path") const root = path.resolve(dir) - await mkdir(path.join(root, "target"), { recursive: true }) await mkdir(path.join(root, "eval"), { recursive: true }) - await writeFile(path.join(root, "program.md"), programMD(goal), "utf-8") await writeFile(path.join(root, "eval", "eval.sh"), evalShTemplate, "utf-8") await writeFile(path.join(root, "start.sh"), startShTemplate, "utf-8") - await writeFile(path.join(root, "README.md"), readmeTemplate(goal), "utf-8") - - // Create empty results.tsv with header - const tsvHeader = "commit\tscore\tstatus\tdescription" + EOL - await writeFile(path.join(root, "results.tsv"), tsvHeader, "utf-8") - // Create .gitkeep in target + const readme = `# Autoresearch: ${goal}\n\nSee \`program.md\` for instructions.\n` + await writeFile(path.join(root, "README.md"), readme, "utf-8") + await writeFile(path.join(root, "results.tsv"), "commit\tscore\tstatus\tdescription" + EOL, "utf-8") await writeFile(path.join(root, "target", ".gitkeep"), "", "utf-8") - - // Make scripts executable - const { chmod } = await import("node:fs/promises") await chmod(path.join(root, "eval", "eval.sh"), 0o755) await chmod(path.join(root, "start.sh"), 0o755) } // --------------------------------------------------------------------------- -// The opencode research command +// Run a baseline eval after scaffolding, so the user sees an initial score +// --------------------------------------------------------------------------- + +async function runBaseline(dir: string): Promise { + const { execSync } = await import("node:child_process") + try { + const score = execSync("bash eval/eval.sh", { cwd: dir, encoding: "utf-8", timeout: 30000 }) + .split(EOL)[0] + .trim() + const line = `none\t${score}\tkeep\tbaseline (initial)` + EOL + await writeFile(path.join(dir, "results.tsv"), `commit\tscore\tstatus\tdescription` + EOL + line, "utf-8") + UI.println(" Baseline score: " + score) + } catch { + UI.println(UI.Style.TEXT_WARNING_BOLD + " !" + UI.Style.TEXT_NORMAL + " Baseline eval failed (customize eval/eval.sh for your task)") + } +} + +// --------------------------------------------------------------------------- +// The research command // --------------------------------------------------------------------------- export const ResearchCommand = effectCmd({ command: "research [query..]", - describe: "set up and run autonomous research (autoresearch pattern)", + describe: "autonomous research loop (autoresearch pattern)", instance: false, builder: (yargs) => yargs .positional("query", { - describe: "research goal or topic", + describe: "research goal", type: "string", array: true, }) .option("dir", { type: "string", - describe: "workspace directory (default: ./.autoresearch)", + describe: "workspace directory", default: ".autoresearch", }) .option("goal", { type: "string", describe: "research goal (if not provided via query)", + }) + .option("model", { + alias: "m", + type: "string", + describe: "model to use for the research agent", + }) + .option("agent", { + alias: "a", + type: "string", + describe: "agent to use (default: build)", + default: "build", + }) + .option("baseline", { + type: "boolean", + describe: "run baseline eval after scaffolding", + default: true, }), handler: Effect.fn("Cli.research")(function* (args) { const goal = args.goal || args.query?.join(" ") || "" if (!goal) { return yield* fail( - "Provide a research goal. Usage: opencode research [--goal ] ", + "Usage: opencode research \n" + + " opencode research --goal \"optimize X\"\n" + + " opencode research --dir ./my-lab \"research topic\"", ) } - const path = yield* Effect.promise(() => import("path")) const dir = path.resolve(args.dir) - const { mkdir } = yield* Effect.promise(() => import("node:fs/promises")) - const { existsSync } = yield* Effect.promise(() => import("node:fs")) + const isNew = !existsSync(dir) - // Scaffold workspace - if (!existsSync(dir)) { - yield* Effect.promise(() => mkdir(dir, { recursive: true })) + if (isNew) { yield* Effect.promise(() => scaffoldWorkspace(dir, goal)) - UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✓" + UI.Style.TEXT_NORMAL + " Research workspace created at " + dir) + UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✓" + UI.Style.TEXT_NORMAL + " Created " + dir) + if (args.baseline) { + yield* Effect.promise(() => runBaseline(dir)) + } } else { - UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + " Using existing workspace at " + dir) + UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + " Using existing workspace " + dir) } UI.empty() - UI.println("Goal: " + goal) - UI.println("Program: " + path.join(dir, "program.md")) - UI.println("Target: " + path.join(dir, "target/")) - UI.println("Eval: " + path.join(dir, "eval/eval.sh")) + UI.println(" goal: " + goal) + UI.println(" program: " + path.join(dir, "program.md")) + UI.println(" target: " + path.join(dir, "target/")) + UI.println(" eval: " + path.join(dir, "eval/eval.sh")) + UI.println(" results: " + path.join(dir, "results.tsv")) UI.empty() - // Launch the research agent + // Build opencode run args + const runArgs = ["run", "--auto", "--agent", args.agent, "--dir", dir] + if (args.model) { + runArgs.push("--model", args.model) + } + runArgs.push("I am an autonomous researcher. Read program.md and start the experiment loop. Never stop until interrupted.") + UI.println("Launching research agent... (Ctrl+C to stop)") UI.empty() - // Use opencode run to start the agent with program.md - // We spawn a child process rather than using the SDK directly for simplicity. - const { spawn } = yield* Effect.promise(() => import("node:child_process")) - yield* Effect.async((resume) => { - const child = spawn("opencode", ["run", "--auto", "--agent", "build", "--dir", dir], { + const child = spawn("opencode", runArgs, { stdio: "inherit", - env: { - ...process.env, - OPENCODE_AUTORESEARCH_GOAL: goal, - }, + env: { ...process.env, OPENCODE_RESEARCH_GOAL: goal }, }) - child.on("exit", (code) => { if (code === 0 || code === 130 || code === 143) { - // Normal exit, SIGINT, or SIGTERM resume(Effect.void) } else { - UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + ` Research agent exited with code ${code}`) + UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + ` Agent exited with code ${code}`) resume(Effect.void) } }) - child.on("error", (err) => { - UI.error("Failed to launch research agent: " + err.message) + UI.error("Failed to launch agent: " + err.message) resume(Effect.void) }) }) From d2577c33469f3da3124cca512de12adf9cc97aa2 Mon Sep 17 00:00:00 2001 From: whiteye <62688683+maskjelly@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:20:33 +0530 Subject: [PATCH 3/4] =?UTF-8?q?fix(opencode):=20address=20review=20feedbac?= =?UTF-8?q?k=20=E2=80=94=20no=20else,=20no=20try/catch,=20git=20init,=20dy?= =?UTF-8?q?namic=20import=20spawn,=20fix=20eval=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opencode/src/cli/cmd/research.ts | 152 +++++++++------------- 1 file changed, 59 insertions(+), 93 deletions(-) diff --git a/packages/opencode/src/cli/cmd/research.ts b/packages/opencode/src/cli/cmd/research.ts index 0f771fd323e9..753473d2503f 100644 --- a/packages/opencode/src/cli/cmd/research.ts +++ b/packages/opencode/src/cli/cmd/research.ts @@ -2,16 +2,10 @@ import { EOL } from "os" import path from "path" import { mkdir, writeFile, chmod } from "node:fs/promises" import { existsSync } from "node:fs" -import { spawn } from "node:child_process" import { Effect } from "effect" import { effectCmd, fail } from "../effect-cmd" import { UI } from "../ui" -// --------------------------------------------------------------------------- -// program.md — the instruction file the agent follows -// Based on Karpathy's autoresearch (modify → experiment → measure → loop). -// --------------------------------------------------------------------------- - function programMD(goal: string): string { return `# Autoresearch @@ -63,11 +57,11 @@ LOOP FOREVER: 1. Review state + past results. 2. Form a hypothesis: what change might improve the score? 3. Implement it in \`target/\`. -4. (Optional) \`git commit -am "experiment: "\`. +4. \`git commit -am "experiment: "\`. 5. Run \`bash eval/eval.sh\` and capture the score. 6. Log the result in \`results.tsv\`. 7. If score improved → keep the change and advance. -8. If score regressed → revert (\`git reset --hard HEAD~1\` or manual undo). +8. If score regressed → \`git reset --hard HEAD~1\`. 9. Go to step 1. ## Guidelines @@ -75,23 +69,14 @@ LOOP FOREVER: - **Never stop**. You are autonomous. The human may be asleep. - **Run out of ideas?** Combine near-misses, try radical changes, re-read prior results. - **Crashes**: fix obvious bugs and retry; if fundamentally broken, discard. -- **Simplicity**: simpler is better. A small improvement with clean code beats a large one with hacky code. +- **Simplicity**: simpler is better. - **Timeout**: kill experiments exceeding 10 minutes, treat as crash. ` } const evalShTemplate = `#!/bin/bash -# eval/eval.sh — Scoring harness. Outputs a single float to stdout (lower=better). -# Exit 0 on success, non-zero on crash. This file is read-only. -# -# Customize this for your specific research task. Examples: -# - Compile and count warnings -# - Run tests, measure pass rate -# - Benchmark execution time -# - Lint and count issues - # Default: count lines of code in target/ (simplicity metric) -find target/ -type f \\( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.go" -o -name "*.rs" -o -name "*.md" \\) 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}' +find target/ -type f \\( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.go" -o -name "*.rs" \\) 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}' || echo "0" ` const startShTemplate = `#!/bin/bash @@ -99,14 +84,9 @@ set -euo pipefail cd "$(dirname "$0")" echo "Autoresearch loop starting. Ctrl+C to stop." echo "Goal: $(sed -n '1s/^# //p' program.md)" -echo "" opencode run --auto --agent build "I am an autonomous researcher. Read program.md and start the experiment loop. Never stop until interrupted." ` -// --------------------------------------------------------------------------- -// Scaffold a research workspace with template files -// --------------------------------------------------------------------------- - async function scaffoldWorkspace(dir: string, goal: string): Promise { const root = path.resolve(dir) await mkdir(path.join(root, "target"), { recursive: true }) @@ -114,36 +94,66 @@ async function scaffoldWorkspace(dir: string, goal: string): Promise { await writeFile(path.join(root, "program.md"), programMD(goal), "utf-8") await writeFile(path.join(root, "eval", "eval.sh"), evalShTemplate, "utf-8") await writeFile(path.join(root, "start.sh"), startShTemplate, "utf-8") - - const readme = `# Autoresearch: ${goal}\n\nSee \`program.md\` for instructions.\n` - await writeFile(path.join(root, "README.md"), readme, "utf-8") await writeFile(path.join(root, "results.tsv"), "commit\tscore\tstatus\tdescription" + EOL, "utf-8") await writeFile(path.join(root, "target", ".gitkeep"), "", "utf-8") await chmod(path.join(root, "eval", "eval.sh"), 0o755) await chmod(path.join(root, "start.sh"), 0o755) + const { execSync } = await import("node:child_process") + execSync("git init 2>/dev/null; true", { cwd: root, stdio: "ignore", timeout: 5000 }) } -// --------------------------------------------------------------------------- -// Run a baseline eval after scaffolding, so the user sees an initial score -// --------------------------------------------------------------------------- - -async function runBaseline(dir: string): Promise { - const { execSync } = await import("node:child_process") - try { - const score = execSync("bash eval/eval.sh", { cwd: dir, encoding: "utf-8", timeout: 30000 }) - .split(EOL)[0] - .trim() +function runBaseline(dir: string): Effect.Effect { + return Effect.promise(async () => { + const { execSync } = await import("node:child_process") + const out = execSync("bash eval/eval.sh", { cwd: dir, encoding: "utf-8", timeout: 30000 }) + const score = out.split(EOL)[0].trim() const line = `none\t${score}\tkeep\tbaseline (initial)` + EOL await writeFile(path.join(dir, "results.tsv"), `commit\tscore\tstatus\tdescription` + EOL + line, "utf-8") UI.println(" Baseline score: " + score) - } catch { - UI.println(UI.Style.TEXT_WARNING_BOLD + " !" + UI.Style.TEXT_NORMAL + " Baseline eval failed (customize eval/eval.sh for your task)") - } + }).pipe( + Effect.catch(() => + Effect.sync(() => { + UI.println(UI.Style.TEXT_WARNING_BOLD + " !" + UI.Style.TEXT_NORMAL + " Baseline eval failed (customize eval/eval.sh for your task)") + }), + ), + ) } -// --------------------------------------------------------------------------- -// The research command -// --------------------------------------------------------------------------- +function spawnAgent(dir: string, goal: string, model?: string, agent?: string): Effect.Effect { + return Effect.promise(async () => { + const opencodeBin = process.env.OPENCODE || "opencode" + const runArgs = ["run", "--auto", "--agent", agent || "build", "--dir", dir] + if (model) runArgs.push("--model", model) + runArgs.push("I am an autonomous researcher. Read program.md and start the experiment loop. Never stop until interrupted.") + const { spawn } = await import("node:child_process") + const child = spawn(opencodeBin, runArgs, { + stdio: "inherit", + env: { ...process.env, OPENCODE_RESEARCH_GOAL: goal }, + }) + await new Promise((resolve) => { + child.on("exit", (code) => { + if (code !== 0 && code !== 130 && code !== 143) { + UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + ` Agent exited with code ${code}`) + } + resolve() + }) + child.on("error", (err) => { + UI.error("Failed to launch agent: " + err.message) + resolve() + }) + }) + }) +} + +function printWorkspaceInfo(dir: string, goal: string): void { + UI.empty() + UI.println(" goal: " + goal) + UI.println(" program: " + path.join(dir, "program.md")) + UI.println(" target: " + path.join(dir, "target/")) + UI.println(" eval: " + path.join(dir, "eval/eval.sh")) + UI.println(" results: " + path.join(dir, "results.tsv")) + UI.empty() +} export const ResearchCommand = effectCmd({ command: "research [query..]", @@ -183,62 +193,18 @@ export const ResearchCommand = effectCmd({ }), handler: Effect.fn("Cli.research")(function* (args) { const goal = args.goal || args.query?.join(" ") || "" - if (!goal) { - return yield* fail( - "Usage: opencode research \n" + - " opencode research --goal \"optimize X\"\n" + - " opencode research --dir ./my-lab \"research topic\"", - ) - } + if (!goal) return yield* fail("Usage: opencode research ") const dir = path.resolve(args.dir) - const isNew = !existsSync(dir) - - if (isNew) { + if (!existsSync(dir)) { yield* Effect.promise(() => scaffoldWorkspace(dir, goal)) UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✓" + UI.Style.TEXT_NORMAL + " Created " + dir) - if (args.baseline) { - yield* Effect.promise(() => runBaseline(dir)) - } - } else { - UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + " Using existing workspace " + dir) - } - - UI.empty() - UI.println(" goal: " + goal) - UI.println(" program: " + path.join(dir, "program.md")) - UI.println(" target: " + path.join(dir, "target/")) - UI.println(" eval: " + path.join(dir, "eval/eval.sh")) - UI.println(" results: " + path.join(dir, "results.tsv")) - UI.empty() - - // Build opencode run args - const runArgs = ["run", "--auto", "--agent", args.agent, "--dir", dir] - if (args.model) { - runArgs.push("--model", args.model) - } - runArgs.push("I am an autonomous researcher. Read program.md and start the experiment loop. Never stop until interrupted.") + if (args.baseline) yield* runBaseline(dir) + } else UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + " Using existing workspace " + dir) + printWorkspaceInfo(dir, goal) UI.println("Launching research agent... (Ctrl+C to stop)") UI.empty() - - yield* Effect.async((resume) => { - const child = spawn("opencode", runArgs, { - stdio: "inherit", - env: { ...process.env, OPENCODE_RESEARCH_GOAL: goal }, - }) - child.on("exit", (code) => { - if (code === 0 || code === 130 || code === 143) { - resume(Effect.void) - } else { - UI.println(UI.Style.TEXT_WARNING_BOLD + "!" + UI.Style.TEXT_NORMAL + ` Agent exited with code ${code}`) - resume(Effect.void) - } - }) - child.on("error", (err) => { - UI.error("Failed to launch agent: " + err.message) - resume(Effect.void) - }) - }) + yield* spawnAgent(dir, goal, args.model, args.agent) }), }) From aeb8af9bdcb9589fb4d45680d662d3d88d1abac2 Mon Sep 17 00:00:00 2001 From: whiteye <62688683+maskjelly@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:26:02 +0530 Subject: [PATCH 4/4] fix(opencode): polish describe text for research command --- packages/opencode/src/cli/cmd/research.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/research.ts b/packages/opencode/src/cli/cmd/research.ts index 753473d2503f..c45e6e9220a1 100644 --- a/packages/opencode/src/cli/cmd/research.ts +++ b/packages/opencode/src/cli/cmd/research.ts @@ -157,7 +157,7 @@ function printWorkspaceInfo(dir: string, goal: string): void { export const ResearchCommand = effectCmd({ command: "research [query..]", - describe: "autonomous research loop (autoresearch pattern)", + describe: "run an autonomous research experiment (autoresearch pattern)", instance: false, builder: (yargs) => yargs