Fluxyr

Fluxyr Research · Code generation

Quill: Web Apps as Micro-Prompts

A proof-of-concept for ephemeral code generation — one spec, three languages.

In Ephemeral Code Generation we argued that the durable asset in an AI-built system is not the code — it is the specification. Code generated to do one job can be discarded and regenerated on demand; the compact natural-language spec, the micro-prompt, is what deserves to be versioned and maintained.

That article ended with a conjecture: a tool that versions a project as a sequence of micro-prompts and a resolved state, and regenerates the actual code on demand — the way database migrations version a schema. Quill is that tool, built as a proof of concept. This post reports what it is, and shows it working: the same specification, regenerated into a Python/Flask, a TypeScript/Express, and a Ruby/Sinatra application — each producing byte-for-byte identical behavior.

Quill is open source (github.com/fluxyr-app/quill) and on npm (npm i -g @fluxyr/quill).

The idea: code as a derived artifact

A conventional web app is a codebase you maintain. A Quill app is an ordered stack of micro-prompts you maintain; the code under generated/ is a derived artifact — like a compiled binary, or a materialized database schema. You edit the prompts, not the code, and Quill regenerates.

Three layers make this work:

  • Core conventions — a single, shared, language-agnostic specification of how an application behaves: how identifiers work, what a resource is, how endpoints are shaped, how authorization and errors work. These are owned by Quill and reused by every target. Crucially, they contain no code and no framework — only behavior.
  • Stack bindings — a stack maps those conventions onto one framework. Each stack ships a binding.md (the framework realization), a minimal scaffold, and a verify() step (compile / lint / test). Quill ships three: Flask, Express, Sinatra.
  • Feature prompts — your project's own micro-prompts, applied in order like migrations. 0001-create-users.md, 0002-add-projects.md, and so on.

The separation is the whole trick. Here is the entire specification of the identifier scheme — a core convention, shared verbatim across all three languages:

prompts / core / identifiers
# Identifiers — short IDs

Every resource is identified by a UUID. Internally the full UUID is used.
Externally — everywhere an id crosses the boundary to a client — the id is
represented as a compact short ID.

- The short ID is a Base-62 encoding of the 128-bit UUID using the alphabet
  0-9, then A-Z, then a-z, most-significant digit first, left-padded to
  exactly 22 characters.
- Encoding and decoding are exact inverses: any UUID -> short ID -> UUID
  returns the original UUID.
- Ids are never hand-formatted in output; serializing a resource emits its
  short ID automatically. Inbound ids are resolved to UUIDs before use.

Nothing there is Python, TypeScript, or Ruby. The binding for each stack says how to realize it — "implement it in app/utils/uuid.py", "in src/utils/shortId.ts", "in lib/utils/short_id.rb" — and the model writes the actual codec.

The engine

quill up applies pending feature prompts. For each one:

  1. Compose the system prompt: the shared core conventions + the stack's binding.
  2. Generate — an LLM (via OpenRouter, so any model works) returns whole-file edits as a small JSON protocol; Quill applies them.
  3. Self-review — one pass where the model critiques its own output against the spec.
  4. Verify — the stack's verify() runs the real toolchain (pytest / tsc + node:test / rake test). On failure, the errors are fed back for up to N repair passes.
  5. Record — the prompt hash, the resulting code hash, and the model are written to quill.lock.json, the resolved-state ledger.

This is the "repair loop" from the original article, made concrete: a cheap model plus a self-review pass, gated by execution feedback.

A walkthrough

terminal
$ npm install -g @fluxyr/quill
$ export OPENROUTER_API_KEY=sk-or-...

$ create-quill-app blog --stack sinatra
✓ Created Quill project 'blog' (stack: sinatra, model: minimax/minimax-m3)

$ cd blog
$ quill new "create users"
✓ created prompts/0001-create-users.md

You fill in prompts/0001-create-users.md with a spec — note it names no files and no Ruby:

prompts / 0001-create-users
Add a User resource.

Fields:
- email — string, required, unique.
- name — string, required.

Endpoints (account-scoped): list; create (400 if email/name missing, 409 if
the email already exists); get one by id (404 if missing).

Tests: cover create + list + get, and assert the returned id is a 22-character
short ID.

Then quill up. The run streams progress — including a … thinking phase for reasoning models, so it never looks frozen:

