Il y a un an, nous avons reconnu qu'une simple boucle d'agent avec des outils suffit pour les tâches de programmation.
Le projet pi / pi-mono de Mario Zechner illustre bien le propos : un agent de codage n'a pas besoin de grand-chose. Un seul outil bash suffit, et son harnais d'environ 1 000 lignes rivalise déjà avec Claude Code et Codex — au dixième de la taille des agents en production, et l'écart ne se trouve pas là où on l'attendrait.
La raison pour laquelle ça fonctionne, c'est que les agents sont déjà bons à deux choses qui se combinent bien : composer des commandes shell en pipelines, et écrire du code. Alors pourquoi les livrer avec une boîte à outils préfabriquée quand ils pourraient écrire les outils eux-mêmes, au moment précis où ils en ont besoin, adaptés à l'utilisateur, au projet, à la tâche en cours?
Ça vous rappelle quelque chose? C'est Emacs, encore une fois. Emacs n'a jamais vraiment été un éditeur avec des greffons — c'est un environnement d'exécution Lisp qui se trouve à éditer du texte et à se réécrire à la volée, ce qui explique pourquoi, quarante ans plus tard, des gens construisent encore des flux de travail entiers à l'intérieur. La même astuce s'applique à un agent de codage : donnez-lui un environnement d'exécution et un REPL, une seule capacité — écrire et exécuter du code — et arrêtez-vous là. Le reste (read, write, edit, grep, subagent, et tout ce qui viendra ensuite), l'agent l'écrira lui-même. Le code se compose bien mieux que MCP ou bash!
Hyper Code
Dans hyper-code, je construis exactement cela — un environnement d'exécution dans lequel l'agent peut vivre et s'étendre. Il est assez petit pour que l'agent puisse tenir toute l'architecture dans son contexte, et assez simple pour qu'il puisse l'étendre sans rien briser.
Un aperçu rapide de la forme de l'environnement d'exécution :
- Bun comme environnement d'exécution principal — rapide, beaucoup de bibliothèques intégrées, zéro dépendance.
- Style procédural/fonctionnel : état séparé des fonctions. Une fonction par fichier, rechargement à chaud sans redémarrage.
- SQLite pour les sessions, dans l'esprit de out of the tar pit.
- Une interface Web avec HTTP et HTML — pas de réinvention de la roue avec une TUI, pas de complexité de l'API VS Code. Facile à étendre et à personnaliser.
Exemple
Pour que ce soit concret, j'ai demandé à Claude de proposer une tâche réaliste et non triviale qu'un agent traiterait en un seul appel evalCode. Voici ce qu'il a répondu. Imaginez un utilisateur disant : « récupère les notes de publication depuis le dernier marqueur — commits, auteurs, PR liés, et mets le résultat en cache pour la prochaine fois. »
Dans le monde bash + MCP, ce sont cinq ou six outils distincts qu'il faudrait décrire et connecter à l'avance : git describe, git log, un analyseur de commits conventionnels, l'API GitHub pour les PR, SQLite pour le cache, la mise en forme Markdown. Chacun avec sa propre surface, chacun nécessitant un aller-retour pour être défini et appelé.
Dans un agent orienté REPL, c'est simplement un seul 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) + "…",
};
Du script ponctuel à votre propre outil
La première fois, l'agent écrit ceci de mémoire. La deuxième fois, il remarque qu'il a fait presque la même chose la semaine précédente. Et la troisième fois, au lieu de le réécrire, il sauvegarde le code dans un fichier et en fait une procédure de projet à part entière :
// 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 prochaine fois que quelqu'un demande des notes de publication, c'est une ligne au lieu de quarante. La fonction réside dans .hyper/, ce qui signifie qu'elle persiste dans le projet — et en quelques semaines, la base de code fait tranquillement croître une petite bibliothèque de procédures, chacune adaptée à ce dépôt précis et à cet auteur précis. On ne peut pas obtenir ça directement depuis n'importe quel harnais fixe, parce qu'aucun harnais fixe ne connaît vos habitudes.
Essayez-le
L'idée en une phrase : donnez au modèle l'environnement d'exécution complet et un seul outil, puis laissez-le écrire le reste — adapté à vous et à votre projet.
Le prototype se trouve à github.com/niquola/hyper-code2. Environ un millier de lignes sur Bun : un seul evalCode, des fichiers rechargés à chaud, SQLite pour les sessions, un clavardage Web à /. Fonctionne avec OpenAI, Anthropic, Groq, OpenRouter et un LM Studio local.
Si vous avez cinq minutes, essayez ceci :
bun install && cp .env.test .env— entrez la clé de votre fournisseur.bun src/$main.ts→ ouvrezhttp://localhost:3000.- Demandez : « lis ta propre base de code et explique comment elle fonctionne. »
- Demandez : « quels outils te manque-t-il pour travailler sur ...? »
- Demandez : « écris-le et appelle-le! »
Le projet est encore tout jeune. Si vous aimez l'idée — rejoignez l'aventure!





