Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160// generate.mjs — author one new pad.//// The prompt is assembled, not written: the engine's own header IS the contract// (lib/pads.mjs says exactly what a wrapper must do), one real pad is included// as a worked example, and the taste block says what got kept and what got cut.// So the thing that teaches the model to write a pad is the same thing that// teaches a person to write one. There is no second, drifting copy of the spec.//// node cancelok/generate.mjs # one candidate → disks/<name>.mjs// node cancelok/generate.mjs --dry # print the prompt, write nothing
import { execFileSync } from "node:child_process";import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";import { resolve } from "node:path";import { REPO, pheromone, lineage, remember } from "./taste.mjs";
// launchd has never heard of ~/.local/bin, so the cron hands us an absolute// path. A bare "claude" works in a terminal and ENOENTs at 3am.const CLAUDE = process.env.CANCELOK_CLAUDE || "claude";
const DISKS = resolve(REPO, "system/public/aesthetic.computer/disks");const ENGINE = resolve(REPO, "system/public/aesthetic.computer/lib/pads.mjs");const EXAMPLE = resolve(DISKS, "wispo.mjs");const dry = process.argv.includes("--dry");// A BLIND generation is told nothing about what anyone liked. That's the point:// a loop fed only its own taste converges, and the convergence is invisible from// inside because every generation still beats the last one. Blindness is the only// part of the machine that can leave a local maximum.const blind = process.argv.includes("--blind");
// Every PAD that exists — from the ^pads bag and the loop's own lineage, not from// the disks directory, which holds several hundred unrelated AC pieces. A blind// generation needs the list of IDEAS already taken; a list of names is useless,// and a list of the wrong names is worse than nothing.function existing() { const kin = lineage(); const bag = JSON.parse( readFileSync(resolve(REPO, "system/public/aesthetic.computer/bags.json"), "utf8"), ); const codes = new Set([ ...(bag.bags?.pads?.items || []).map((i) => i.code), ...Object.keys(kin), ]); return [...codes].map((c) => (kin[c]?.trait ? `- ${c}: ${kin[c].trait}` : `- ${c}`));}
// Pad names are invented syllable-words (wispo, zorb, plimo, glibo, torva…).// The model picks one; we only insist it isn't already taken.const taken = (name) => existsSync(resolve(DISKS, `${name}.mjs`));
// The engine header is the contract — everything above `let cfg` is the spec.const contract = readFileSync(ENGINE, "utf8").split("let cfg =")[0];
const prompt = `You are writing ONE new "pad" for Aesthetic Computer.
A pad is a self-running audiovisual instrument piece. The whole screen is onebutton: tap or XY-drag to play it. Left alone it still runs itself. Every pad isa marriage of a VISUAL GENERATOR and a SYNTH VOICE, and the visual should BE thescore — you should be able to see the sound you're hearing.
Every pad is a thin wrapper over the shared engine. Here is the engine's ownheader, which is the complete contract. Obey it exactly:
--- lib/pads.mjs (contract) ---${contract}--- end contract ---
Here is one real, working pad as a worked example of the form:
--- disks/wispo.mjs ---${readFileSync(EXAMPLE, "utf8")}--- end example ---
${ blind ? `THIS ONE IS BLIND. You are deliberately NOT being told what anyone liked.
Here is every pad that already exists, so you can avoid them:
${existing().join("\n")}
Make something that is NOT ON THAT LIST and not adjacent to it. Different physics,different family of motion, different corner of music. If the list is full ofpendulums and orbits, do not make a third kind of swinging thing.
This generation exists precisely BECAUSE the loop is drifting toward its ownfavorites. Taste, fed back to itself, converges — and it can never notice from theinside, because every new pad still scores well against the last one. You are thepart of the system that gets to be ignorant on purpose. Use it.` : await pheromone()}
WRITE ONE NEW PAD.
Hard requirements:- A single .mjs file, a thin wrapper: import from "../lib/pads.mjs", define a CONFIG with bpm/steps/hooks, call initPad(CONFIG) inside boot (NOT at import — the engine is a session singleton), and export { boot, sim, paint, act }.- READ ctx.quality in onPaint and scale your cost by it (stride per-pixel loops, scale particle counts). It must hold ~60fps. A pad that can't will be killed by the gate before anyone sees it, and that is a wasted generation.- Pick a name that is an invented syllable-word in the house style (wispo, zorb, plimo, glibo, vunn, quilo, nibbo, torva, flim, brindo, cobo, loxa, gulmo). These names are ALREADY TAKEN — pick something that is not on this list: ${readdirSync(DISKS).filter((f) => f.endsWith(".mjs")).map((f) => f.slice(0, -4)).join(" ")}- Comment like the example: say WHY, name the allegory, no banner walls.
Output format — EXACTLY this, nothing else, no commentary before or after:
NAME: <the-name>TRAIT: <visual generator> × <synth voice>\`\`\`javascript<the complete file>\`\`\``;
if (dry) { console.log(prompt); process.exit(0);}
console.log("🧠 generating…");// `--tools ""` because `claude --print` can WRITE FILES, and a generator that// writes its own pad will then collide with itself when we check the name. We// want a language model here, not an agent: it returns text, WE decide what// lands on disk. (This cost a whole generation to learn.)const raw = execFileSync(CLAUDE, ["--print", "--model", "sonnet", "--tools", ""], { input: prompt, encoding: "utf8", maxBuffer: 1024 * 1024 * 8, cwd: REPO,});
const name = /NAME:\s*([a-z][a-z0-9-]*)/i.exec(raw)?.[1]?.toLowerCase();// AC's font is ASCII — a stray × lands on screen as a `?`. The trait is shown// in the judging bar, so it has to be typeable in the same alphabet as a piece.const trait = (/TRAIT:\s*(.+)/.exec(raw)?.[1]?.trim() || "") .replace(/[×✕]/g, "x") .replace(/[^\x20-\x7e]/g, "");const code = /```(?:javascript|js)?\n([\s\S]*?)```/.exec(raw)?.[1];
if (!name || !code) { console.error("❌ couldn't parse a pad out of that. Raw head:\n" + raw.slice(0, 400)); process.exit(1);}if (taken(name)) { console.error(`❌ ${name} already exists — burned a generation on a collision.`); process.exit(1);}if (!/initPad\(/.test(code) || !/export\s*\{[^}]*boot/.test(code)) { console.error(`❌ ${name} doesn't honor the contract (no initPad, or no boot export).`); process.exit(1);}
writeFileSync(resolve(DISKS, `${name}.mjs`), code);// An invented pad has no parent, but it still has an IDEA — and the idea is what// the next blind generation needs in order to avoid repeating it.remember(name, { parent: null, second: null, mutation: blind ? "blind" : "fresh", trait, born: new Date().toISOString() });console.log(`✍️ ${name} — ${trait}${blind ? " (blind)" : ""}`);console.log(JSON.stringify({ code: name, name, trait }));