terminal — quill up
$ quill up
Applying 2 pending prompt(s) via OpenRouter (model: anthropic/claude-haiku-4.5)...

▸ applying 0001-create-users
  → anthropic/claude-haiku-4.5 …
    … receiving (3035 chars, 8s)
  · 4 file(s) written (13s) — User resource: short-ID codec, model,
    account-scoped list/create/get routes, tests.
  ↻ self-review pass
  ✓ verify: syntax + tests pass
✓ 0001-create-users  (repair passes: 1)

▸ applying 0002-add-projects
  → anthropic/claude-haiku-4.5 …
  · 3 file(s) written (11s) — Project model, projects routes
    (list/create/get/delete), tests.
  ↻ self-review pass
  ✓ verify: syntax + tests pass
✓ 0002-add-projects  (repair passes: 1)

✓ Up to date — applied 2 prompt(s).

The generated/ directory now contains a working Sinatra app. quill status shows the ledger; quill checkpoint v1 snapshots it and proves reproducibility by regenerating from scratch and re-running the tests.

Does it actually work?

Three independent signals say yes.

The repair loop catches real bugs. These are not hypothetical. On the Flask app, the self-review pass caught the model hallucinating a foreign key to a table that did not exist and removed it. On a later run, a reasoning model's self-review found and fixed a TypeScript strict-null narrowing bug in its own output:

self-review note
· Fixed strict-null-check issue: the resolved userUuid (string | null) was used
  after a truthy check on a different variable, so it wasn't narrowed. Restructured
  to check userUuid directly before use.

verify() is a hard gate. A prompt is only marked applied after the real toolchain passes — pytest for Flask, tsc --noEmit + node --test for Express, rake test for Sinatra. Generated code that doesn't compile or fails its tests triggers another repair pass rather than being silently accepted.

Regeneration is reproducible. quill checkpoint snapshots the app, then does a full regen from the prompt stack and verifies it. With a deterministic model at temperature 0, two independent regenerations of the same app produced byte-identical code (matching codeHash in the ledger) — the prompts genuinely reproduce the app, not just something that looks similar.

One spec, three languages

This is the core demonstration. We wrote two feature prompts — a User resource and a Project resource owned by a user — and applied them, unchanged in intent, on all three stacks. The identifier convention is the most load-bearing shared behavior, so we scrutinized the generated short-ID codec in each language.

Python (Flask) — generated app/utils/uuid.py:

python
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

class Uuid:
    @staticmethod
    def shorten(value):
        num = value.int if isinstance(value, uuid.UUID) else uuid.UUID(value).int
        chars = []
        for _ in range(22):
            num, rem = divmod(num, 62)
            chars.append(ALPHABET[rem])
        return "".join(reversed(chars))

TypeScript (Express) — generated src/utils/shortId.ts:

typescript
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

export function shorten(uuid: string): string {
  let num = BigInt("0x" + uuid.replace(/-/g, ""));
  const digits: string[] = [];
  while (num > 0n) {
    digits.unshift(ALPHABET[Number(num % 62n)]);
    num = num / 62n;
  }
  return digits.join("").padStart(22, "0");
}

Ruby (Sinatra) — generated lib/utils/short_id.rb:

ruby
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

def self.shorten(uuid)
  num = uuid.to_s.delete("-").to_i(16)
  encoded = ""
  while num > 0
    encoded = ALPHABET[num % 62] + encoded
    num /= 62
  end
  encoded.rjust(22, "0")
end

