Chris Sells

Hi, I’m Chris Sells.

Welcome to my corner of the web. I write about building software and the things I learn along the way.

I’m the CEO and Co-Founder of Gas City, Inc., the OSS steward for the Gas City ecosystem, including Beads, Gas Town, Gas City and The Wasteland.

More about me
So You’re Saying There’s A Chance: FreeCell Game #11982 October 24, 2026 AI Fun

So You’re Saying There’s A Chance: FreeCell Game #11982

FreeCell deal #11982 laid out as eight columns of cards on a dark navy background, with the eight cards that can never move outlined in gold, a dashed fence labeled C around them, and the Lean theorem 'for all s, Reachable s implies not Won s'.

I have a bit of a history with Solitaire. I built my own version for WinFX back in 2005 and was still blogging about it in 2016 (granted, that was Klondike, but it’s still solitaire). So when my friend Kevin Moore sent me an Instagram reel about the one game of Windows FreeCell that nobody has ever won, game #11982, he had my attention.

Of the original 32,000 numbered Microsoft FreeCell deals, #11982 is the only one that’s never been beaten. The bit that got me was that solver programs that try every possible line of play have never found a win.

I don’t know about you, but “never found” isn’t the same as “doesn’t exist.” So I handed the transcript to my Gas City coding agents and asked them a simple question: can you solve this remaining game?

They couldn’t. Nobody can. And a couple of days later, we had a proof of that, checked by the Lean theorem prover.

Lean is a programming language for writing mathematics in a form a computer can check. Leonardo de Moura started it at Microsoft Research in 2013, and it has some serious mathematical street cred: the Liquid Tensor Experiment used it to check a theorem of Fields Medal winner Peter Scholze. More recently, it’s the tool Anthropic used to formalize Fermat’s Last Theorem. Checking my card game seemed like a reasonable thing to ask of it.

The appeal here is that my agents can write the proof and Lean can check whether it follows from the rules we’ve supplied. An agent saying “trust me, there’s no solution” is one thing; handing me a proof I can run through a separate checker is quite another. We still have to get the rules right, and there are a few details about what we’re trusting that I’ll get to.

Here’s how we proved game #11982 can’t be won.

The Deal Is Just Arithmetic

FreeCell started life when Paul Alfille implemented it on the PLATO system. The Windows version you remember is Jim Horne’s. He wrote a DOS version, then joined Microsoft and brought it to Windows. Horne’s clever bit was making every deal a number. Type in 11982 and you get those 52 cards in those exact places in the Microsoft deal layout, because the shuffle is nothing but the random number generator from Microsoft’s C runtime, seeded with the game number:

x0=11982,xk+1=(214013xk+2531011)mod⁡231,rk=⌊xk+1216⌋x_0 = 11982, \qquad x_{k+1} = (214013\,x_k + 2531011) \bmod 2^{31}, \qquad r_k = \left\lfloor \frac{x_{k+1}}{2^{16}} \right\rfloor

The deck starts in order (A♣ A♦ A♥ A♠ 2♣ … K♠). On each of the 52 steps, the game takes the next random number, picks the card at index

jk=rkmod⁡(52−k),k=0,1,…,51,j_k = r_k \bmod (52 - k), \qquad k = 0, 1, \dots, 51,

deals it, and fills the hole with the last card in the deck. Cards go out left to right in rows of eight, so the first eight cards dealt end up at the bottom of the columns. Run that for game #1 and you get the famous J♦ 2♦ 9♥ J♣ opening. Run it for game #11982 and you get this:

The deal for game 11982 as eight columns. Column 1: A♥ 3♦ K♦ J♣ 6♣ J♦ K♣. Column 2: A♠ 3♥ 6♥ 5♦ 2♣ 7♦ 8♦. Column 3: 4♥ Q♠ 5♠ 5♣ 10♥ 8♥ 2♠. Column 4: A♣ Q♣ 4♦ 8♣ Q♥ 9♣ 3♠. Column 5: 2♦ 8♠ 9♥ 9♦ 6♦ 2♥. Column 6: 6♠ 7♥ J♥ 10♦ 10♣ Q♦. Column 7: 10♠ A♦ 9♠ K♥ 4♠ 4♣. Column 8: J♠ K♠ 3♣ 7♣ 7♠ 5♥. Eight cards are highlighted in gold as never moving: A♠, 3♥, 6♥, 4♥, Q♠, A♣, Q♣ and 6♠.

Three of the aces are at the bottom of their columns under six cards each, and the fourth is under four. Those gold cards are a spoiler for later in the post: they’re the cards that never move, no matter what you do.

First, the Obvious Thing

The agents started by writing the deal generator, checking it against game #1, and then writing an exhaustive solver in C++: a depth-first search over single-card moves that remembers every position it’s seen so it never explores one twice. It solved game #1 in about 27,000 positions. On #11982 it ran out of positions to try after 61,643. No win.

That number surprised me. I’d assumed #11982 was a long, subtle game that you lose at the very end. It’s the opposite. The whole universe of positions you can ever reach from that deal is tiny, and in every single one of them, you’ve gotten at most two cards to the foundations. Not two suits. Two cards.

Then I asked my agents to turn things up to eleven.

They found that the game is one favor away from winnable. If you could hold a single card in your hand for one move while all four free cells are full, the whole deal falls open. With a fifth free cell, it’s winnable outright. But I’d told them no cheats and no relaxed rules. Under the real rules, it was exhaustive search or nothing.

Is Exhaustive Search Even a Proof?

Yes, exhaustive search counts. Mathematicians leaned on proof by exhaustion for the four color theorem in 1976. If you can enumerate every case and check each one, you’ve proved the claim. The catch is that you have to trust the program that did the enumerating.

And trusting the program is what the history of #11982 rests on. Dave Ring’s Internet FreeCell Project got over a hundred volunteers to solve 31,999 of the 32,000 deals by hand in 1994 and 1995. On November 1, 1995, Scott McCarter at HP emailed in the results of a breadth-first search he’d written over several lunchtimes:

What finally happened?
  NO SOLUTION FOUND
  num of hands explored             73600
  most cards moved to "suit" area       2

That “2” matches the ceiling our search found 31 years later with completely different code. McCarter was also honest about it: “The program may have defects in it. I would not be surprised,” he wrote, and the archived email doesn’t include his source code. (McCarter’s email, preserved by the Wayback Machine.)

Since then, a lot of solvers have failed on #11982. The Freecell Solver FAQ lists eleven of them. But “eleven programs agree” is still evidence, not proof, and the folks who maintain Freecell Solver know it. Their to-do list says that the exhaustive solvers that traversed the deal’s game graph were never “implemented using a proof assistant which verified and validated its correctness proof,” and that “we would like to see such a solver written.”

So we tackled the #11982 part of that request. We didn’t prove a general solver correct but we did build a checkable proof that this particular deal can’t be won, and then submitted it to the nice Freecell Solver folks.

Stating the Theorem

Before Lean can check anything, you have to say precisely what “can’t be won” means. A position is the eight columns, the contents of the four free cells, and how high each foundation is:

s=(c1,…,c8;F;h),ci∈Card*,F⊆Card,|F|≤4,h∈{0,…,13}4s = (\,c_1, \dots, c_8;\ F;\ h\,), \qquad c_i \in \mathrm{Card}^{*}, \quad F \subseteq \mathrm{Card},\ |F| \le 4, \quad h \in \{0,\dots,13\}^4

The free cells are a set because they’re interchangeable: a 9♣ in the first cell is the same position as a 9♣ in the third. The rules boil down to two predicates. A card x can go on a column whose top card is p, or onto its foundation, when

stack(x,p)⇔rank(p)=rank(x)+1∧red(p)≠red(x),home(x,h)⇔hsuit(x)+1=rank(x)\mathrm{stack}(x, p) \iff \mathrm{rank}(p) = \mathrm{rank}(x) + 1 \ \wedge\ \mathrm{red}(p) \ne \mathrm{red}(x), \qquad \mathrm{home}(x, h) \iff h_{\mathrm{suit}(x)} + 1 = \mathrm{rank}(x)

Add “any card can go on an empty column” and “a card from a column can go into a free cell if one is open,” and you have moves(s), the set of positions one legal move away from s. The reachable positions are the smallest set that contains the opening deal s₀ and is closed under moves:

ℛ=⋂{X⊆𝒮|s0∈X∧∀s∈X,moves(s)⊆X}\mathcal{R} \;=\; \bigcap \left\{\, X \subseteq \mathcal{S} \;\middle|\; s_0 \in X \ \wedge\ \forall s \in X,\ \mathrm{moves}(s) \subseteq X \,\right\}

A position is won when every foundation is up to the king, and the theorem says no reachable position is won:

𝒲={s∣h(s)=(13,13,13,13)},ℛ∩𝒲=⌀\mathcal{W} = \{\, s \mid h(s) = (13, 13, 13, 13) \,\}, \qquad\qquad \mathcal{R} \cap \mathcal{W} = \varnothing

But wait, you say, real FreeCell lets you move a whole run of cards at once. It does, as long as the run has at most

k≤(f+1)⋅2ek \le (f + 1) \cdot 2^{e}

cards, where f is the number of empty free cells and e the number of empty columns available for temporary storage, excluding the destination column. That limit exists because a “supermove” is just shorthand for a sequence of single-card moves through the free cells and empty columns. It can’t reach a position that single moves can’t, so modeling single moves loses nothing. (We checked that too. Adding supermoves to the solver reached the identical 61,643 positions.)

The Trick: Don’t Verify the Search, Verify the Answer

This is the part I find elegant. Proving a 100-line search algorithm correct in Lean would be a real slog: termination, hash set invariants, the whole bit. So we don’t. Instead, the search just proposes an answer, a set C of positions, and Lean checks three easy things about it.

s0∈C,∀s∈C,moves(s)⊆C,C∩𝒲=⌀s_0 \in C, \qquad \forall s \in C,\ \mathrm{moves}(s) \subseteq C, \qquad C \cap \mathcal{W} = \varnothing

If all three hold, we’re done, because of one small lemma:

s0∈C∧(∀s∈C,moves(s)⊆C)⇒ℛ⊆Cs_0 \in C \ \wedge\ \bigl(\forall s \in C,\ \mathrm{moves}(s) \subseteq C\bigr) \;\Longrightarrow\; \mathcal{R} \subseteq C

The proof is induction on how a position got reachable. There are only two ways: it’s the deal, or it’s one move away from something reachable. Both cases land in C:

s0∈C(deal)s∈Ct∈moves(s)t∈C(step)\frac{}{\;s_0 \in C\;}\ (\text{deal}) \qquad\qquad \frac{s \in C \qquad t \in \mathrm{moves}(s)}{t \in C}\ (\text{step})

So every reachable position is in C, nothing in C is won, and therefore nothing reachable is won. Mathematicians would call C an inductive invariant. I think of it as a fence: start inside, and there’s no legal move that takes you outside.

Two diagrams side by side. On the left, a dashed gold fence labeled C encloses a graph of positions including the starting deal, with every edge staying inside and the Won position outside, captioned C intersect Won is empty. On the right, a similar fence where one red arrow escapes from a position inside to a point outside, captioned moves of s is not a subset of C.

The nice thing about a fence is that it doesn’t matter how sloppy the search was. If it missed a position, some legal move will escape the fence and the check fails. If it added junk, the junk has to be closed under moves too, and it still can’t contain a win. The search can be as buggy as it likes; it can’t make the check pass for a deal that has a win.

Walking Through the Lean

The whole thing is one Lean 4 file of about 150 lines. Here’s how the pieces fit together:

Flow diagram in three bands. The trusted spec band holds msDeal 11982, moves, and Reachable and Won. The untrusted search band holds explore, which proposes a certificate C with 84,767 positions. The checked-by-Lean band holds checkCert C equals true evaluated by native_decide, cert_sound proved by induction, and the final theorem freecell_11982_unwinnable: for all s, Reachable s implies not Won s.

The top band is the part you have to read and believe. Everything else is either checked by Lean or doesn’t matter.

The deal

Lean doesn’t take my word for the cards. It regenerates them from the game number with the same arithmetic as above:

/-- MSVC `rand()`: seed ← seed·214013 + 2531011 (mod 2³¹), output seed / 2¹⁶. -/
def lcg (seed : Nat) : Nat := (seed * 214013 + 2531011) % 2 ^ 31

def dealOrder (game : Nat) : List Card := Id.run do
  let mut deck : Array Card := (List.range 52).toArray.map fun i => ⟨i / 4 + 1, i % 4⟩
  let mut seed := game
  let mut out : Array Card := #[]
  for _ in [0:52] do
    seed := lcg seed
    let j := (seed / 2 ^ 16) % deck.size
    out := out.push deck[j]!
    deck := deck.set! j deck.back!   -- fill the hole with the last card
    deck := deck.pop
  return out.toList

def deal : State := { cols := msDeal 11982, cells := [], home := [0, 0, 0, 0] }

The file prints game #1 as a sanity check, and it starts J♦ 2♦ 9♥ J♣, just like it did on your Windows 95 box.

The rules

moves is the heart of the trusted part, so it’s written to be read: take a card off the top of a column or out of a free cell, then try every place it could go.

def canStack (c parent : Card) : Bool := parent.rank == c.rank + 1 && parent.red != c.red
def canHome (home : List Nat) (c : Card) : Bool := home.getD c.suit 0 + 1 == c.rank

