Vor einem Jahr haben wir erkannt, dass eine einfache Agentenschleife mit Werkzeugen für Programmieraufgaben ausreicht.
Mario Zechners pi / pi-mono bringt es auf den Punkt: Ein Coding-Agent braucht nicht viel. Ein einziges bash-Werkzeug genügt, und sein ~1000-Zeilen-Rahmenwerk hält problemlos mit Claude Code und Codex mit – ein Zehntel der Größe der produktiven Agenten, wobei der Unterschied nicht dort liegt, wo man ihn vermuten würde.
Das funktioniert, weil Agenten bereits gut in zwei Dingen sind, die sich hervorragend ergänzen: dem Zusammensetzen von Shell-Befehlen zu Pipelines und dem Schreiben von Code. Warum sollte man sie also mit einem vorgefertigten Werkzeugkasten ausliefern, wenn sie die Werkzeuge selbst schreiben könnten – genau dann, wenn sie gebraucht werden, zugeschnitten auf den Benutzer, das Projekt und die jeweilige Aufgabe?
Kommt Ihnen das bekannt vor? Das ist Emacs – der gleiche Gedanke. Emacs war nie wirklich ein Editor mit Plugins – es ist eine Lisp-Laufzeitumgebung, die zufällig Text bearbeitet und sich selbst im laufenden Betrieb umschreibt. Genau deshalb bauen Menschen auch vierzig Jahre später noch ganze Arbeitsabläufe darin. Derselbe Trick passt auf einen Coding-Agenten: Geben Sie ihm eine Laufzeitumgebung und eine REPL, eine einzige Fähigkeit – Code schreiben und ausführen – und hören Sie dort auf. Den Rest (read, write, edit, grep, subagent, was auch immer als Nächstes kommt) schreibt der Agent selbst. Code lässt sich viel besser kombinieren als MCP oder bash!
Hyper Code
In hyper-code baue ich genau das – eine Laufzeitumgebung, in der der Agent leben und sich selbst erweitern kann. Sie ist klein genug, dass der Agent die gesamte Architektur in seinem Kontext hält, und einfach genug, dass er diese Architektur erweitern kann, ohne etwas zu beschädigen.
Ein kurzer Überblick über die Struktur der Laufzeitumgebung:
- Bun als Haupt-Laufzeitumgebung – schnell, viele eingebaute Bibliotheken, keine Abhängigkeiten.
- Prozeduraler/funktionaler Stil: Zustand von Funktionen getrennt. Eine Funktion pro Datei, Hot Reload ohne Neustart.
- SQLite für Sitzungen, im Geiste von out of the tar pit.
- Eine Web-Oberfläche mit HTTP und HTML – kein Neuerfinden des Rades mit einem TUI, keine Komplexität der VS Code-API. Leicht erweiterbar und anpassbar.
Beispiel
Um dies zu veranschaulichen, habe ich Claude gebeten, eine realistische, nicht triviale Aufgabe zu entwickeln, die ein Agent in einem einzigen evalCode-Aufruf erledigen würde. Hier ist, was dabei herauskam. Stellen Sie sich vor, ein Benutzer sagt: „Hol die Release Notes seit dem letzten Tag – Commits, Autoren, verknüpfte PRs – und speichere das Ergebnis für das nächste Mal im Cache."
In der Welt von bash + MCP sind das fünf oder sechs separate Werkzeuge, die man im Voraus beschreiben und verdrahten müsste: git describe, git log, ein Conventional-Commits-Parser, die GitHub-API für PRs, SQLite für den Cache, Markdown-Formatierung. Jedes mit seinem eigenen Aufwand, jedes ein Hin- und Hergang beim Definieren und Aufrufen.
In einem REPL-first-Agenten ist es nur ein einziger evalCode-Aufruf:
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) + "…",
};
Vom Einmal-Skript zum eigenen Werkzeug
Beim ersten Mal schreibt der Agent dies aus dem Gedächtnis. Beim zweiten Mal stellt er fest, dass er letzte Woche fast dasselbe getan hat. Und beim dritten Mal schreibt er es nicht erneut, sondern speichert den Code in einer Datei und macht daraus eine ordentliche Projektprozedur:
// 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");
Wenn jemand das nächste Mal Release Notes anfordert, ist es eine Zeile statt vierzig. Die Funktion liegt unter .hyper/, was bedeutet, dass sie im Projekt erhalten bleibt – und über einige Wochen wächst die Codebasis still und leise zu einer kleinen Bibliothek von Prozeduren heran, jede zugeschnitten auf dieses spezifische Repository und diesen spezifischen Autor. Das kann kein fertiges Rahmenwerk von Haus aus bieten, denn kein fertiges Rahmenwerk kennt Ihre Gewohnheiten.
Probieren Sie es aus
Die Idee in einem Satz: Geben Sie dem Modell die vollständige Laufzeitumgebung und ein einziges Werkzeug, und lassen Sie es den Rest selbst schreiben – zugeschnitten auf Sie und Ihr Projekt.
Der Prototyp ist unter github.com/niquola/hyper-code2 verfügbar. Etwa tausend Zeilen auf Bun: ein evalCode, Hot-Reload-Dateien, SQLite für Sitzungen, ein Web-Chat unter /. Funktioniert mit OpenAI, Anthropic, Groq, OpenRouter und einem lokalen LM Studio.
Wenn Sie fünf Minuten haben, probieren Sie Folgendes:
bun install && cp .env.test .env— tragen Sie den Schlüssel Ihres Anbieters ein.bun src/$main.ts→ öffnen Siehttp://localhost:3000.- Fragen Sie: „Lies deine eigene Codebasis und erkläre, wie sie funktioniert."
- Fragen Sie: „Welche Werkzeuge fehlen dir, um an ... zu arbeiten?"
- Fragen Sie: „Schreib es und ruf es auf!"
Das Projekt ist noch in den Kinderschuhen. Wenn Ihnen die Idee gefällt – machen Sie mit!