Three languages, three idioms (Python's divmod, TypeScript's BigInt, Ruby's to_i(16)), one algorithm. To confirm they are not just similar but equivalent, we cross-checked each generated codec against a reference implementation of the Base-62 scheme over 2,000+ UUIDs, including the edge cases (all-zero, all-ones):

All three codecs are byte-identical to the reference across 2,002+ UUIDs, with perfect round-trips.

And the apps behave identically on the wire. A live request against each — create a user, create a project referencing it, fetch it, attempt a duplicate — returns the same status codes and the same 22-character short-ID format:

live round-trip — identical across stacks
user 201 · id length 22 · project 201 · FK matches · get 200 · duplicate email 409

The numbers

StackLanguage / frameworkModelFiles (2 features)TestsCodec vs. reference
FlaskPython / Flask / SQLAlchemyminimax-m3618 pass (pytest)identical / 2,003 UUIDs
ExpressTypeScript / Expressclaude-haiku-4.5811 pass (node:test) + tsc cleanidentical / 2,002 UUIDs
SinatraRuby / Sinatraclaude-haiku-4.5718 runs, 58 assertions (rake)identical / 2,002 UUIDs

The same two prompts, three stacks, one behavior.

The same two prompts, three languages, one behavior. The spec is the asset; the language is a deployment choice.

What we learned

A cheap model plus one self-review pass is enough. Consistent with the original benchmark, we did not need a frontier model. Both fast, inexpensive models and a slower reasoning model reached passing, verified code — the self-review pass and the verify gate do the heavy lifting, not raw model capability.

A new stack is the best test of your spec. Building the third stack was where the conventions got stress-tested. Express registers routers at the application root, so routes need full paths — but the model, reasonably, assumed a prefixed mount and wrote relative paths, producing 404s. The fix was not in the model or the code; it was making the binding explicit ("declare full, absolute paths") with a worked example. An underspecified convention is invisible until a second implementation disagrees with the first.

Reasoning models need a visible thinking phase. One model streamed several minutes of internal reasoning tokens before emitting any code. Because our progress display initially only surfaced content, the run looked frozen. Surfacing the reasoning stream as a … thinking heartbeat fixed the UX — a small but real lesson about driving reasoning models in a tool.

Context hygiene matters more than you'd think. An early bug let the source-context collector walk into node_modules, inflating a single prompt to 200k+ tokens. A cheap model silently tolerated it (slowly, expensively); a model with a hard context limit rejected it outright. The generator must aggressively exclude every stack's dependency directory.

The code still matters — just not the way it did

It's tempting to read "code is a derived artifact" as "code is disposable." That's too strong. The generated code still runs in production, still has a security surface, still accrues performance characteristics and technical debt. It has to live somewhere — committed, deployed, observable. What changes is not whether code matters, but how we relate to it.

Today, the human relationship to code is review: someone reads a 25,000-line pull request and tries to reason about its correctness. That doesn't scale — and in an AI-generation world it's the wrong layer to review anyway. The diff is a rendering of a decision that was actually made in the spec.

The plausible shape is a division of labor:

  • Humans review specifications. The micro-prompt is the small, durable, meaningful artifact — that's what gets read, argued about, and versioned. A 30-line spec is reviewable in a way a 25k-line diff never was.
  • Agents tend the code. The stored code is worked on continuously by autonomous agents whose job is not to add features — features come from the spec — but to improve what's already there: reduce debt, tighten performance, patch vulnerabilities, refactor toward the conventions, keep dependencies current. They operate on a codebase that is stored and real, but they are not gated by a human reading a giant diff.

In that world, Quill's ledger — prompts, plus resolved code, plus verification state — is the substrate both halves work against. A human changes a spec and the code regenerates; an agent improves the code and the verification gate plus the spec keep it honest. Nobody reviews a 25k-line PR, because that was never the artifact worth reviewing.

Quill today implements only the first half — spec to code. The second half — fleets of agents that continuously tend stored code against a spec and a verification gate — is the natural next system to build. The code doesn't disappear. It just stops being the thing humans pore over, and becomes the thing agents maintain.

Limitations and what's next

Quill is a proof of concept, and honest about it. Generation is not byte-deterministic in general (temperature 0 helps but does not guarantee it across models); the checkpoint's inner repair loop is currently thinner than the interactive one; and the shipped stacks use in-memory or SQLite stores to stay hermetic, not production datastores. The economics are attractive but real — each quill up is a handful of model calls.

The direction is clear, though. The core conventions are already portable across three languages; adding a fourth (Go, FastAPI, Rails) is a binding.md and a scaffold, not a rewrite. And the deeper claim — that a versioned stack of specifications is the program, and code is a build output — now has a working implementation behind it.

Try it

terminal
npm install -g @fluxyr/quill
export OPENROUTER_API_KEY=sk-or-...

create-quill-app my-app --stack flask   # or node / sinatra
cd my-app
quill new "create users"
quill up

Source: github.com/fluxyr-app/quill.

Quill was built and validated in a single working session. Every transcript, codec, and number in this article is from that run.