Hace un año reconocimos que un bucle de agente simple con herramientas es suficiente para tareas de programación.
El proyecto pi / pi-mono de Mario Zechner lo demuestra: un agente de código no necesita mucho. Una herramienta bash es suficiente, y su harness de ~1000 líneas ya se mantiene a la par de Claude Code y Codex — una décima parte del tamaño de los agentes en producción, y la diferencia no está donde uno esperaría.
La razón por la que funciona es que los agentes ya son buenos en dos cosas que se combinan muy bien: componer comandos de shell en pipelines y escribir código. Entonces, ¿para qué enviarlos con una caja de herramientas predefinida cuando podrían escribir las herramientas ellos mismos, justo cuando las necesitan, adaptadas al usuario, al proyecto y a la tarea en cuestión?
¿Le resulta familiar? Es Emacs de nuevo. Emacs nunca fue realmente un editor con plugins — es un entorno de ejecución Lisp que, de paso, edita texto y se reescribe a sí mismo al vuelo; por eso, cuarenta años después, la gente sigue construyendo flujos de trabajo enteros dentro de él. El mismo truco encaja en un agente de código: proporciónele un entorno de ejecución y un REPL, una sola capacidad — escribir y ejecutar código — y deténgase ahí. El resto (read, write, edit, grep, subagent, lo que surja después) el agente lo escribirá por sí solo. ¡El código se compone mucho mejor que MCP o bash!
Hyper Code
En hyper-code estoy construyendo exactamente eso — un entorno de ejecución donde el agente puede vivir y extenderse a sí mismo. Es lo suficientemente pequeño como para que el agente mantenga toda la arquitectura en su contexto, y lo suficientemente sencillo como para que pueda ampliar esa arquitectura sin romper nada.
Un esquema rápido de la forma del entorno de ejecución:
- Bun como entorno de ejecución principal — rápido, con muchas bibliotecas integradas, sin dependencias externas.
- Estilo procedural/funcional: estado separado de las funciones. Una función por archivo, recarga en caliente sin necesidad de reiniciar.
- SQLite para sesiones, en el espíritu de out of the tar pit.
- Una interfaz web con HTTP y HTML — sin reinventar la rueda con una TUI, sin la complejidad de la API de VS Code. Fácil de extender y personalizar.
Ejemplo
Para concretar esto, le pedí a Claude que propusiera una tarea realista y no trivial que un agente gestionaría en una sola llamada a evalCode. Esto fue lo que respondió. Imagine que un usuario dice: «extrae las notas de lanzamiento desde la última etiqueta — commits, autores, PRs vinculados, y guarda el resultado en caché para la próxima vez.»
En el mundo de bash + MCP, serían cinco o seis herramientas separadas que habría que describir y conectar con antelación: git describe, git log, un analizador de commits convencionales, la API de GitHub para PRs, SQLite para la caché, y formato markdown. Cada una con su propia superficie de API, cada una con un ciclo de ida y vuelta para definir y llamar.
En un agente orientado al REPL es simplemente un evalCode:
const repo = "niquola/hyper-code2";
const headers = { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` };
// 1. Last tag and commits since
const tag = (await Bun.$`git describe --tags --abbrev=0`.text()).trim();
const raw = await Bun.$`git log ${tag}..HEAD --pretty=format:%H|%s|%an`.text();
// 2. Local SQLite cache — if we've been here already, return what we have
await ctx.fns.db.exec(ctx, `
CREATE TABLE IF NOT EXISTS release_notes (
tag TEXT PRIMARY KEY, head TEXT, body TEXT, at INTEGER
)
`);
const head = (await Bun.$`git rev-parse HEAD`.text()).trim();
const cached = await ctx.fns.db.select(ctx,
"SELECT body FROM release_notes WHERE tag = ? AND head = ?", [tag, head]);
if (cached.length) {
agent.scratchpad.release = { tag, body: cached[0].body, cached: true };
return { since: tag, cached: true, preview: cached[0].body.slice(0, 300) + "…" };
}
// 3. Parse conventional commits: "feat(scope): message (#42)"
const commits = raw.split("\n").filter(Boolean).map(line => {
const [sha, subject, author] = line.split("|");
const m = subject.match(/^(feat|fix|chore|refactor|docs|test)(?:\([^)]+\))?:\s*(.+?)(?:\s*\(#(\d+)\))?$/);
return { sha, author, type: m?.[1] ?? "other", title: m?.[2] ?? subject, pr: m?.[3] };
});
// 4. Fetch linked PRs in parallel — labels and linked issues come with them
const prNums = [...new Set(commits.map(c => c.pr).filter(Boolean))];
const prs = await Promise.all(prNums.map(n =>
fetch(`https://api.github.com/repos/${repo}/pulls/${n}`, { headers }).then(r => r.json())
));
// 5. Group by type, build markdown
const byType = Map.groupBy(commits, c => c.type);
const body = [...byType].map(([type, cs]) =>
`## ${type}\n${cs.map(c =>
`- ${c.title}${c.pr ? ` (#${c.pr})` : ""} — @${c.author}`
).join("\n")}`
).join("\n\n");
// 6. Save the cache — good until HEAD moves
await ctx.fns.db.exec(ctx,
"INSERT OR REPLACE INTO release_notes VALUES (?, ?, ?, ?)",
[tag, head, body, Date.now()]);
// 7. Heavy stuff stays in scratchpad. The model gets a short summary.
agent.scratchpad.release = { tag, commits, prs, body };
return {
since: tag,
commits: commits.length,
by_type: Object.fromEntries([...byType].map(([t, cs]) => [t, cs.length])),
contributors: [...new Set(commits.map(c => c.author))],
prs_linked: prs.length,
labels: [...new Set(prs.flatMap(p => p.labels?.map(l => l.name) ?? []))],
preview: body.slice(0, 300) + "…",
};
Del script puntual a su propia herramienta
La primera vez, el agente escribe esto de memoria. La segunda vez, se da cuenta de que hizo casi lo mismo la semana pasada. Y la tercera vez, en lugar de volver a escribirlo, guarda el código en un archivo y lo convierte en un procedimiento de proyecto propiamente dicho:
// Turn N: pin the pattern
await Bun.write(".hyper/release/notes.ts", `
export default async function (ctx: Context, repo: string) {
// ... same logic as above — now a reusable function
return { tag, commits, prs, body };
}
`);
// Hot-load into the live runtime and regenerate types — no restart
await ctx.fns.repl.load(ctx, "release");
await ctx.genTypes(ctx);
// Turn N+1: call it like a built-in
await ctx.fns.release.notes(ctx, "niquola/hyper-code2");
La próxima vez que alguien pida las notas de lanzamiento, será una línea en lugar de cuarenta. La función vive bajo .hyper/, lo que significa que permanece en el proyecto — y a lo largo de unas semanas la base de código crece silenciosamente con una pequeña biblioteca de procedimientos, cada uno adaptado a este repositorio específico y a este autor específico. Eso no se puede obtener de serie con ningún harness fijo, porque ningún harness fijo conoce sus hábitos.
Pruébelo
La idea en una frase: proporcione al modelo el entorno de ejecución completo y una sola herramienta, y deje que escriba el resto — adaptado a usted y a su proyecto.
El prototipo está en github.com/niquola/hyper-code2. Unas mil líneas sobre Bun: un evalCode, archivos recargados en caliente, SQLite para sesiones, un chat web en /. Funciona con OpenAI, Anthropic, Groq, OpenRouter y LM Studio local.
Si dispone de cinco minutos, pruebe esto:
bun install && cp .env.test .env— introduzca la clave de su proveedor.bun src/$main.ts→ abrahttp://localhost:3000.- Pregunte: «lee tu propia base de código y explica cómo funciona.»
- Pregunte: «¿qué herramientas te faltan para trabajar en ...?»
- Pregunte: «¡escríbela y llámala!»
El proyecto todavía está en pañales. Si le gusta la idea — ¡únase a la diversión!