def moves (s : State) : List State :=
  let fromCols : List (Card × State × Option Nat) :=
    (List.range 8).filterMap fun i =>
      match s.cols.getD i [] with
      | [] => none
      | c :: rest => some (c, { s with cols := s.cols.set i rest }, some i)
  let fromCells : List (Card × State × Option Nat) :=
    s.cells.map fun c => (c, { s with cells := s.cells.erase c }, none)
  (fromCols ++ fromCells).flatMap fun (c, s', src) =>
    -- to its foundation
    (if canHome s.home c then [{ s' with home := s'.home.set c.suit c.rank }] else []) ++
    -- onto an empty column, or onto a card one higher of the opposite colour
    ((List.range 8).filterMap fun j =>
      if src == some j then none else
      match s'.cols.getD j [] with
      | [] => some { s' with cols := s'.cols.set j [c] }
      | p :: ps => if canStack c p then some { s' with cols := s'.cols.set j (c :: p :: ps) } else none) ++
    -- from a column into a free cell
    (if src.isSome && s'.cells.length < 4 then [{ s' with cells := insertSorted c s'.cells }] else [])

Notice what’s not there: no moves off the foundations, which matches Microsoft’s rules. (We also checked what happens if you allow them: the deal still can’t be won, and at most two cards ever reach the foundations.) Reachability and winning are then stated directly, as an inductive definition that mirrors the two rules above:

inductive Reachable : State → Prop
  | deal : Reachable deal
  | step {s t : State} : Reachable s → t ∈ moves s → Reachable t

def Won (s : State) : Prop := s.home = [13, 13, 13, 13]

The check and the lemma

checkCert is the three conditions from the fence, written as a function that returns a Bool:

def checkCert (c : HashSet State) : Bool :=
  c.contains deal && c.toList.all fun s => !won s && (moves s).all c.contains

And cert_sound is the induction, which Lean’s kernel checks in full:

theorem cert_sound (c : HashSet State) (h : checkCert c = true) :
    ∀ s, Reachable s → c.contains s = true ∧ won s = false := by
  simp only [checkCert, Bool.and_eq_true, List.all_eq_true, Bool.not_eq_true'] at h
  obtain ⟨hdeal, hall⟩ := h
  have key : ∀ s, c.contains s = true → won s = false ∧ ∀ t ∈ moves s, c.contains t = true := by
    intro s hs
    have := hall s (HashSet.mem_toList.mpr (HashSet.mem_iff_contains.mpr hs))
    exact this
  intro s hr
  induction hr with
  | deal => exact ⟨hdeal, (key _ hdeal).1⟩
  | step _ ht ih =>
    have hc := (key _ ih.1).2 _ ht
    exact ⟨hc, (key _ hc).1⟩

The deal case and the step case are exactly the two inference rules from earlier.

The search and the big check

explore is an ordinary depth-first search with a hash set. Lean doesn’t care whether it’s right; it only cares about what it hands back:

def certificate : HashSet State := explore 10000000

theorem certificate_ok : checkCert certificate = true := by native_decide

theorem freecell_11982_unwinnable : ∀ s, Reachable s → ¬ Won s := by
  intro s hr hw
  have := (cert_sound certificate certificate_ok s hr).2
  simp [won, Won] at *
  simp [hw] at this

The certificate holds 84,767 positions. That’s more than the 61,643 the C++ solver saw, because the Lean version keeps the columns in fixed places instead of treating a rearrangement as the same position, and it doesn’t play cards home automatically.

What You Still Have to Trust

No proof is trust-free, so here’s the honest list. When you ask Lean what the final theorem rests on, it says:

'freecell_11982_unwinnable' depends on axioms: [propext,
 Classical.choice,
 Quot.sound,
 certificate_ok._native.native_decide.ax_1_1]

The first three are Lean’s standard axioms. The fourth is the price of speed: native_decide evaluates checkCert by compiling it and running the compiled code, rather than having the kernel grind through 84,767 positions one reduction step at a time. So you’re trusting Lean’s compiler for that one step. The induction, the theorem statement and everything connecting them are checked by the kernel.

The other thing you have to trust is the spec: msDeal, moves, Reachable and Won. If moves were missing a legal move, the proof would be a perfectly valid proof of the wrong thing. So we tested the spec from the other direction:

  • The same rules win games that are winnable. Using that exact moves, a search found wins for games #1, #617 and #1941.
  • The check isn’t a rubber stamp. Deleting the deal, its first successor or its last successor from the certificate makes checkCert return false every time.
  • The deal is the real deal. Lean’s msDeal 1 matches the published game #1, and msDeal 11982 matches the layout above.

Why It’s Stuck

A machine-checked proof tells you that the game is lost. It doesn’t tell you why, and I wanted the why. We’d already found that the black aces never get free. Across every reachable position, the Q♣ sitting on the A♣ never moves, and neither does the 6♥ two cards above the A♠. No black ace means no clubs or spades on the foundations, which means no win.

Even better, the Q♣ is uncovered in only 8 of those positions, and they all look like this:

A position with all four free cells full (3♠, 9♣, 8♣, 4♦), empty foundations, and column 4 dug down to A♣ with Q♣ exposed and highlighted. Q♥ sits on K♣ at the end of column 1. The two red kings, K♦ in column 1 and K♥ in column 7, are tinted to show they are buried.

In this one, digging down to the Q♣ meant parking the 3♠, 9♣, 8♣ and 4♦, and every card they could legally sit on is buried, so they used up all four free cells. Now the Q♣ needs a free cell, an empty column or an exposed red king, and there aren’t any. The other seven positions are variations on that squeeze. The whole deal fails by one space, which is why a fifth free cell fixes it.

I also asked for a proof a human could check without running a program, and that’s where we failed. The natural approach is to count parking spaces at the moment one of the frozen cards is about to move:

4⏟free cells+V⏟emptied columns≥P⏟parked cards+1\underbrace{4}_{\text{free cells}} \;+\; \underbrace{V}_{\text{emptied columns}} \;\ge\; \underbrace{P}_{\text{parked cards}} \;+\; 1

For this attempt, we considered candidate arrangements without requiring that a legal sequence of moves could reach them. That makes this a larger set than the reachable positions counted earlier. For each of three target cards, Q♣, Q♠ and 6♥, we considered about a million candidate arrangements. With no emptied columns (V = 0), none allowed the target card to escape. But with exactly one emptied column, counting alone allows between 24 and 136 arrangements (depending on the card) where it could escape, and ruling those out means following the actual order of moves. At that point you’re doing the exhaustive search again, just by hand. We looked for a slicker invariant that sidesteps the move order and didn’t find one.

Has Anyone Done This Before?

I couldn’t find anyone that had done this kind of formal proof for this elusive game of FreeCell. Lots of people have searched #11982 exhaustively, including McCarter in 1995, and Freecell Solver lists eleven programs that come up empty. There’s a Lean 4 FreeCell project that calls itself verified, but its theorems check the deck size and a stacking rule, rather than proving anything about a particular deal. I couldn’t find a Coq, Isabelle or Lean proof about #11982 anywhere, and Freecell Solver’s broader request for a verified solver is still on its to-do list. If you know of an earlier machine-checked proof, please let me know. I’d love to give credit where it’s due.

A reel about a 30-year-old card game turned into a few days of writing solvers, failing at a hand proof and learning more Lean than I wanted to. The answer is still the one Dave Ring’s volunteers came up with in 1995. What I hadn’t expected was that I could work with coding agents to produce a formal proof of it. They couldn’t win the game, but they could help prove that nobody can.

OpenAI, Anthropic and Google have been going on about what their models can do for mathematics. OpenAI is publishing new mathematical results with Lean formalizations, Anthropic has formalized Fermat’s Last Theorem with Claude, and Google reports that AlphaEvolve is helping solve Erdős problems and improve mathematical bounds. It’s fun to find that I can do my little part in the math world, too. Naturally, mine involves Solitaire. : )

Download the Lean proof and run lean FreeCell11982.lean with Lean 4.35 to check it yourself. Enjoy.

October 13, 2026 Colophon

Initial Launch, Take 2

In my Initial Launch post, I said that this site launched sometime in 1995 and that I had no idea when. I still don’t know the day it went up, but I found my original home page in an old project archive, and the last time I saved it was August 23, 1995.

Netscape - [The Memorial Chris Sells Home Page]_□×
FileEditViewGoBookmarksOptionsDirectoryHelp
BackForwardHomeReloadImagesOpenPrintFindStop
Location:http://www.develop.com/csells.htm
What’s New!What’s Cool!HandbookNet SearchNet Directory
Document: Done    
A reconstruction, not a screenshot: my home page in the 1996 DevelopMentor look, in Netscape Navigator on Windows 95.

It wasn’t sellsbrothers.com yet — that domain came along in October 1996. I was an instructor at DevelopMentor and my page lived on the DevelopMentor web site, a place for the links I wanted to share with my students.

Coders with Agents Fixing Websites October 12, 2026 Colophon

Coders with Agents Fixing Websites

Chris Sells holding coffee in the driver's seat of a vintage convertible while two robot agents repair the car, one under the open hood and one at a wheel beside a toolbox.

This site started in 1995 as a page of links for my students. Since then, I’ve used FrontPage, ASP.NET, SQL Server, a blogging API and Windows Live Writer to keep it going. The history of this site is the history of which tools I wanted to use with it.

In 2015 I moved to Blot because I wanted to write Markdown in whatever editor I felt like. It was a great service when I chose it and it remains so. Blot runs like the wind, and the proprietor, David Merfield, is super responsive. I was keeping my posts in a Dropbox and Blot picked up on changes to publish updates to the site. I enabled features and set styles by editing config and templates online.

However, now that I work with essentially all of the CLI coding agents via Gas City (it supports 16 as I write this), I wanted the whole website in a form that a set of agents and I could work on together. I’d prompt an agent for a change — a new feature, a template change, some CSS updates — look at the result locally and publish it when it was ready.

The result is a site built with Eleventy and hosted on Cloudflare Pages. Because David has so generously provided the Blot runtime as OSS, I was able to start my migration by self-hosting the site, checking that I got it right with pixel-by-pixel checks between the staged and the live site.

Along the way, the agents and I built our own static site generator, which made no sense to me, given the plethora of such tools already available (don’t let those agents push you around!). So I had them build the site with Hugo and with Eleventy using real pages from my blog. I picked Eleventy because it could apply Blot’s typography (small caps and hanging punctuation) as it rendered each page, so the local preview looked just like the real thing.

Once that was working and I was back in control of my site end-to-end, there were new features I wanted to build.

But first, I had three decades of tech debt to address.

I’ve been handing out links to sellsbrothers.com for thirty-one years. Some of them look like /news/showTopic.aspx?ixTopic=1153 from the ASP.NET days, and I still expect them to work (along with all of the other link formats I’d supported over the years).

I also had the agents track down the 404s. Some older posts linked to 75 places on my own site that weren’t there anymore. Most of them were links to news items on the old home page that had long since scrolled off, so now they point at the actual post. A few more needed redirects for old book and feed URLs. That fixed 39 of them. The rest point at pages that were gone long before this move, like the old conference pages under /public/conference.

Chasing down dead links also turned up images that went missing from the old site. We (the agents and I) recovered several full-size originals from the Wayback Machine and other verified sources, stored them with the content and updated the posts to use the local copies. The migration left six old image URLs without a verified original. I can’t fix the entire dead web, but I can keep a few more pieces of my corner of it together.

Finally Fixing “Here”

Before the site could show the beginning of an article as its summary, I used to write a separate little post describing it, with a link labeled “Here” (itself a fix from a previous port). The description would send you to the actual article. Once automatic summaries arrived, those extra posts were unnecessary. Finding and removing them was not a problem. Forwarding the description URL to the real post was not a problem. Moving all of the Disqus comments that had collected on the descriptions… that was a problem.

I wasn’t willing to lose those comments. We saved the Disqus exports and checked which discussions belonged to which pages before changing their associations. Where the destination had no comments of its own, we could display the original discussion there. Where both pages had comments, we used Disqus’s merge process and checked the result. Some destinations needed separate discussions kept accessible rather than folded together. The source content and migration evidence stayed backed up throughout.

The descriptions turned out to hold 731 approved comments and another 37 hidden records. All 768 were preserved, including their authors, dates, replies and moderation state.

Inline HTML Style

Before I moved to Markdown for content authoring (I’m typing these words into Typora, my favorite Markdown editor ever), I wrote content in HTML using FrontPage.

While reviewing the cleanup locally, I noticed that my 2004 post about Edward Tufte used different fonts from the rest of the site. The HTML still contained <font face="Times New Roman" size="3"> and inline 12-point sizing, including inside section headings. The template couldn’t give those headings the site’s typography while the content insisted on its own.

We cleaned up the old presentation markup in the source files, keeping the words, links, anchors and meaningful formatting. That didn’t mean stripping every style attribute. Code samples, tables, diagrams and mathematical notation can have good reasons for particular formatting. We compared the content before and after, then looked at representative pages on desktop and mobile.

This is the kind of thing that has been bothering me for a decade or more, and I never found the time or energy to track it down. But agents could do all of it in minutes.

Giving It a New Look

With the old stuff fixed, I could get to the new stuff, starting with a few design updates and a couple of new features.

For example, readers can now choose their light/dark preference in the header. 30-year-old HTML made dark appearance more interesting than changing the page background; some old posts supplied their own text and table colors, which dark mode now overrides. And I made sure it looked and worked well in both light and dark mode on mobile, too.

The homepage on a phone in dark appearance, with search on its own row and horizontally scrolling topics.
On a phone, in dark mode.

Looking through the restyled pages exposed a few things that weren’t design choices at all. Topic listings already had additional pages, but no links to get to them. Some browser tabs showed heading markup in the title, and a paragraph beside a floated image could intercept the click meant to enlarge it. We fixed those while we updated the styles.

My Own Personal Wayback Machine

After all of the work to fix all of the old posts and update the style a little, I wanted to address navigation. Scrolling through 2600+ posts stretching back to 1995 is not an ideal experience. Search helps when you remember what to ask for, but sometimes you just want to see what was going on in 2004.

So we added an archive browser. There’s a bar for every year, with the busy years easy to spot, and a button for every tag. Pick a year to see its posts, or pick a tag to follow a subject across the years. Each entry takes you to the original article. I’ve turned the site into my own personal Wayback Machine.

The Blog Archive with 2025 selected, showing a bar for every year from 1995 through 2026, a row of tag buttons and the year's posts on a timeline.
Browsing 2025 in the archive. The busy years are a little farther to the left.

The archive also has an “All posts” view, with a way back to it from any year or tag. These are ordinary links to generated pages, so browsing doesn’t depend on JavaScript. I can send you a link to 2025, and you can open that same view directly.

Putting the tags in front of me also exposed some housekeeping. Posts about Oslo had ended up under several different tags; now they’re together. We folded “books” into “book” and made the Gas City tag consistent with the others. The old tag URLs redirect to their replacements, so tidying up the navigation didn’t break somebody’s bookmark.

And you don’t even have to go to the archives to see posts from the old days. The home page now has an “On this day” section that picks one of the posts I wrote on today’s date in an earlier year:

An On this day in 2002 card on the home page, showing the post ISO close to approving C#, CLI as standards, its opening lines and a Read the full post link.
October 12, 2002, when C# was about to become a standard.

The good news is that you’ll find no postings from Christmas Day in my blog history. The bad news is that I was working pretty much every other holiday at least twice and sometimes up to 25 times (why did I post so often on Halloween?).

Books and Magazines and Podcasts, Oh My!

The blog is not the only place where I pontificate. The new Books & Magazines page used to be a plain list of book titles. Now it’s a timeline of my magazine articles (both online and off), including the ones Microsoft dropped and the Internet Archive kept.

The Books & Magazines page with a bar for every year from 1995 through 2012, filters for books, MSDN Magazine, Microsoft Systems Journal and online columns, and the 2012 entries with their magazine covers.
Every book, article and column, 1995 through 2012.

I’ve never done a good job tracking the various podcasts and video talks on the site. I enjoy the sound of my own voice, so I figured that there were a few dozen. So when the search turned up 102 results, I was skeptical. And I was right to be — the validated results listed only 101 (!). And then we found another one, so I was actually wrong. But in my heart I was right!

If you’re interested in that kind of archeology, those recordings are all on Podcasts & Videos, from calling into .NET Rocks in 2002 to talking about Gas City in 2026. Both pages have the same histogram across the top as the blog archive, plus filters, so you can zoom in on the spot in history that interests you.

The Podcasts & Videos page with a bar for every year from 2002 through 2026, filters for podcasts, videos, talks, interviews and webcasts, and the 2026 entries with their artwork.
From .NET Rocks! in 2002 to Gas City in 2026.

Finding What I Meant

Speaking of finding the parts that interest you, I needed an improved search, which never worked very well on Blot. We did the work to make sure that a static website had dynamic search. Then I searched the live site for wasm and got nothing.

I had a post about WebAssembly, but the search didn’t know that the abbreviation meant the same thing. A sweep of 32 representative queries against a local build turned up two more gaps: misspelled short queries and standalone pages we’d deliberately left out of the index, including About and Books & Magazines.

We added a small, hand-maintained list of aliases. Searching for wasm also searches for WebAssembly; dotnet includes .NET, and winforms includes Windows Forms. Results are combined, duplicates removed, and the page tells you which alternate spelling it included. The WebAssembly post now comes first:

A local search for wasm includes results for webassembly and lists WebAssembly explodes client-side programming first.
The post was there all along. Now searching for “wasm” finds it.

For misspellings, the build collects words that appear at least twice in the indexed text. When a search still finds nothing, the browser loads that vocabulary and looks for nearby spellings. A search for flutterz can offer “Did you mean flutter?” as a link, while still reporting no results for what you actually typed. Quoted queries keep their exact meaning. The old fallback could work around a bad word in a query of three or more words; this also helps with short searches.

What We Have to Work With

The content is still ordinary Markdown and HTML. Here’s the rest of it:

  • Eleventy builds the site
  • Pandoc renders the Markdown, including all of the HTML mixed into the old posts
  • Typeset does the typography I liked from Blot
  • Pagefind does the search, right in the browser
  • Disqus hosts the comments
  • Cloudflare Pages hosts the site, with a couple of small functions to handle old URLs like that showTopic.aspx one

When I push to main, GitHub Actions runs the tests, builds and verifies the site, uploads it to Cloudflare and checks the result. Blot is no longer a dependency.

Where Are We?

The bulk of this work was done over a weekend. I made more fixes and added more features in that weekend than in the previous 11 years. All on a whim.

This is the way.

Feedback? Comments are open below!

Announcing Gas City 1.0! April 28, 2026 Gas City AI

Announcing Gas City 1.0!

You may have heard from Steve Yegge, the creator of Beads and Gas Town, that we’ve released Gas City 1.0! As the CEO of the newly minted Gas City, Inc., I thought I’d provide some additional details on Steve’s excellent blog post.

Welcome to Gas City

It’s hard to look back on the release of ChatGPT as anything but the firing of a starter pistol. In that time, we’ve completely changed how software engineers do their jobs, moving from handwriting their code to using CLI coding agents like codex or claude to do the coding for them. Unfortunately the cost of the increased efficiency in producing the code is reduced quality in the output. It’s not uncommon for professional software engineers to declare the agent output to be “slop.”

In an attempt to drive up the quality of agent output, the typical agentic engineering experience has turned into a babysitting job. You cycle between your terminals, reminding each coding agent what they’re supposed to be building, injecting quality guardrails periodically as those details fall out of their context, manually asking for sub-agents to do research deep dives or reviews. All of that is if you can track where each agent is on their quality path and what the right magic phrases are, based on a year of cave drawings and campfire stories. In fact, I even know of one prolific agentic engineer who has built themselves a custom keyboard so that all of their favorite prompts are available at the touch of a button.

If you’d rather move back to engineering, I recommend Gas City.

agents + orchestration = joy

Gas City is for agentic engineers to build their own software factories.

A software factory is a system for building, validating, deploying, operating, and maintaining production software.

To enable all of this, the core of Gas City is an agentic orchestration engine. Its job is to let you harness swarms of agents to color inside the lines that you define. As an example, here’s a simple Gas City formula that iterates over code incorporating reviews from other agents:

# formulas/code-review-loop.toml
formula = "code-review-loop"
version = 2
contract = "graph.v2"

[[steps]]
title = "Code Review Loop"
id = "code-review-loop"

[steps.check]
title = "Code + Review (loop up to 3 times)"
max_attempts = 3

[steps.check.check]
mode = "exec"
title = "Check Review Results"
path = "checks/review-gate.sh" # reads reviews to decide whether to loop or not
  
[[steps.children]]
id = "code"
title = "Write code + tests"
description = "Write code and tests based on user input and agent feedback"
metadata = { "gc.run_target" = "coder" }

[[steps.children]]
id = "review-codex"
title = "Code Review (Codex)"
description = "Review the implementation for architectural best practices."
needs = ["code"]
metadata = { "gc.run_target" = "reviewer-codex" }

[[steps.children]]
id = "review-claude"
title = "Code Review (Claude)"
description = "Review the implementation for architectural best practices."
needs = ["code"]
metadata = { "gc.run_target" = "reviewer-claude" }

[[steps.children]]
id = "review-gemini"
title = "Code Review (Gemini)"
description = "Review the implementation for architectural best practices."
needs = ["code"]
metadata = { "gc.run_target" = "reviewer-gemini" }

[[steps.children]]
id = "synthesize-reviews"
title = "Synthesize review results"
description = """
  Read all three review outputs, produce one concise synthesis, and decide whether
  the implementation is approved.

  Write the synthesis to docs/reviews/latest.md.

  Close this bead with:
  - review.verdict=approved if no blocking or major findings remain
  - review.verdict=iterate if another coding pass is required
  - gc.outcome=pass
"""
needs = ["review-codex", "review-claude", "review-gemini"]
metadata = { "gc.run_target" = "synthesizer" }

Five agents are involved, one coder, three reviewers and one review synthesizer.

The reason to pull in multiple agents from multiple vendors is that each one has been trained differently and has a different point of view. It’s the combination of those points of view, as well as multiple iterations, that drives up the quality of your agentic output.

The agent definitions are separate for reuse amongst formulas, but they each look like this:

# agents/coder/agent.toml
scope = "rig"
wake_mode = "fresh"
provider = "codex"
option_defaults = { model = "gpt-5.5" }
default_sling_formula = "code-review-loop"

Each agent also has a prompt template file:

{{/* agents/coder/prompt.template.md */}}
# Coder
## Role
You implement code changes. Read the request, any prior review feedback,
and the project context, then write the implementation with tests and commit it.

## Inputs
- The current routed formula step.
- Project context: AGENTS.md, specs/ and docs/
- Prior review feedback in docs/reviews/ (if retrying after a failed review cycle).

## Graph Work Process
1. Run `gc prime`.
2. Read the routed step and project files.
3. If docs/reviews/ contains prior feedback, read it and address findings.
4. Implement the change with tests.
5. Commit to a feature branch.
6. Close the current step.

## Output Format
A git commit on a feature branch with passing tests.

## Close Behavior                                       
Close the current step when the commit is made and tests pass.

Notice that the definition only has to mention “codex” and not set up a path or CLI args or output formats. That’s because Gas City has a built-in “factory worker” protocol to standardize access to your favorite CLI coding agents, including codex, claude, and gemini, of course, but also amp, opencode, pi, etc. That means that you can change your coder agent to be another OpenAI model or another CLI coding agent altogether, all with a simple set of declarative settings and without changing your formulas. And the settings also support MCP servers, skills, overlays, and the full set of capabilities that you need for your agents, all in an agent-neutral way.

Formulas — step-by-step workflows for as many agents from as many providers as you like — are the key to moving from babysitting your agents to harnessing them. To be able to handle our record peak of 74 PRs in a single day, Gas City runs on Gas City. For a more in-depth look at the power of agentic orchestration, I recommend Orchestration Is All You Need.

get involved!

Harnessing agents with formulas is the core of Gas City, but there’s more, including beads (the way work is defined), orders (trigger-based automations), packs (shareable sets of agents, formulas and orders), a rich event stream, and the list goes on!

To help you get up to speed on all of this tech, I recommend the Gas City docs, made explicitly friendly for you AND your agents. Also, there’s a vibrant and growing community of people experimenting with Gas City and working together to invent the future of software engineering, with 2600+ members in the Gas Town Hall Discord server. From this community, the core projects — Beads, Gas Town, and Gas City in the Gas Town Hall organization on GitHub — have gathered more than 35K stars, with more than 5,000 PRs and 2,500 issues from 500+ contributors.

But that’s not all, the popularity of Gas City has grown enough that Actual AI has started a set of hands-on workshops called The Software Factory Intensive, with five cities already planned. In fact, last week was the first one and we had more than 70 engineers building their own software factories with Gas City.

swi-seattle-group

Things are moving fast: from Steve’s initial commit on Beads in October to hands-on workshops across multiple cities running right now.

Interested in joining us? Come on over to Gas City Hall — we’re happy to have you!

where are we?

You can build your own custom agent keyboard if you want to.

coding-agent-keyboard

If you’d like to take the leap to agentic engineering instead, I recommend Gas City.

Orchestration Is All You Need April 21, 2026 Gas City AI

Orchestration Is All You Need

What agentic engineering looks like when you really mean it.

In the last year, as our AI software engineering tools have evolved, so have our software engineering processes. We have learned to take advantage of the tools to cut down the latency of our SDLC and CI/CD processes. We’ve already learned that if we’re not careful, we’re going to vibe code AI “slop,” the unchecked code and configuration that an AI coding agent generates when run without guardrails.

The kind of guardrails I mean come straight out of the software engineering 101 playbook:

  • Getting feedback from multiple points of view
  • Checking technical designs for completeness, consistency and accuracy
  • Comprehensive unit, UI, integration, performance and security test suites
  • Thorough error instrumentation and run-time monitoring in operation
  • Human taste and judgment as the definition of “done”

Using agents for what they’re good at (coding, checking, testing, iterating) while letting the humans set the bar for quality elevates your process from “it works on my machine” and defines the “agentic engineering” discipline. You can use agents interactively to get the quality you’re looking for, if you’re careful to apply the guardrails. But if you want to build automated pipelines that require less and less human interaction over time, you’re going to have to start with something called harness engineering.

“Harness engineering” is the act of gathering together the underlying LLM with the tools and context (data, instructions, information about the environment) into a program optimized for a specific task. Each one of the CLI coding agents itself is a harness that’s optimized for getting the most out of the underlying LLM from a latency, throughput and quality point of view, packaged in a TUI with programmatic access built in.

And while it’s easy enough for a human to switch between tools from an interactive point of view, it’s real work to be able to switch between codex and opencode in your automated processes. While each of them provides the programmatical surface area you need to automate them, all agents are different in their invocation arguments, service endpoints, etc. To solve that problem, we need harness engineering again, but one level of abstraction up.

As an example, the Gas City OSS project defines a provider protocol that we implement on top of each of the major (and several of the long tail) CLI coding agents to enable them to be swapped in and out of city, pack and formula definitions (more on that later):

// Pseudo-code for the canonical in-memory worker API.
type FactoryWorker interface {
    // Lifecycle
    Start(context.Context) error
    StartResolved(context.Context, string, runtime.Config) error
    Attach(context.Context) error
    Create(context.Context, CreateMode) (sessionpkg.Info, error)
    Reset(context.Context) error
    Stop(context.Context) error
    Kill(context.Context) error
    Close(context.Context) error
    Rename(context.Context, string) error
    State(context.Context) (State, error)

    // Messaging
    Message(context.Context, MessageRequest) (MessageResult, error)
    Interrupt(context.Context, InterruptRequest) error
    Nudge(context.Context, NudgeRequest) (NudgeResult, error)

    // Transcript and History
    History(context.Context, HistoryRequest) (*HistorySnapshot, error)
    Transcript(context.Context, TranscriptRequest) (*TranscriptResult, error)
    TranscriptPath(context.Context) (string, error)
    AgentMappings(context.Context) ([]AgentMapping, error)
    AgentTranscript(context.Context, string) (*AgentTranscriptResult, error)

    // ... and more!
}

Not only does this allow you to eliminate the programmatic differences that get in the way of your swapping yesterday’s best coding agent out for today’s best agent out of your automations, but it also allows you to easily take advantage of the variations between the models, e.g. cost, limits, quality, availability, suitability to a specific job (e.g. planning, coding, image processing).

The use of the provider harness in Gas City allows you to turn a CLI coding agent into a “factory worker” that you can tailor to the specific job (“you’re an expert UX designer…”) and drop into your custom workflows (“write the plan, then get it reviewed by these other agents, then ask the UX designer agent to flesh out the jobs to be done, …”) that we call orchestrations.

orchestration all the way down

Reminding ourselves of human software engineering teams again, we have a series of workflows that human teams go through to get a product or feature out the door. For example, imagine an engineer that’s just written some code. Do they immediately release it to customers? No. They must solicit review feedback from their teammates first, dealing with any issues that arise. Only when they get the thumbs up are they allowed to merge new code into the main product codebase.

Now, if Eddie the Engineer is a human, he can do his coding, solicit feedback and iterate all on his own. Likewise, his teammates will get their notifications and do their part to keep the flow of software moving.

However, if Eddie is an AI coding agent, to do his part requires that he’s “orchestrated,” e.g. that he’s been defined as an expert software engineer running on the codex provider using the GPT5.4 model. Once an agent is included in an orchestration, we can have as many instances of it as we want, each dedicated to execution on its own work item with its own agent Eddie instance.

Once each Eddie is done with his work, the orchestration engine will hand it off to as many LLMs as you decide to review the work, gathering that feedback to iterate on until Eddie’s code gets a clean bill of health.

This isn’t theoretical. The code review orchestration we use with Gas City combines a dynamic set of agents configured to focus on security, performance, React, etc. that are then spread across the codex, gemini and claude providers so that we get a wide range of review feedback to iterate on. We know from experience that this greatly improves the quality of our code (and plans and tests and …).

Every part of our human software engineering processes can be turned into an agentic orchestration. The artifact-review-iterate process is the same for engineers, PMs, UX designers, etc. And those processes string together into the overall software process itself.

The factory worker harnesses allow you to bring the CLI coding agents into your orchestrations, but it’s the orchestrations that allow you to spread the work across as many agents as you can spin up.

embracing the bitter lesson

There’s a tempting alternative to all of this: just wait for the models to get good enough that you don’t need orchestrations. Eventually, the thinking goes, GPT-7 or Claude 6 or Gemini Ultra will be capable enough that you can hand it your repo and your customer feedback and walk away.

This is the wrong side of the bitter lesson. Rich Sutton’s observation was that general methods that leverage compute have always eventually beaten methods that leverage human-encoded domain knowledge. Waiting for the perfect model is turning away from that lesson. It’s a bet that compute will eventually stop mattering, that there’s a model capability threshold past which the apparatus becomes unnecessary. And while the singularity is by definition the horizon beyond which we cannot predict the future, the past is pretty clear about the benefits of being on the right side of that lesson.

A software factory is on the right side. Orchestrations are how you turn more compute into better output, indefinitely. Five agents from different providers providing feedback is how you broaden your range of feedback no matter how good any specific agent’s response is. A better model just makes the orchestration better.

And now let’s talk about how you might build the software factory to run your orchestrations.

Gas City: build your software factory

Like Steve Yegge’s Gas Town that inspired it, Gas City provides the harness that turns CLI coding agents from different vendors into factory workers that can be plugged into “formulas” (orchestrations) to execute “beads” (work) against multiple “rigs” (projects).

Another name for an agentic orchestration system is a “software factory” or “dark factory” (so named because you don’t need humans to run it). Gas City provides the pieces — the agentic harness, the orchestration engine, the automation engine — and lets you configure the software factory to match the way that you want to work.

Want to start from scratch to build your factory alongside your software? You can. Want to have a fully featured Gas Town out of the box? Gas City provides that as a pre-packaged “pack” that you can choose at initialization time. Want to define your own pack that defines your favorite combo of agents, formulas (orchestrations) and orders (automatically triggered scripts or formulas) and share it with a team? That’s exactly what packs are for.

In fact, we’ve engineered Gas City such that any orchestration system you can think of — Ralph Loops, StrongDM Software Factories, Anthropic Agent Teams — can be captured in a pack. We’ve already started to gather some of them in the gascity-packs repo with more to come.

Those packs come from our own use of Gas City to build Gas City, as you might expect. On a single day, we were able to merge 74 PRs. Of course, the only way we can handle that kind of volume is by using Gas City itself.

But what surprised us wasn’t just that we are using Gas City for implementation tasks. We’ve also built issue triage, deployment and operations. We’ve hired 15 agentic employees to help us run Gas City, Inc, ranging from marketing to release management. And, leaning on the techniques in OpenClaw, each of them has a personality and the ability to chat with us via Discord.

What we’re finding is that not only does Gas City evolve based on bug reports and feature requests, but the Gas City software factory itself evolves to meet the needs of the software. The more we push on the limits of what orchestrated agents can do, the more we realize that we’re just getting started!

getting involved

The Gas Town ecosystem has grown quite a bit in popularity since Steve Yegge submitted the first commit to the Beads project in October. Together, Beads, Gas Town and Gas City have gathered more than 35,000 stars, 5000 PRs and 2500 issues from more than 500 contributors.

In addition, we’ve got The Wasteland, which is our burgeoning set of federated Gas Towns and Cities for centralized management of distributed work. And we’ve got a hosted version of Gas City coming soon.

Of course, we’ve also got Gas City Hall, the community for all things in Gaslandia, including the Discord server, the social news feed, the docs and a link to the repo. Or perhaps you’d like some hands-on training? Then sign up for Software Factory Intensive, a two-day workshop from our partners at Actual.ai on how to build your very own software factory with Gas City.

where are we

It might not have sunk in yet, but your job has changed. Instead of building the software, you’re going to be building the factory that builds the software. And reviews it. And validates it. And deploys and operates and maintains it.

But the agents still need you to define what “done” looks like. Encode those definitions into reusable orchestrations, spinning up as many as you need, and let the factory carry the work your team used to carry by hand.

Start with Gas City. Plug in the Gas Town pack or build your own from scratch. The ecosystem is open source, the community is growing, and we’re using it ourselves every day to build the thing you’re about to install.

Go build your factory.

February 8, 2026

We Pour — Chapter 3

A Cyberpunk Story in the Fifth Element Universe

by Chris & Eli

The street hits me like a wall. Steam and noise and the churning river of foot traffic that parts slightly around me the way it always does, because people see 6’8” coming and step aside without thinking about it. I walk. I don’t flag a cab. I need the movement.

Mox.

His real name is Maxwell Huang and he was four foot nothing until he was sixteen and then he was five foot four and that was it, that was all he was ever going to get. On the commune, the two of us were a sight — the beanpole and the bean, mom called us. Stuck together because nobody else would have either of us.

The other kids threw rocks at the cows and called it fun. Mox built a radio out of combine parts and pulled in signals from orbit. I sat next to him in the barn loft and listened to cargo ship chatter from the outer stations and he’d say “Dunk, there’s a whole world up there that doesn’t give a shit how tall either of us are” and I believed him because Mox never lied to me.

I stop walking. I’m in the middle of the sky bridge between Sectors 7 and 9, a hundred floors up, wind pulling at my jacket. Below me the city falls away into blue haze and blinking lights. A police cruiser drifts past at eye level, close enough to see the officer’s coffee cup on the dash.

I know what Mox would say. He’d say it was just a job. He’d say he didn’t know it was Ellie. He’d say that David came to him cold, through a cutout, and he never asked who the target was because you don’t ask, that’s the rule, that’s how it works in this business.

And maybe that’s even true.

But he took the job. And he’s good enough that he would have seen the name in the access logs. Ellie Covacs. Same last name as the best friend he’s had since he was nine years old. Same girl who he never quite was able to outgrow.

And he did the job anyway.

My phone buzzes. It’s Mox.

[mox] hey. noodles later? im buying. got something to tell you

I stare at the screen. The wind picks up. A cab honks somewhere below.

yeah man lets do noodles later

Later was as soon as I could do it. I needed time to think. I took another turn and then a turn and then I was back at my place, sitting on a dusty old couch of suspicious origins and looking out into the world as the traffic went by. As soon as I sat, I automatically pulled up my phone.

[mox] sure thing big man. the job today ends 5ish. usual spot also ur cats here again

I type back.

keep him

The couch smells like the couch has always smelled, which is like whoever owned it before me and that no amount of enzyme cleaner will ever fully kill. The ceiling is close enough that I could touch it if I reached for it. I don’t reach. I just lie there and watch the traffic paint light across the plaster. Red. White. Blue. Red. White.

The cat’s with Mox. Mox is three floors down and two units over and we’ve been in each other’s lives for twenty-three years and he might have…

Wait.

I pull up the phone. Thumb past Mox, past Ellie’s ALL-CAPS flare, past the junk, and there it is again. The job ping. Routed through three anonymizers, which means whoever sent it knows enough to not want to be found but also knows enough to find me, which narrows the field more than they’d probably like.

No name. No company. No brief. Just:

[unknown]
RETRIEVAL -- DIGITAL ASSET
TIMELINE: COB FRIDAY
COMPENSATION: 15,000 CR
ACCEPT / DECLINE / QUERY

Fifteen thousand credits. That’s five months of rent. That’s Ellie’s lawyer if things go bad. That’s breathing room I haven’t had since the last corp job dried up in November.

Close of business on Friday. Same as Ellie’s deadline. Stated the same way.

My thumb hovers over QUERY. The part of my brain that keeps me alive — the pattern-matching, paranoid, hacker part — is already whispering. Anonymous job, big money, fast timeline, lands in my inbox the same morning my sister tells me she’s being squeezed by a guy who hired my best friend to build the frame.

Some people believe in coincidences. I believe in patterns.

The traffic light cycles. Red. White. Blue.

Eventually I pressed the QUERY button.

The screen refreshes. Whoever’s on the other end was waiting.

[unknown]
ASSET: Personnel file. Encrypted.
LOCATION: Zorg Industries internal network
DIVISION: Applied Human Resources

TARGET FILE: ZI-AHR-00891

Retrieve and deliver. File must be intact
and uncopied. Original extraction only.
You keep nothing.

Dead drop coordinates on acceptance.

ACCEPT / DECLINE / QUERY

Zorg Industries.

I sit up on the couch. The springs groan.

Zorg is not a noodle shop. Zorg is the third largest defense contractor on Earth, supplier of weapons systems to half the governments in the Federated Territories and most of the ones that aren’t. Their network security isn’t a wall — it’s an ecosystem. Adaptive ICE, behavioral analytics, honeypot architectures so deep that slicers have gone in for a soda pop and come out hours later to find federal agents sitting in their apartment.

I know this because two years ago I did a white hat audit for a Zorg subsidiary. They paid for it. Third-tier, nothing classified, but I saw enough of the outer architecture to know the inner layers would be a different animal entirely. I still have my notes from that job somewhere on my terminal.

“Applied Human Resources.” I’ve never heard of that division. HR is HR. You don’t put “Applied” in front unless it’s doing something that isn’t human resources.

And the stipulation — original extraction, no copies, keep nothing. That’s not a corporate espionage play. If
someone wanted to steal Zorg secrets they’d want to leave behind the original to cover their tracks. This is someone who wants a file to disappear. Or someone who wants to know what’s in their own file and can’t get to it through normal channels.

Fifteen thousand credits. COB tomorrow. And a target that would take most slicers a month to plan.

But most slicers didn’t spend three months inside Zorg’s outer ring with a badge and a smile.

My thumb hovers.

The light through the slats shifted from gray to gold to amber to the deep bruised purple that passes for sunset when you’re ninety-seven floors up and the smog catches it just right. None of it registers.

The job ping is still unanswered. Three tabs running — Zorg subsidiary architecture from my
old audit notes, a Sector 12 corporate registry search that’s half-loaded, and Ellie’s doctored records that I’ve stared at for so long the numbers have stopped meaning anything. I drop my phone and rub both fists into my eyes, trying to push back the headache forming at the base of my skull.

My brain is hot swap thrashing. Ellie. Mox. Zorg. Fifteen thousand. Friday. Twenty-four hours. Mox’s hands built those forgeries.

Then, Mox’s notification buzzing on the cushion next to me.

[mox] dunk?? its 520 u coming or what? i ordered for u. the good stuff not cheapo

5:20. Shit.

I’m off the couch and out the door and I haven’t decided anything. The elevator drops and my stomach drops with it and the whole way down I’m rehashing conversations that haven’t happened yet. I’ll look at him across the table. He’ll be Mox — same grin, same fast hands pulling apart chopsticks, same guy who built a radio in a barn. And I’ll know what he did and he won’t know I know and I’ll have to eat
noodles and act like everything is…

He said he had something to tell me.

February 6, 2026

We Pour — Chapter 2

A Cyberpunk Story in the Fifth Element Universe

by Chris & Eli

The express elevator drops 97 floors in eleven seconds. My ears pop at 40. The lobby is its usual zoo — a Mangalore in a maintenance uniform arguing with the building super, two kids chasing each other through the turnstiles, and a police drone hovering by the mailboxes, scanning packages with a blue fan

of light.

Nobody looks twice at me. Everybody looks once.

Outside, I flag a cab. The driver — a wiry guy with a headset bolted to his skull and a dozen dashboard saints glued to the console — tilts his mirror to take all of me in.

“Where to, long man?”

“Sector 7. Yung Fat Noodle, street level.”

“Ah, the noodle girl! She your lady?”

“She’s my sister.”

“Even better. Family! I give you fast route.”

He yanks the stick and the cab lurches into the fourth lane, cutting off a freight hauler that screams past close enough to rattle my fillings. Through the windshield, New New York stacks itself up and out forever — towers threaded with sky bridges, holographic billboards the size of city blocks, sunlight hitting the upper levels while the streets below stay in permanent blue dusk.

The cab drops hard through three lanes and pulls up at street level. Down here the light is neon and steam. Ellie’s shop — YUNG FAT NOODLE, hand-painted sign, been here since before I was born — sits between a vape den and a Zorg-brand electronics outlet with a cracked window display.

The lunch rush hasn’t started. Through the glass I can see Ellie wiping down the counter, alone. She moves the way she always moves — efficient, contained, like everything is a task and tasks get done. But there’s something in her shoulders today. A tightness.

She sees me through the window and stops wiping.

The pay pad in the cab reads my thumb and the number blinks at me. Not enough to flinch at. Enough to notice. The anonymous ping flickers in the back of my skull like a pilot light.

“You have blessed day, long man!” The cab lurches skyward and is gone.

I stand on the sidewalk for a second. Steam curls up from a grate and wraps around my knees. Down here at street level the air tastes like grease and ozone, and for just a moment — the neon, the wet pavement — it hits me sideways.

I see rain on the long dirt road up to the house. The combine coughing black smoke while Dad swore at it and Mom handed him the wrong wrench on purpose just to hear him say “Margaret, that is NOT a nine-sixteenths.” Chickens in the yard. Ellie at the dinner table doing homework with her tongue poking out the side of her mouth, same face she makes now when she’s reconciling the register.

The commune kids thought I was a freak. Thirteen years old and already looking down at every adult on the property. But Ellie — five foot two, all elbows and opinions — would walk next to me like a bodyguard escorting a building. Daring anyone to say something. Nobody ever did.

I push open the door. The little bell rings.

Ellie looks up. She’s got a rag in one hand and a spray bottle in the other and she’s wearing the same Yung Fat apron she’s worn for six years and I think the thing I always think and never say to her: you should own this place. You should own five of these places. You’re smarter than everyone you work for.

But she took the waitress gig when I needed someone to co-sign for the apartment. And she never left. Because leaving would mean not being twenty minutes away when I go dark for a week and stop answering my phone.

“Sit down,” she says. Not mean. Just — the way you say it when you’ve been practicing what comes next.

She pours me a coffee without asking. Sets it down. Sits across from me in the booth by the window, where the neon from the vape place next door turns everything a faint pink.

She’s not wiping anything. She’s not moving. Ellie always moves.

“Dunk, I’m in trouble.”

My hands go flat on the table. I don’t realize I’ve done it until I feel the cold laminate under my palms. Something in my chest pulls tight like a cable taking load.

“What kind of trouble.”

It comes out flat. Not a question. She hears the difference. She’s heard it before — the voice I get when the part of me that sits in front of terminals and cracks jokes with Mox steps aside and the other part comes out. The part that once put a Mangalore debt collector through a drywall partition just for showing up at her apartment.

Ellie wraps both hands around her own mug. Looks into it.

“You know Mr. Yung’s son. David.”

“The one with the teeth.”

“The one with the teeth. He’s been running the books for the last two years. His dad’s too old, and David…” She trails off. Starts again. “David’s been skimming. Not a little. A lot. But that’s not — that’s his problem, not mine. Except three weeks ago he came to me and said he needed to move some money through the shop’s account. Digital transfers, routed offshore. He said it was to cover what he took before

the auditors caught it.”

She looks up at me.

“I said no. Obviously. And then last Tuesday he told me if I didn’t help him, he’d tell the Feds I was the one

skimming. He’s got records, Dunk. Altered ones. He was so proud that he left me a copy. Time-stamped with my access codes. I don’t know how he…”

Her voice catches. Just for a second. She sets her jaw and pushes through it the way she’s pushed through everything since the commune foreclosed and Mom got sick and Dad just stopped.

“He says I have until Friday before close of business.”

Today is Thursday.

The coffee sits untouched in front of me. The neon pinks and pulses. Somewhere in the kitchen a timer goes off and nobody’s back there to answer it.

I lean back in the booth and the vinyl creaks under me as I think.

David Yung. I run what I’ve got.

The teeth — capped, porcelain, expensive. Way too expensive for a guy running the books at his dad’s noodle shop. I noticed that two years ago and filed it in the back drawer where I keep things that aren’t my business yet.

I’ve seen him maybe a dozen times. Comes in late, always on his phone, never eats here. Drives a Zorg Stratos — the sport model, not the fleet one. Again: too much money. I clocked the plates once out of habit. Registered to a leasing company in Sector 12. Corporate shell. I remember thinking that was a weird way to lease a car unless you didn’t want your name on things.

And there was that one night. Six, maybe eight months back. I was picking up takeout and David was in the back booth with two guys I didn’t recognize. Suits, but not corp suits — the kind that fit too well. Private sector. Security or money, one of the two. David saw me looking and smiled with all those perfect teeth and I thought: that’s the smile of a man who wants you to know he’s got friends in high places.

Mox. Mox would know more. Mox knows things about everyone in Sector 7 because Mox never leaves Sector 7 and never stops listening. And Mox owes me for the firewall job last month.

So I’ve got threads. The car. The shell company. The suits. And a guy who somehow got Ellie’s access codes, which means he’s either got a slicer on payroll or he’s poking around in systems he shouldn’t be able to reach — and either way, that’s my turf, not his.

Bessie would be faster. Bessie lives under my bunk and has never failed to make a point. But Ellie’s looking at me right now and what she needs isn’t a big man with a bat. She needs the other thing I do.

“Ellie. I need you to tell me everything about how you log into the shop’s systems. Every code, every terminal, every time you’ve ever let David touch your workstation. And I need the name of whoever does the shop’s network.”

She blinks. Then — just barely — the shoulders drop half an inch.

“You’re going to fix this.”

“I’m going to fix this.”

Ellie slides her phone across the table and I scroll through the doctored records. Whoever built these didn’t just change numbers. They rebuilt the access logs from the inside out — spoofed timestamps, cloned Ellie’s credential hash, even salted the metadata so a surface audit would read clean. This isn’t some script kiddie with a purchased toolkit. This is craft.

And that’s what’s bugging me.

Every slicer has a style. The way a locksmith can look at a picked lock and tell you what brand of tension wrench was used. I’m staring at the way the timestamp spoofs are layered — three-pass overwrites, each one seeded off the previous hash, cascading so the whole chain validates if you pull any single link. It’s elegant. It’s paranoid. And it’s backwards.

Most slicers build forward. This person builds from the endpoint and works back to the origin. I’ve seen exactly one person do it that way.

My stomach drops about six inches.

No.

I scroll back. Look at the credential clone. The way it mirrors Ellie’s key signature — it doesn’t just copy

the hash, it reconstructs the generation pattern so it would pass a deep audit. There’s a little flourish in the

reconstruction. A redundant verification loop that serves no functional purpose. It’s a signature the way a painter leaves a brushstroke. It’s there because he can’t help himself.

Mox.

My best friend. The guy who never leaves Sector 7. The guy who owes me for the firewall job. The guy who knows things about everyone because he never stops listening.

The guy who apparently knows David Yung well enough to do a five-figure forgery job for him.

Ellie’s watching my face.

“Dunk? What is it?”

The neon pulses pink. The kitchen timer is still going off. I get up to go and am halfway to the door before turning back. Ellie looks at me with worried eyes.

I walk back to put my hand on Ellie’s shoulder. She reaches up and squeezes my fingers once, hard, the way she used to when I was small and the thunder got bad. Somehow her touch means that I’m the one who feels steadied.

“It’s going to be fine. I know exactly what to do.”

She nods. She believes me. That’s the worst part.

February 4, 2026

We Pour — Chapter 1

A Cyberpunk Story in the Fifth Element Universe

by Chris & Eli

The alarm doesn’t wake me. The cat does.

Not my cat. I don’t have a cat. But the orange tabby that lives two vents over has figured out my ductwork again, and it’s sitting on my chest like it owns the lease. Which, given what I pay for a 400-square-foot box on the 97th floor of Tower Block Chinatown, it might as well.

I peel myself out of the fold-down bunk — a process that involves actual origami when you’re 6’8” — and stand in the blue-gray light filtering through the slatted window. Outside, the morning traffic is already stacking up. Hover cabs and freight drones jostle for lane position, their running lights smearing across the glass like wet paint.

The terminal on my desk is blinking. Two messages.

One’s from my sister. The subject line just says “CALL ME.” All caps. No punctuation. That’s her version of a flare gun.

The other’s a job ping, routed through three anonymizers. No name. No brief. Just a figure.

It’s a very good figure.

The cat meows. The coffee maker gurgles to life on its timer, and for about four seconds the apartment smells like something other than duct metal and old takeout.

I thumb the call back to my sister. She picks up before the first ring finishes.

“Shit, Ellie…”

“Don’t ‘Shit, Ellie’ me, Duncan. Where have you been? I called you four times yesterday. Four. And don’t say you were working because I pinged Mox and he said you’ve been quote ‘doing absolutely nothing of value for like a week’ unquote, which sounds right because…”

“Ellie.”

“…because you always go dark when you’re between jobs and I know what that means, it means you’re eating noodle packets and staring at your ceiling and telling yourself you’re ‘thinking’ when really you’re just…”

She stops. Takes a breath. When she comes back her voice is different. Quieter. The real voice under the big-sister artillery barrage.

“I need a favor. A real one. Not a tech one.”

The cat jumps onto my desk and sits directly on the terminal, covering the anonymous job ping with its ass.

“Can you come to the shop today? Before lunch. It’s… I don’t want to do this over the phone, okay? Just come.”

Something in the way she says it. Ellie doesn’t ask. Ellie tells. Ellie has been telling me what to do since I was four years old and she was six and a half. The fact that she’s asking sits wrong in my chest, like a rib out of place.

“Dunk? You there?”

“Shit, Ellie… Yeah. I’ll be right there.”

She hangs up without saying goodbye. That’s normal. Ellie treats phone calls like airlock cycles — get in, get out, don’t waste the oxygen.

I pull on yesterday’s jacket. It fits the way all my jackets fit — about two inches short in the sleeves. I grab the coffee on the way out and duck through the doorframe, a motion so automatic my body does it without consulting my brain. The cat watches me go with the deep indifference of someone that is allowing you to come and go as you please. For now.


Older Entries →