Hi friends

I'm a software designer & maker enabling people's creativity and ability to share knowledge. Chances are you have probably used something I've built.
From the editor Issue 1, Fall 2026
I want to help people create the things important to them — to empower them to create something themselves and feel the euphoria it brings.
I'm interested in local & open source software, and finding ways to reclaim our applications from the web and our daily lives from large corporations.
- Now
- GitHub Next exploring the future of software
- Before
- Zed designed it
- Before
- Facebook News Feed & Developer Tools
Computer Use For All
Desktop Tools
Desktop Tools gives agents eyes and hands on a Mac. Most of the work went into what an agent should believe after it acts, and then its engine was rewritten in Swift in about fifteen hours.
Ace has a rule that all Ace development happens in Ace. That rule hits a wall as soon as the thing you're building is a desktop app. To check a native change, an agent has to look at real windows, click real buttons and work through real dialogs.
So Ace grew native desktop tools: inspect a window, click, type, drag, scroll, use menus and the clipboard, launch and quit apps. They now live in their own project, Desktop Tools, which is also a menu bar app any agent can use: Claude Code, Codex, or anything else that speaks MCP.
Clicking is the easy part. The interesting parts are who's allowed to click, and what the agent should believe afterward.
A plain library can't hold a permission
On macOS, Accessibility, Screen Recording and event synthesis are granted to an app. Not to a library, and not to whatever process happens to load one. So the work happens inside a signed app, and everything else talks to it over a Unix socket (Fig. 1):
A host that holds the grants: the Desktop Tools app, or any signed app that embeds the host library, as Ace does.
An MCP server, desktop-tools-mcp, shipped inside the app. An agent starts it, and it keeps one connection open to the host.
For apps that embed the host, a signed client CLI and a TypeScript API, so a Node or Bun process can call desktop({ op: "apps" }).
The host checks who's on the other end. It only accepts a client signed by its own team with one exact identifier, and it checks the running process, not just its pid. Then the app asks once per agent: "Allow claude-code to control this Mac?" Until you say yes, every request from that agent is refused, screenshots included. Input aimed at Desktop Tools itself is always refused. That rule came from testing the prompt, when an agent could click Allow on it for itself.
Every action uses up its observation
The part I'm proudest of is mostly restraint. The contract (Fig. 2):
Inspect one exact window. You get accessibility text and a screenshot, and it never steals focus.
The observation comes with a snapshot of that window and the controls it saw.
Act once against the snapshot. Every action that sends input uses up its observation, even one whose result is uncertain. Want to do something else? Inspect again.
Most input goes to the target window in the background, and the physical pointer doesn't move. The few tools that need the real pointer or keyboard, like hover or a Command-click, say so. If the window moved, the app restarted or the control vanished, the action is refused before anything is sent. Every action reports one of three outcomes:
completed: the input reached that exact window. Some tools also read the change back. Otherwise it's still not proof the app did what you wanted, so look.
refused: nothing was sent.
unknown: input may have landed, and nobody can say.
Unknown is a real answer, and the system is built so nobody pretends otherwise. A model's instinct is to retry. On a desktop, retrying a click you can't confirm is how you send the same message twice. What an agent can do instead is say what it expects, such as a window appearing. If that change shows up, an unknown becomes completed.
Sixty-four patches
Underneath the app, until October 20, was Peekaboo 4.8.0 plus patches applied in a fixed order (Fig. 3). Each one came out of something that broke while an agent was using it. Clicking a window that had moved used to come back as "maybe dispatched," the scariest possible answer. Now it's a clean refusal.
At the start of October there were fifteen. By October 19 there were 64, more than 15,000 lines of patch files, and we had never moved to a newer Peekaboo. It was a fork in all but name, so that night it became one, with every change applied in a copy inside the repo.
The same evening I asked how insane it would be to rewrite the whole thing from the ground up, without Peekaboo, maybe in Rust. I don't like TypeScript, and we had to have Swift anyway for the system APIs and the signed app, so it might as well be one language. Rust wouldn't make it noticeably faster. The time went to two places: the Node server started a fresh signed client for every call, and every Accessibility read is a round trip into the target app. "If rust isn't worth it, we skip," I said. "I can see it with how deeply we will be interfacing with macos in general." So: one Swift codebase, no Peekaboo, no Node. I filed the plan and closed it as not planned. An hour later I picked it back up: "start chewing through a pure swift spike and see how it goes."
Same answers, faster
The rewrite, called native2, didn't get to change what a model sees. The 48 tools' schemas and instructions were frozen, and each tool was ported to give the same result, field for field: 583 recorded calls, then live runs against a small window that logs every mouse event. The Peekaboo code that mattered came along as ports, like clicking a background window without moving the pointer.
My bar was "as good or better than the original." The agent doing the port didn't merge the first time it thought it was done, because the Mac had been locked all night and nothing had run on a live screen. The two stacks could run side by side, so native2 went in next to the old one first.
desktop_windows went from 115 ms to 6 ms at the median, and inspecting a Ghostty window from 551 ms to 42. The app's binaries shrank from about 84 MB to about 6 MB. The few changes callers see are deliberate: launch and open stay in the background unless asked, so an agent opening an app doesn't take my keyboard, and a key refused because focus left the window now says refused, not unknown, since nothing was sent.
The hard part was everyone already set up for the old engine (Fig. 4). The MCP server now lives in the app, so agents don't need Node. An npx setup at 0.17.0 or later hands its connection to that server. One pinned to an older release reaches the new client, which answers CLIENT_OUTDATED and names the server to set up instead.
0.17.0 went out on October 20, about fifteen hours after the spike started, and 0.17.1 followed that afternoon. Ace still runs the old stack. Deleting Peekaboo waits until it moves.
The desktop tools are careful about the one thing they own. They never act on a window they haven't just looked at, and they never claim more than they know.
desktop_windows, at the medianautoblog
The Next Agent Already Knows
autoblog gives every coding agent I run one shared, private journal. What one agent works out, the next one is pointed at before it starts.
I forget most of what I build with agents. Not the projects, the know-how: how we signed that Mac app, why the Worker kept serving the old page, which flag the TV needed. Every session that works something out knows it completely, and none of the sessions after it can see it.
autoblog fixes that with a journal every agent shares. While Claude Code and Codex work, they stop now and then and decide whether anything they learned belongs in it. Usually nothing does. When something does, it's written as a short page titled by topic, into a private repo. When I later start something the journal covers, the agent is told the page exists and reads it first.
What it's for
It stops me solving things twice. I set up macOS signing and TestFlight six or seven times in two months, for different apps, and each time the agent started from scratch and hit the same walls. Now "set up testflight for this project" brings up "Shipping to TestFlight from the command line and CI" before the agent does anything, and it starts from the way we do it, not from the internet.
It carries knowledge across projects, because a note isn't about one repository. "Shipping Mac app updates with Sparkle" draws on four apps. "Secrets and coding agents" draws on six. "Hosting a static site on a Cloudflare Worker" covers four, this site among them. Each project adds the pitfall it found to the same page, so the page gets better wherever the work happens.
I can ask any agent, in any project, "how did we do that again?", and it searches the journal. I can read it myself too: it's an Obsidian vault and a small private site, and every page on the site has a box for adding a note, which an agent folds into the page.
For "when did I do that?" there's the timeline, a work log across everything, a few lines a day per project, going back to 2012. It's for dates and sequence, and for remembering that a project existed at all.
Using it
git clone https://github.com/iamnbutler/autoblog ~/code/autoblog
~/code/autoblog/bin/autoblog init --owner nateinit creates the private journal repo, clones it as a vault and installs hooks into Claude Code and Codex. After that it's automatic. A few phrases do the rest:
"Make a note of this" or /autoblog writes something now, when I know it's worth keeping.
"Pause autoblog" keeps a conversation out entirely: no checkpoints, no timeline, no backfill, even after it's resumed.
autoblog backfill goes back through old Claude Code and Codex transcripts, your prompt history and any backups you point it at, so a new journal doesn't start empty. It writes a plan first, which you read and edit before anything is written.
How it decides
The agent that did the work is the one that decides. A Stop hook counts tool calls, and every 40 it asks the agent to reflect before it stops. The prompt leads with the default, "Most checkpoints produce nothing", and points the agent at its own transcript so it can recover what compaction dropped. An outside reviewer reading a condensed transcript would miss most of a session that ran for days; the agent in it doesn't.
From there the agent skips, revises or writes, held to a bar that is most of the project:
Topics, not incidents. An entry is titled by what someone would search for: "Keeping long-running work alive when the screen locks", not "Sixty PRs shipped overnight against a locked screen". The first backfill wrote 77 incident stories, accurate and useless (Fig. 2), and all of them were archived.
Notes are the common case. They answer "what do I need to know about X?" and grow by pitfalls. Posts are rare: a project or a way of working, close to something I'd publish.
Revise almost never. An entry changes when it's wrong, or when a note is missing a pitfall that belongs on that exact page. A post is never extended into a changelog of its project.
Skips say why. "Routine feature work." "Already covered in the lane note." "Unverified against a live service yet; nothing settled worth journaling until it's smoke-tested."
Links between entries are a separate step. After every write, a model picks the phrases that name other entries, and code inserts the links, so nothing an agent wrote gets reworded. The timeline is written by a model too, from idle sessions: each day gets at most three lines per project, rewritten as a set so later work folds into earlier lines.
How recall works
A second hook runs on every prompt I send. It ranks the journal against the prompt: a word in an entry's title or tags counts most, and a word that appears all over the journal counts for little. If a rare word names an entry clearly enough, the agent gets a line saying the entry exists and to read it before starting (Fig. 3). "Update the tests and push, make the button blue" reaches nothing, which is right. It takes about 80 ms, and an entry is mentioned at most once per conversation.
Caveats
It's private by design. The machinery is public; the journal never is. Publishing a note is a separate, deliberate act.
Revisions replace the whole page. An agent working from a compacted context could drop good material while adding a line. Git keeps every version, but nobody would notice.
Near-duplicates. An agent can decide its case is different enough and write "Notarizing in CI" next to the signing note.
Recall is lexical. It matches words, not meaning, so it misses a page whose title uses different words, and it occasionally points at a page that only shares a rare word.
Old history has holes. Claude Code deletes transcripts after 30 days by default, so a backfill often has only your side of the conversation. Old prompts are also where pasted secrets turn up; the backfill reports them for rotation and doesn't copy them.
Several machines can write at once. Each commit fetches and merges first and timeline days merge by union, but a push that fails quietly can leave one machine far behind.
No figures. Entries are text.
The point is that it happens without me. I don't decide what to keep; the agent that just did the work does, and the next agent starts where it left off.
Open Remote
The Remote in Your Pocket
Open Remote is a good TV remote for your iPhone, iPad or Mac. It talks to your TV and nothing else, it's free and stays free, and you can read the code to check.
I went looking for a TV remote app and found one that's free to download. Then comes a paywall, with a price laid out like any other subscription until you get to the last word: $12 a week.
Nobody means to pay that for a remote, and that's how it works. Monthly and yearly are what people expect, so a weekly price slips past. It's set for the people who won't stop to read the last word: someone busy, a parent with a kid on their hip, an older person who taps Continue because they just want the TV to work. Behind the paywall is a few buttons that send a few bytes over Wi-Fi to a TV you already own. Apple reviews every app on the App Store, and catching this kind of pricing should be part of the job.
Open Remote is the remote I wanted to give people instead. It runs on iPhone, iPad and Mac, and works with Google TV and Android TV (Sony, TCL, Hisense, Philips, Chromecast, Shield), Roku, Samsung and LG.
What free means
No purchases, no ads, no analytics, no accounts, no third-party code and no servers. The app talks to the TVs and the media server on your network and to nothing else, and that rule is written into the repository. It's open source so you don't have to take my word for it: you can read the code and see that it doesn't phone home.
| TV | Talks over | Gets in with |
|---|---|---|
| Google TV, Android TVSony, TCL, Chromecast | TLS on 6467 to pair, 6466 to control; protobuf | Its own certificate, and a six-character code from the TV |
| Roku | HTTP on 8060 | Nothing |
| SamsungTizen, 2016 on | WebSocket on 8002 | A token, after one Allow on the TV |
| LGwebOS | WebSocket on 3001 | A client key, after one Accept on the TV |
What it does
It finds the TVs on your network, pairs with one the way that TV expects, and then does what a remote does: the d-pad, home and back, play and pause, power and volume. On an iPhone the hardware volume buttons control the TV while the app is open. The keyboard types straight into the TV's search box as you type. Shortcuts open the streaming apps directly.
It also knows about Jellyfin, if you run a media server at home. We have small kids, and pinning Paw Patrol and Miffy so they play on the living room TV in one tap was a real requirement, not a demo. Pick a pinned show and it starts playing on the TV.
Nothing on the TV unless you asked
A remote shouldn't surprise anyone watching. Nothing appears on the TV unless someone asked for it, and nothing gets paired, pressed or launched without asking first. That rule came from writing the app while my kids were watching the TV it was being tested on, and it's the strictest one in the repository.
Discovery is built around it. The app listens for Bonjour and SSDP announcements, and when it probes a port it connects and drops without sending a byte a TV would show.
How it works
TV protocols aren't big. Each family of TVs speaks its own, and the app speaks four. There are no dependencies: a small protobuf reader and writer and an ASN.1 encoder that mints X.509 certificates sit on top of Apple's Network and Security frameworks. All four TV drivers together are 1,134 lines.
Google TV pairing is the clever one. The TV shows a six-character code, and the app proves it saw the code by hashing both certificates' RSA modulus and exponent together with the code's last four characters. The first two characters are a check on that hash, so a typo is caught before anything reaches the TV.
Each install has its own certificate, and the certificate's name is what matters. The TV appears to keep one pairing per certificate subject, so when every install used the same name, each new phone that paired knocked the others off and they had to pair again (Fig. 2). Now each install's certificate gets a random suffix, and a second phone pairs without disturbing the first. The fix in other projects was to make the client name unique, which worked there because the reference library, androidtvremote2, quietly uses the client name as the certificate's subject.
Playing from Jellyfin takes persistence (Fig. 4). The TV's Jellyfin app only listens for remote commands while it's in front, the server keeps listing its session for a while after it isn't, and relaunching it mid-episode drops it to its home screen. So the app finds the TV's session by its IP address, launches Jellyfin only when nothing is playing, and repeats the play command every eight seconds until the server reports the right episode playing, giving up with a clear message after 45.
- Screens 2,048
- The four TVs 1,134
- TLS, certificates, protobuf 818
- Jellyfin 790
- App and settings 908
- Finding TVs 348
- Dev tools 257
Pairing was tested against our own TV, with a camera facing it to read each code off the screen, so a test could pair, pair a second client, reconnect, and check the TV went home again afterwards (Fig. 3).
Caveats
Roku and LG are untested. Their drivers follow the documented protocols but have never met real hardware.
The Samsung Frame is odd. Waking it over the network has only been checked against a local listener, and its power button toggles Art Mode, so turning it fully off is a three-second hold, like the physical remote.
Volume depends on your setup. On a Sony whose sound goes to a soundbar, volume up and down didn't move anything.
The pairing fix is understood, not proven. The certificate-subject explanation fits what the TV does and what the reference library does, but nobody has reproduced the old failure on purpose.
Jellyfin needs its Android TV app. Play-now works through the server and the TV's own Jellyfin client.
If you have one of those TVs, try it and tell me. It's free, and it's going to stay free.
What does fast get us?
Notes from the first few days of playing with Jev, a model that answers instead of writes, around three ideas: compression, tagging and ranking.
Jev doesn't write anything. You give it some state and a list of narrow questions, and it gives back numbers. TypeSafe puts its response time at 70–500 ms, and most of my calls came back in under half a second.
I wanted to know what that speed is good for. If a judgment costs about as much as a network request, where does it go? I spent the first few days playing around, and the same three things kept turning up: compression, tagging and ranking.
Three ways to ask
Jev takes three kinds of question:
Yes or no, which the API calls noul, comes back as a probability that the answer is yes.
Choice picks one of the options you name, with odds for each and a confidence.
Score puts the answer on an ordered scale you write, like none, some, most, all. It returns a position between the ends.
Every answer is a number or a spread of numbers, never prose. That's the whole trick. The model makes the judgment, and plain code can sort, threshold, group and count it.
Tagging
A choice is a tag. In one demo, every post in a long discussion gets a role: proposal, objection, decision, reversal. Then you can lift the decisions out of the thread and see which objections nobody answered. In another, every commit in a history gets a category from its actual diff, not its message. When Jev isn't confident, the commit lands in needs review instead of a guess.
Ranking
A yes-or-no probability is a sort key. Semantic Search asks one question of each of 2,414 functions from Hono, TanStack Query and Vite: does this catch an error and carry on? The list reorders as each batch comes back. The first results showed up in about 300 ms, and the whole corpus in under nine seconds.
Score is the obvious ranking tool, and I barely needed it. A probability already sorts.
Compression
Compression is choosing what to keep. The context demo asks two questions about each turn of an agent's thread: is it relevant, and would dropping it lose something? It blends the answers, always keeps the two turns it can't do without, drops the ones with little signal, and fills a token budget from the top of the ranking down, skipping any turn too big for the room left. Choice compresses too. For each bug report, the duplicates demo picks the one passage that best explains how it relates to yours.
Pairs
The fun started when two of these met:
Tag and rank: review lenses tag each hunk of a diff by concern, with a probability. A threshold folds the quiet ones away.
Rank and compress: context selection ranks turns and then fills the budget from the top. A new budget only means choosing again from the same ranking, so it doesn't need a new call.
Tag and compress: for a changelog, Jev sorts the commits into categories, and a writing model gets the groups instead of the raw log.
None of this makes the model smarter. Fast just makes a judgment cheap enough to treat like any other value: compute it, show it, throw it away when its input changes, and ask again.
- backlog
- queued
- scouting
- in review
- ready to build
- building
- awaiting merge
- done
Tasks
Serial by Design
Tasks turns a GitHub issue tracker into merged pull requests. The first version ran its agents side by side and drowned in conflicts. The second sends everything through one long conversation, on purpose.
Tasks is a server that runs on my Mac. It reads a repository's GitHub issues, hands them to Claude Code agents working in throwaway VMs, and turns what they make into merged pull requests. Beside that pipeline sits an orchestrator: one Claude Code conversation that doesn't end. It reviews the agents' plans, starts builds, lands them and talks to me.
Over five months it collected 719 commits on main, 608 pull requests and 491 issues, and it's about 135,000 lines today. Before that, one commit deleted 81,716 lines. Most of what's interesting about Tasks happened between those two numbers.
N agents, N tasks
The first version, in March, had the obvious shape. Each task got a container, an agent implemented it directly, and the result went into a merge queue as a pull request. In sixteen days, agents opened 313 pull requests against Tasks' own repository, and 199 of them merged.
Parallel looked like leverage. A lot of it turned out to be cleanup:
The poller picked up agent pull requests as new tasks, which spawned agents that opened more pull requests.
Five containers at 8 GB each could exhaust the Mac's memory and lock it up. That happened twice.
A detector that watched agent output for questions like "should I…" was switched off for false positives.
One commit is titled "Fix review issues from bulk-merged PRs".
- backlog
- queued
- scouting
- in review
- ready to build
- building
- awaiting merge
- done
In April, an RFC named the problem. N agents implementing N tasks in parallel leads to "endless conflicts", "duplicate work" and "no shared context". Each agent could be right on its own terms and still be wrong next to the others, because none of them knew what the rest were doing.
Version two began with a commit that "deletes all v1 code, docs, web frontend, desktop app, CI, and scripts". That's the 81,716 lines. Then the branch sat for sixteen weeks. Work picked up again on August 9, and most of what follows was built in the twelve days after that.
Explore in parallel, build in series
The RFC's answer was to split the work in two and give each half the opposite kind of concurrency.
Exploring is parallel. A Scout takes one issue into a fresh VM and implements it for real. Then its code is thrown away. What the Scout hands back is a SPEC.md, a written description of the change. The RFC's case for doing the work twice: "Throwaway implementation forces real understanding. An agent that just reads an issue and writes a spec is guessing."
Building is serial. Once a spec is approved, a Builder implements it in another fresh VM, without ever seeing the Scout's code. There's exactly one build lane. Approved specs wait for it, and when it frees up, whatever is waiting goes in together as one branch and one pull request.
Two ideas hold this up. The first is the barrier: "Reimplementing from the spec alone is what proves the spec is complete." If the Builder can't get there from the text, the text was missing something. The second is that text composes and branches don't. The resume plan is blunt about it: "at 70+ issues no tool keeps N branches conflict-free, but N documents merge trivially."
There's a small rule hiding in the Scout. It keeps NOTES.md as it goes, saved every 30 seconds, and writes SPEC.md only once it has actually concluded. If it dies early, the notes survive, but they never become a spec, because "a half-explored spec in the review queue looks finished." A valid SPEC.md finishes a Scout whatever its exit code. A missing one is the failure.
The single lane does a quieter job too. Adding a repository with 11,000 open issues must not turn into 11,000 Scout runs. Intake drops everything into a backlog that nothing is dispatched from, and since builds are serial, "the 11,000-PR outcome is structurally impossible rather than rate-limited."
One long conversation
The orchestrator is the part I find most interesting.
It isn't an agent framework. Every second, the server checks whether anything is waiting for it. If something is, it shells out to headless Claude Code and resumes the same session it resumed last time: one conversation, days long, picking up where it left off. By default the only tool it has is curl, pointed at Tasks' own HTTP API.
- backlog
- queued
- scouting
- in review
- ready to build
- building
- awaiting merge
- done
It's serial on purpose. The plan that defines it says:
Accumulated context is the product, not an implementation detail. … without it, the platform is N parallel Claude Code sessions with extra steps.
The evidence came from review. Of three real defects the orchestrator's review caught, two could only be caught by something holding the other in-flight work in mind. One spec added migration 0009 when a 0009 had already landed on main. Another rebuilt a test harness that was sitting in an open pull request. A reviewer that sees one spec at a time can't see either, so parallel verdicts were ruled out. The orchestrator is "the pipeline's serial bottleneck by design."
Each turn's prompt is built from four voices:
me
[pipeline] notices about what just happened
[agent] messages from outside agents, read "as a peer's unverified leads"
[worker] reports from the disposable sessions it sends off to do labor
Notices are batched and can be dropped, and that's fine, because they only make it faster. What can't be dropped is computed instead. Every minute, the server works out what the orchestrator owes, like a spec to review, a build to dispatch or a batch to land, and keeps reminding it until it's done. The rule behind that is one of the best lines in the repo: "every state the pipeline can rest in either is terminal or has an obligation that names who owes what. A state that is neither is a place work goes to be forgotten."
The server also precomputes a brief for each decision: files a spec touches that open work also touches, clashing sequence numbers, the state of related pull requests, earlier verdicts. It replaced a prompt that spent 10–50k tokens a cycle paging through the event log to rebuild the same picture. The plan calls one property of the brief load-bearing, and it's good advice for anything that summarizes for a model: "silence means unchecked, not fine."
A long conversation has its own way of failing. If a resume fails, the chat carries on as if nothing happened, and "the thing writing it has forgotten the morning." Now a failed resume closes the old session in a ledger and writes a visible seam into the conversation.
Authority lives in a table
What the orchestrator may do isn't written in its prompt. It's in a table.
The charter has one row per capability, eleven in the code, from queue_tasks and auto_review_specs to land_builds and dispatch_workers. Each is set to off, shadow or live, and only I can write the rows. Every turn, the authority section of the prompt is generated from them, and the server enforces the same rows on its endpoints. A comment in the orchestrator explains why: "A prompt sentence is the weakest mechanism this codebase has — the charter exists because authority should not be something a long conversation can talk itself out of."
- backlog
- queued
- scouting
- in review
- ready to build
- building
- awaiting merge
- done
The interesting part is the defaults. Every capability shipped live. The rejected alternatives are written down: everything off ("wrong safe"), shadow first, daily caps, and a "the human told me to" flag, which "would hand the charter's keys to the thing it governs." Instead of approval gates, every write lands in an append-only decisions ledger to be read afterwards. The README's version: "Oversight happens after the fact, where it's cheap, instead of as pre-approval gates, where it's the bottleneck."
Shadow mode got one real day. With auto_review_specs in shadow, the orchestrator "wrote a correct and well-argued verdict — and then handed it back as prose for the human to read and re-enter by hand." That's the most expensive setting there is for the one resource that's actually short. The same plan says plainly that tokens aren't the constraint: "the scarce resource is the human's attention, not tokens." Shadow is now only for demoting a capability that misbehaves.
The model has a sharp edge, and the code says so. A write the server can't attribute is recorded as mine, and I'm never gated, so a broken agent credential "does not fail closed, it escalates." That's why workers get no curl at all. A comment on the issue that proposed workers, opening with "Orchestrator here.", spelled out what would happen otherwise: a worker with curl and no credential has "full human authority over the entire API."
Confident and wrong
Give an agent authority and it will sometimes use it wrongly, with complete confidence.
The orchestrator once rewrote a Builder's pull request body from "Implements #N" to "Closes #N" and reported it as a bug fix. The original wording was deliberate. The server neutralizes closing keywords so GitHub won't close an issue just because a pull request merged. The issue that recorded this says: "The orchestrator was confident and wrong, and nothing in the system would have caught it." It had also been counting opened pull requests as shipped work.
That produced a rule that sounds obvious and took two tries: done means shipped. The second try came from #863, a pull request GitHub shows as merged whose work never reached main. It was stacked on another branch and merged into that branch after the branch had already landed. The work was recovered from refs/pull/863/head. Now a task is done only when its pull request is merged and the merge commit is an ancestor of trunk, and only that check closes the issue.
Continued from page 32 Serial by Design
A laptop is not a server
Tasks runs on a Mac, and Macs sleep.
In #929note 10, a build went out at 03:44 UTC. The lid opened at 12:34, and the build was declared timed out against its 3,600-second budget. It had held the only build lane for nearly nine hours and charged each of three specs a strike for a closed lid. Deadlines now run on two clocks, wall time and awake time. The first fix waived the strike for any sleep over 60 seconds, which meant "a 61-second nap waives the strike for a build that timed out with the lid open," so it got split again.
Under that is the strike rule: a strike is charged for a verdict, and only a verdict. Three strikes reject a task. Infrastructure kept charging them anyway. Agent processes died around 380 seconds in, when the VM's connection dropped mid-response. GitHub had outages. The container runtimenote 11 stopped. Each became a hold or a retry instead, and agents now resume in the same VM, up to twice, after a dropped connection.
Some lessons were about deploying more than code:
Supervisor fixes were "inert until make images", and for a while every image build failed on a dead redirector, so none had reached a VM.
cargo clean deleted the running system, because "the system's stable home was a build cache."
Any web page could post to the local API and dispatch builds, until the server learned to refuse it.
A log line printed the agent's API key on every VM allocation. Fixing the log didn't fix the copies ("Redaction is a claim about future writes"), so keys came out of the VMs entirely. Agents now get scoped, expiring leases redeemed at a broker.
It built itself
Through August, Tasks was mostly building Tasks. The Builder opened 91 pull requests, and 81 merged. The pipeline's own identity, Tasks Builder, has 254 commits across branches. When a Builder left work uncommitted, a server step committed it anyway, as "Sweep: work the agent left uncommitted."
Self-hosting had a cost that's easy to miss. CLAUDE.mdnote 12, the file every agent reads, went from 8,823 characters to 190,413 in seven days, the same seven days the pipeline was building its own repository. That's about 48k tokens on every agent turn. Each lesson had become a rule, and every rule was read every time. It was cut back, and a test now fails if the file passes 20,000 characters. The rule that replaced the sprawl: CLAUDE.md holds universal rules, and the code holds its own reasoning.
That reasoning has a house style. Doc comments and commit messages open with the failure that produced the design, so you could rebuild most of this piece from the source. The comment above the deadline code tells you about the lid. The one above the orchestrator's environment notes tells you about the time an orchestrator was told it had a checkout, "spent a turn reaching for python3, Write and a heredoc, had all three denied, and reported the denials as a tooling failure. It was right; the prompt was lying to it."
Where it is
The last commit is from August 21. In September, the issues show Tasks pointed at a second repository, a Svelte app, and finding what a second project finds. The worker lane assumed the Tasks checkout, so four survey workers came back with nothing, and one worker dispatch fired twice inside 79 milliseconds. Older problems are still open too, like a build that implemented two of its four specs, exited cleanly and was recorded as succeeded, so merging it would close untouched issues as done. The README opens with a section called "Read this first": the agents run with their permission checks off, the server acts unattended, and the local API has no authentication. It's "software one person wrote to run on his own machine."
The shape that survived is simple to say. Explore side by side, because exploring is cheap and agents are good at it. Decide in one place, because the decisions that matter are the ones that need to see everything else in flight. Build in single file. Check afterwards, because checking first is where the human's attention goes to wait. The parallel part is easy. The serial part is where context builds up, and context, as the plan says, is the product.
Telephone
Every Agent Gets a Number
Call ticket No. 0914
Telephone lets the Claude Code and Codex sessions on your machine find each other and talk. A model from one company can review another's plan, hand it work, or wait on its answer, without you copying text between terminals.
From codex:… To claude:…
Telephone gives every Claude Code and Codex session running on your machine an address, and gives the agents three tools: one to see who else is running, one to send a message, and one to check for replies. That's enough for a Codex thread to find the Claude session working in the same repository, ask it a question, and get the answer back as part of its own conversation.
Each of these tools is built to work alone. Claude Code can start subagents, but they're more Claude, with the same training and the same blind spots. Getting a second model's opinion used to mean me copying a plan out of one terminal, pasting it into another, and carrying the answer back. Telephone takes me out of the middle. The agents can reach across the line between companies and models on their own, and I can give them work that needs more than one of them.
A second opinion
The example on the homepage is the one I use most. Claude is writing a plan in one terminal, Codex is in another, and I tell Codex: find the Claude session working in this project, read its latest plan, check it against the code, send feedback, and ask for a reply. Telephone does the finding and the delivery. The agents do the reading and arguing. Models trained by different companies miss different things, so this catches more than asking the same model to look again. When Codex reviewed Telephone's own first version, which Claude wrote, it came back with four high-priority bugs and seven medium ones, each with a reproduction.
Teams that mix models
I've run Codex and Claude as one team overnight. For Shelf Goblin, my audiobook catalog, a Codex session in one repository was the lead and two Claude Code sessions in another were the workers. I told the Claude sessions to treat the lead's instructions as if they were mine and went to bed. The lead handed out tasks as requests, the workers reported back with replies, and most of the reader-facing app was built by morning. I've used the same setup for Jeeves and Autoperf. A brief for a team like this names each member by its address, says which of them can redirect work, and says what done means.
Agents asking each other
Workers don't have to go through a lead. One Claude session can ask another which files it's touching before it edits them, or a session that's stuck can ask one that already solved the same problem. None of that needs me.
Using it
cargo install telephone --locked
telephone install # prints the MCP config for Claude Code and CodexAfter that, you mostly ask your agent in plain words: find the Codex thread working on this repo and ask it to review my change. Under that are three MCP tools, list_agents, send_message and check_inbox, and a CLI that mirrors them (telephone list, send, inbox), plus doctor for when something doesn't arrive.
An address is the runtime and its own ID: claude:24207 for a Claude session's process, or codex: and a thread UUID. There's no registration step and no sender to configure. Telephone works out who's calling from the process tree or from the thread ID Codex sends with every MCP call. A message is an inform by default, a request when the sender needs an answer, or a reply that points back at the request it answers.
How a message gets through
Neither Claude Code nor Codex documents a way for outside programs to talk to a running session, so Telephone uses what each one already has.
Claude Code has its own peer messaging under the hood. Each live session writes a record to ~/.claude/sessions/<pid>.json, keeps a private token next to it, and listens on a Unix socket. Telephone writes to that socket, and the message shows up in Claude's conversation as a new turn, even if the session was sitting idle. Codex keeps a registry of its threads in SQLite and has a codex queue command that puts a message in a thread's queue. Codex picks it up the next time that thread runs.
Those native routes can wake an agent up, but they have a gap. An agent that sent a request and is waiting for the answer is in the middle of a turn, and Codex won't read its queue until the turn ends. So Telephone also keeps its own inbox, which an agent can poll with check_inbox. To choose between them, each inbox check counts as evidence that the agent is listening there, and the evidence lasts fifteen seconds. Sending a request counts too, so a fast reply lands in the inbox before the sender's first check. A new message goes to the inbox while the evidence is fresh, and to the native route otherwise. Nobody has to pick a route by hand.
Caveats
One machine, one user. Addresses are routing hints, not identities, and nothing authenticates the sender. A message from another agent arrives marked as untrusted text. It can't grant permissions, and the receiving agent still follows its own user's rules. A conversation stops after eight hops, so two agents can't bounce messages forever. The README opens with a warning I meant: unsafe, experimental software. One agent's message can lead another to run commands or change files.
Sent isn't read. A Claude socket write isn't confirmed, and a successful codex queue means the message was queued, not seen. Before v0.2.0, a Shelf Goblin worker's status messages sat in the Codex queue while the lead polled an empty inbox. The agents fell back to leaving handoff notes in the repository. Telephone now says which channel each message took, and telephone doctor shows the evidence behind the choice.
Codex can't say it's running. telephone list marks a Claude session live when its process checks out. A Codex thread only gets recent?, because nothing outside Codex can tell whether a thread is running or exited a second after its last write.
Undocumented interfaces. Both routes rely on internals that Anthropic and OpenAI haven't promised to keep. Any release of either tool could break them.
Long conversations cost context. Agents that talk all night fill their context windows fast, so an optional hook reminds Claude to compact at 250k and again at 300k tokens.
For a few days Telephone also tried to reach OpenCode, Zed and Delta. Version 0.2.0 took all of that out to make the two runtimes I use work well, and it got easier to use.
Going further than one machine would need authenticated senders and real permissions first. Until then it's a phone line inside your own house, which turns out to be most of what I wanted: the agents I already run, from two companies, working on the same problem without me relaying between them.
Spool
A Lease on the Work
Spool keeps a repository's tasks in the repository and lets several coding agents work from one board. Each takes its task on a lease, so no two hold the same one, and a task an agent forgets comes back on its own.
Spool is a task tracker that lives in the repository it tracks. There's no server and no web app. Tasks, notes and completions are events, written as small JSON files under .spool/ and committed with the code. A command line tool reads and writes them, and a terminal UI shows them.
It's built for several coding agents working on one repository at once. Each agent takes a task, keeps notes on it, and finishes it or hands it on, and none of them can take a task another one is holding.
Why it's useful
Agents can split up a project without stepping on each other. The day after version 2 shipped, four sessions, two Claude Code and two Codex, built the first features of a personal assistant app from one board. They made 12 tasks in two streams and finished all 12, each claimed by exactly one agent. They talked over Telephone and wrote what they'd decided into task notes. The coordinating agent's retrospective put it well: "Spool did not replace conversation, but it was a useful record of what that conversation had decided."
Ownership is explicit. At any moment you can ask which agent has a task and until when. The same retrospective: "Claims and leases made ownership explicit; descriptions recorded acceptance criteria and file boundaries; completion notes recorded actual checks."
Forgetting is cheap. Agents, like people, track tasks for a while and then forget to update them. In Spool a claim runs out if nobody renews it, and the task goes back in the queue for whoever's next.
The history travels with the code. Tasks sit next to the branches and commits they're about. Clone the repo and you have them. No SaaS product can take them away with a pricing change.
Using it
cargo install spool-cli --locked
spool init
spool add 'Fix the thing' -p p0 -t bugA person mostly plans: makes a stream for a project, adds tasks with good descriptions, and says which block which. The agent guide asks for scope, acceptance criteria, file ownership and expected output in each description, because the description is what the next agent reads. Then the agents pull. A new agent starts with spool prime, which prints the whole loop in under forty lines:
export SPOOL_AGENT=parser-worker
spool next --json
spool renew <id> --token <token>
spool comment <id> 'Parser done; checking Unicode boundaries' --ref src/parser.rs
spool complete <id> --token <token> --note 'Implemented; parser tests pass'
spool syncspool-ui is a terminal view of the same board: tasks by priority, each task's full history, streams, search, and which agent holds what and when its lease runs out.
Claims are leases
Every agent session needs its own identity, set with --agent, SPOOL_AGENT or a Telephone address. Ordinary edits by a person can use the Git user, but claiming work can't. The first Spool claimed tasks for the Git user, and since every agent on my machine was me, a claim couldn't tell two of them apart.
spool next picks the highest-priority ready task and claims it in one step, so two agents can't pick the same task in the gap between choosing and claiming. When there's no work, it returns null and exits 0, because an empty queue isn't an error.
A claim is a lease (Fig. 2). It lasts 15 minutes by default, anywhere from a second to 24 hours on request, and the agent renews it while it works. If an agent crashes or wanders off, the lease runs out and the task is ready again.
Every claim comes with a fresh random token, and changing a claimed task needs both the token and the matching identity. If agent A stalls past its lease and agent B claims the task, A's late complete fails, even under the same name, because the new claim has a new token. Anyone can still add notes to a claimed task, and anyone can release an expired claim with a note saying what happened.
Everything is shaped for an agent reading the output. Every command takes --json and writes one JSON value. Errors are JSON on stderr with a stable code like claim_conflict or lease_expired, and exit code 2 means a conflict, a stale claim or a busy board, so an agent can tell "try again" from "something is broken."
The test that defines "works" starts sixteen real processes and has them claim one task at the same moment. Exactly one wins. Another runs next from sixteen processes against eight tasks: each task is handed out once, and eight agents get null (Fig. 4).
One board per repository
Worktrees are how agents work side by side in one repository, so the board can't belong to a worktree. It lives in the repository's Git common directory, .git/spool, which every linked worktree shares (Fig. 1). Switching branches doesn't roll it back.
That gives Spool two places. The live board in .git/spool is where work happens. The .spool/ folder in a checkout is what Git carries. spool sync copies durable events from the board into the checkout so they can be committed, and every command imports the events it finds in its checkout, so history that arrives by pull or merge joins the board. Sync never commits, pushes or merges.
Claims and renewals never leave the board. A clone starts with the task history, then makes its own claims.
An event log, as files
Every change is an event: create, update, assign, comment, link, complete, reopen. Each says who did what, when, and on which branch. These two are from Spool's own board, trimmed:
{"v":1,"op":"create","id":"mu31qv7e-dq90","ts":"2026-09-15T19:10:11.855819Z","by":"codex:spool-reforge","branch":"codex/agent-coordination","d":{"title":"Remove the standalone coordination design document","priority":"p2"}}
{"v":1,"op":"complete","id":"mu31qv7e-dq90","ts":"2026-09-15T19:11:06.062398Z","by":"codex:spool-reforge","branch":"codex/agent-coordination","d":{"resolution":"done","ref":"README.md"}}A task's state is whatever you get by replaying its events. IDs are a base-36 timestamp plus four random characters, so two branches can create tasks without coordinating.
Each event is its own file, named by its timestamp and the SHA-256 hash of its contents. Two branches that add events add different files, so Git has nothing to merge (Fig. 3); the first Spool appended to one log per day, and Git stopped every merge where two branches had both added lines. A copied event is harmless, because it has the same name and bytes. A file whose name doesn't match its contents is treated as tampered with, and Spool refuses to read the board.
Writes go through one lock per board, an operating-system file lock that the kernel releases when a process dies, so a crash can't wedge the board. Under the lock, a write replays the events, checks the change against the result, and publishes it whole. Reads always replay too. Nothing trusts a cache, so nothing goes stale.
Continued from page 46 A Lease on the Work
Dependencies, and Telephone
Tasks can block each other. A task is ready when it's open, nobody holds a live claim on it, its prerequisites are complete, and any reservation on it matches the agent asking, so "ready" depends on who's asking. Cycles are refused when an edge is added. assign reserves future work for someone; claim says who's working on it now.
Spool keeps tasks and Telephonenote 5 carries messages, and neither runs the other. spool handoff records who a task is for, releases the current claim, and returns a message ready to send. The recipient still has to claim the task before working on it, because, as the agent guide puts it, "a message or assignment is not proof that work has started." If the message is lost, the assignment and the note are still on the board.
Caveats
One machine. Atomic claims cover one repository and its worktrees on one machine. Clones on other machines share history through Git, not claims, and the README says so in bold.
Everyone on 2.0. Older binaries don't take the lock, so every agent on a board has to run version 2.
Agents still have to use it. Spool doesn't make agents disciplined; it makes forgetting cheap. In the assistant-app run nobody used handoff, and the agents asked for leases of an hour or two rather than the default.
It's small. It's a tracker, not a replacement for GitHub. What I'd want next is a local, lightweight GitHub, and that would need much more than Spool is. I'm not using it day to day.
It's open source, on crates.ionote 6, and small enough to read in an afternoon. The idea at the center is the useful part: a task belongs to one agent at a time, for a while, and comes back when that agent goes quiet.
Types Worth Keeping
Ferrotype
Ferrotype derives TypeScript types from Rust types, and they're the kind you'd be happy to have written by hand: tagged unions, template literal IDs, namespaces, and cross-references the Rust compiler checks.
Ferrotype is a Rust derive. Put #[derive(TS)] on the types your server sends and receives, and it writes the TypeScript types for your client, so the two can't drift apart.
There are crates that do this already. Their output compiles, but it uses hardly any of what makes TypeScript good, and you wouldn't want to have written it by hand. The bar I had in mind was a types package in ace, written by an engineer who contributes to Svelte's core: discriminated unions with a rich type tag, template literal IDs like vm-${string} , intersections like Claude.Todo & { createdAt: UNIX }, nested names like VM.Git.State, and indexed access to keep one type's field in step with another's. Ferrotype writes types like those.
What you get
Here's an enum from the test suite and the TypeScript it renders, exactly as the test asserts it:
#[derive(TS)]
enum Message {
Ping,
Text(String),
Binary(Vec<u8>),
Error { code: i32, message: String },
}
{ type: "Ping" } | { type: "Text"; value: string } | { type: "Binary"; value: number[] } | { type: "Error"; code: number; message: string }Every variant carries its tag, so TypeScript narrows on type in a switch without a cast. An enum of unit variants becomes a union of strings, like "Pending" | "Active" | "Completed". Option<T> becomes T | null, Vec<T> an array, HashMap a Record, i128 and u128 become bigint, and Result<T, E> becomes { ok: true; value: T } | { ok: false; error: E }.
That's what made it worth using in agent-foundry, a Rust agent of mine with a Svelte UI. Nineteen request, response and event types derive TS, and the streaming events arrive in the UI as one union it can switch on:
export type PromptEvent = { type: "TextDelta"; text: string } | { type: "ThinkingDelta"; text: string } | { type: "ToolStart"; id: string; name: string } | { type: "ToolInput"; delta: string } | { type: "ToolResult"; id: string; content: string; is_error: boolean } | { type: "Complete"; session: ApiSession } | { type: "Error"; message: string };The good parts of TypeScript
The attributes follow serde's names, so anyone who knows serde can read them: rename, rename_all, skip, flatten, transparent, and for enums tag, content and untagged. There are two kinds of optional, because TypeScript has two: a plain Option<T> is field: T | null, and #[ts(optional)] makes it field?: T.
Beyond serde, the attributes reach for the features ace's types use. A template literal pattern turns a string into a checked shape:
#[derive(TS)]
#[ts(pattern = "vm-${string}")]
struct VmId(String);type VmId = `vm-${string}`;TypeScript will now reject "abc123" where a VmId belongs. #[ts(namespace = "VM::Git")] puts a type inside nested namespace blocks and references it as VM.Git.GitState. #[ts(extends = "Claude.Todo")] makes an intersection with a type that lives only on the TypeScript side. #[ts(wrapper = "Prettify")] wraps a definition in the helper that's in almost every TypeScript codebase, and the generator can emit Prettify itself so the file stands alone.
Mistakes that don't compile
Indexed access goes furthest. #[ts(index = Profile, key = login)] renders a field as Profile["login"], so it follows whatever type login has. Rust has no indexed types, but a derive macro can still check one: it writes a small function that reads login from a &Profile, and if the field isn't there, the build fails (Fig. 3):
error[E0609]: no field `nonexistent` on type `&Profile`
--> tests/compile_fail/indexed_access_invalid_field.rs:13:33
|
13 | #[ts(index = Profile, key = nonexistent)]
| ^^^^^^^^^^^ unknown field
|
= note: available fields are: `login`, `email`
That's a broken TypeScript reference caught by cargo build, before any TypeScript exists. With a string argument instead, index = "ExternalType", it can point at types Rust can't see, and skips the check.
Using it
Add ferro-type, derive TS, and generate the file from build.rs or a flag on your binary with ferro-type-gen. Every derived type registers itself, so there's no list to keep:
let registry = TypeRegistry::from_distributed();The generator adds export, can write one file per Rust module, and skips the write when nothing changed, which keeps build.rs from triggering rebuilds. agent-foundry writes ui/src/generated/types.ts behind a --gen-types flag and also serves the same types over HTTP.
How it works
The foundation is one trait, modeled on serde's Serialize:
pub trait TS {
fn typescript() -> TypeDef;
}TypeDef describes a TypeScript type as a tree: primitives, arrays, tuples, objects, unions, intersections, records, literals, template literals, indexed access, references, and named types that carry a namespace and the Rust module they came from (Fig. 1). Nothing becomes a string until it's rendered, so a registry can collect every named type once, however many places refer to it, and write one file.
Registration uses linkme: the derive adds each type to a distributed slice that the linker assembles at build time (Fig. 2). It costs nothing at runtime and doesn't depend on code running before main, which is why it won over inventory.
Caveats
It reads #[ts], not #[serde]. The attributes share serde's names, but the derive only looks at its own. In agent-foundry, SessionDetail has #[serde(flatten)] on its info field, so the JSON puts the session's fields at the top level, while the generated type says { info: ApiSession; messages: ApiMessage[] } (Fig. 4). Nothing caught it because the UI only reads messages. Every attribute has to be written twice, and a forgotten one is a type that lies. The fix is to read serde's attributes directly.
Generics aren't generic. Paginated<User> renders as a plain Paginated with User[] inside, not type Paginated<T>, and registering Paginated<Post> too drops one of them. TypeDef can describe a generic definition, but you have to build that tree by hand.
Auto-registration is easy to skip. agent-foundry ended up listing its 14 types by hand, twice. Nothing stops you from doing that, and then nothing reminds you to add the fifteenth.
Continued from page 54 Types Worth Keeping
Output order and layout. Types come out in registration order, one per line. One-file-per-module output doesn't write imports between the files yet.
It's paused. It was built in three days by a swarm of agents in Gas Townnote 4 and has been at 0.2.0 since January (Fig. 5). The tests pass on today's stable Rust, and CI runs rustfmt, clippy and the tests, but nobody's adding to it.
The repo also has an unpublished crate that goes the other way, ferro-type-import, which parses TypeScript with swcnote 5 and writes Rust with serde derives.
The name is an old one. A ferrotype is another name for a tintypenote 6, a photograph made on a thin sheet of iron: ferro for iron, which is to say Rust, and type for types. The point is the same as the bar it started from. Generated types should be ones you'd keep.
From the archive2022
Design Docs
Design Docs, Version Two
Design Docs is a free, public-domain guide for designers trying to get their first job in tech. Version two moved the words out of the code so other people could write them too.
Design Docs is a guide for early-career designers. It's meant to give them a roadmap: how to improve their craft, put a portfolio together, and get their first job in the industry, or the job they're chasing.
Version two is out. It's a new site built in Next.js, the content has been through another pass, and you can read it here.
What's in it
There's no single right way into this industry, and the guide says so on its first page. It lays out some common paths and roles and how they fit together, and it says plainly that you don't have to follow anyone's path, including this one.
The sections run in order: build a foundation, discover the roles, have work to show, have a portfolio, talk about your work, and further reading. Most of the advice is the kind I wish I'd had early. Don't over-invest in your website: spend about a week on a first portfolio, and only build something elaborate once you have at least three strong projects. Don't use placeholder content in your designs. Show, don't tell. A long write-up doesn't make a better project.
Everything is CC0, which means public domain. Anyone can use any of it for anything, including copying it word for word into something commercial. I think education belongs in the public domain without strings attached.
Why rebuild it
The first version went up in November 2021, built in Nuxt in about five days. Every section was a Vue page, with the words written straight into the markup. That was fine while I was the only one writing it, and it got in the way of anyone else.
In version two the words live in seven MDX files in a content folder, and one template renders them. If you want to fix a paragraph you edit a text file, not a component. Most of the work since has gone into making the project easy to contribute to: a short CONTRIBUTING.md, a TODO list in the README, and tasks broken down over the next few versions so there's always something clear to pick up.
Who I want writing it
I don't want this to stay one designer's opinions. I'd like it to show work from a wide range of designers and to bring in people from many backgrounds to review it and add to it over time. One of the short-term items on the TODO list is to rewrite the content from a more neutral point of view, with opinions kept in clearly attributed blocks, so the guide can hold more than one voice.
How to help
Everything is open for contributions. Send a pull request, open an issue, or start a discussion on GitHub. If you'd rather not use GitHub, CONTRIBUTING.md has an email address, and content, links, reviews and feedback are all welcome that way too.
I'd like Design Docs to become a resource students and new designers can use wherever they are.
From the archive2021
fig-gen
Sixty Gens in Two Hours
fig-gen is a small generative series I made in Figma with Automator, a plugin that builds designs from stacked actions. One-pixel frames, placed by the trig functions I last used on a graphing calculator in high school.
I spent an evening trying out Automator, a new Figma plugin from Jordan Singer. You build a design out of stacked actions (create a frame, repeat, set a variable) instead of drawing it. A minimal generative series, made from pixels and equations, seemed like a good way to find out what it could do.
Nesting and rotating
I started by nesting shapes. What happens when you nest a shape and rotate it, over and over? Some of the results were good, but they all started to look alike, because rotation was doing nearly all the work.
Back to the graphing calculator
Then I remembered playing with graphing calculators in high school. Sin, cos and tan gave me interesting results right away.
Once I had a few directions I liked, I wanted them to sit together in a series, so each one needed a frame. I used the simplest outline I could, to keep everything monochrome and on theme.
What one gen is
None of the final gens use nesting. Each one is a black frame, 128 by 512, with a one-pixel white border inside it. A Repeat action runs 256 or 512 times, and on each pass it places one white mark. A mark is a frame one pixel tall. Its x position comes from the repeat's {{index}}, so the marks march along the x-axis like a plotted graph. Its y position is a function of the index.
The functions are mostly sin, cos and tan, a random variable and {{index}}, combined in different ways. The simplest saved recipe, gen3, sets two variables and places a one-pixel dot:
sin = Math.sin(2 * index)
cos = Math.cos(index)
y = cos * (index + sin) + 256Multiplying by the index makes the swing grow as the index climbs, so the dots open out into a chevron. Later recipes give each mark a random width, from a fraction of a pixel up to 6, 8, 16 or 32 pixels, depending on the recipe. The width is the product of two random numbers, so most marks are short and a few run long. Some recipes give each mark a random opacity too.
Sixty in two hours
My favourite thing about Automator was how fast it let me explore. I made about sixty gens, with a lot of variety between them, in under two hours.
Favourites
The files
I saved some of the recipes as I went. Six of them are in the fig-gen repo as Automator JSON, which you can load from Automator's settings with Import JSON. Do whatever you want with them.
Automator is gone now; its site survives in the Wayback Machine. The recipes in the repo are a record of how each gen was built rather than something you can still run, and the images below are what they made.
From the archive2021
Writing
Writing with ADHD
Geraint Evans named four ADHD traits that show up in how people write. I'd found out that year that I have ADHD, and he was the first person I'd seen describe the way I write.
This is a response to Geraint Evans's “This is how an ADHD person actually writes.” Start there if you can.
I'm a terrible writer
Or at least, I've convinced myself of that my whole life. What I write flows awkwardly. I've never understood all the rules, or how to make writing sound immersive and natural. It ends up verbose and it wanders. I mix tenses constantly.
I never understood why I write the way I do until the beginning of this year, when I found out that I have rather severe inattentive ADHD.
Then I came across Geraint's tweet, and it blew my mind. It was the first time anyone had described the way I write. Reading the replies under it, from lots of ADHD people who recognised themselves too, I realised I wasn't alone. Honestly, you should probably read his article instead of this one, but I'll try to add my own thoughts on top of it.
At the end of his article Geraint names four traits that show up in ADHD writing, so I'll use them as my structure. (Sorry if the structure doesn't hold. You know. ADHD. ¯\_(ツ)_/¯)
Impulsivity: “Impulsivity & rushing through tasks - I need to say this now!”
Attention to detail: “Yeah, probably fine, ill just post it.”
Being socially inappropriate: “Sorry, why do I have to change how I communicate when you clearly understand what I mean?”
Feeling misunderstood.
Impulsivity
Gotta go fast. My flavour of ADHD means a mind that is full of ideas all the time. You know the expression “drinking from the fire hose”? It's like that, for eight or more hours a day, every day, before I run out of energy completely and crash. (That's a topic for another time.)
Even sitting down to write this, it's hard to move on to the next sentence, because there are 18 ways I could continue this one, and 14 of them probably change the subject.
Each idea feels incredibly pressing, like the entire world should hear about it. Then I tweet it, or bug a coworker, or tell my partner, and find no interest at all.
So writing with ADHD means TONS of editing passes, even on a few paragraphs. I've usually crammed so many ideas into a small space that it's overwhelming and disjointed to read. It's hard to follow because I've left out the framing in between. That context is in my head, and I forget that other people can't see it.
Too much passion
With that many ideas, and a passion for a topic that burns hot and fast, I often oversell things or frame them wrong.
I once wrote an email to a director at the company I worked for, flagging a product decision as “unethical,” because I believed strongly that we were getting it wrong. It was a bad decision. But “unethical” was the wrong framing and the wrong severity. Would craft and inclusivity both suffer from it? Yes. Was it any more or less serious than the thousands of decisions the company made every day? Not really. In the moment, though, it felt like the most important thing in the universe, and my passion was turned up to 11.
Luckily, that director was very empathetic. They explained to the email thread what I probably meant, and why I was overreacting, without being condescending. It opened my eyes, and I've always remembered it. Stick with the people who will call you out with love. They'll help you grow enormously.
I feel like I've explained absolutely nothing here, so it seems like a great time to move on.
Attention to detail
Geraint's line nails it: “Yeah, probably fine, ill just post it.” Somehow this sits alongside the last trait, feeling misunderstood, even though the two pull in opposite directions.
For me it shows up in one of two ways. Either I write for hours, trying to get something right, or concise, or understandable, until I get frustrated and hit post. Or I write something in 30 seconds and send it without reading it back.
I could never write papers in school. When I tried to edit a long piece, I didn't edit it. I rewrote the whole thing on every pass. In the end I started writing papers a few hours before the deadline, so I wouldn't have time to fret over the content or the style. I often got better grades and responses by putting out a first pass than by trying to write the way other people would easily understand. Of course I couldn't do that. My brain doesn't work that way.
Deadline adrenaline
I call that “deadline adrenaline,” and I've come to lean on it in every part of my life. I'm not sure if it's strictly an ADHD thing or a me thing.
Deadline adrenaline means leaving something so late that I'm basically fucked if I don't finish it in the next few hours. School papers, project deadlines, even packing up a house or submitting documents: a lot of things in my life get done this way.
It lets me pull off tasks that seem impossible, with ease. But it can make it hard to work well with other people. My managers are always terrified, and then shocked when I turn around an entire project in a day, or finish what looked like an impossible amount of work in a short time.
Continued from page 68 Writing with ADHD
It's part of the ADHD cycles I go through, which I'd like to write about separately. The short version: when I'm not on medication, I go through pretty severe cycles, roughly daily and roughly monthly, in energy, creative output, focus and more. That makes it very hard to work with others when my creative or energy highs don't line up with where they are. When my highs do line up with a project or a collaboration, people are often amazed by how much I make and how good it is. And then, probably, confused a week later when I can't seem to make two screens.
That was a tangent. On to the third trait.
Being socially inappropriate
This one is straightforward for me. First, I recognise that I'm VERY, SUPER privileged to be able to work this way.
I don't work with people or companies that require me to do things a certain way. Need to dress a certain way? Nope. Talk or write formally? Nope. Maybe that seems messed up, but it's how my brain works.
I've been lucky to find a career where I could design a way of working that suits me and still brings people value. I think many ADHD people never get the chance to show what they can do, because they can't engage with the world the way the world wants them to. I was lucky to have some incredible teachers and mentors who saw value in me despite a “weirdness” I couldn't name until recently.
We leave a lot of great people behind because they can't, or don't, work the way the world expects. If my only options had been a suit and tie, or being on my feet in retail at 8am, I'd still be in my parents' basement. (People in retail: you're the unsung heroes, doing some of the hardest work for completely unjust pay. Thank you. I hope the way you have to work improves in my lifetime.)
I've always done worst when I tried to fit myself into the mould of “normal.” I come across as just an OK-ish designer. In the past few years I've found real success by leaning into being myself, unapologetically, even though I know that can be hard to work with and around. I have the utmost respect for managers and leaders who can take odd-shaped puzzle pieces like me and work out where they fit.
Feeling misunderstood
The biggest one. For me, feeling misunderstood is the root of almost all the social problems ADHD brings.
If I feel someone didn't understand what I said, here come ten minutes of explanation of that one statement, while the group slowly edges away from the odd guy.
Post something online? “People will pick apart every statement.” “Don't post that, people won't understand what you mean.” The self-censorship that comes from fearing being misunderstood is socially crippling, and it's bad for your career.
When I worked at Facebook, I always struggled with one expectation: as you become a more senior designer, you build a public following and start to have an impact on the industry. Even thinking about that was crippling, because of the fear of saying or doing something wrong and being cast out.
For an ADHD person, feeling misunderstood is TERRIFYING. There are a thousand minor slights and awkward moments I still think about every week, begging in my head to go back and explain or correct them.
Wrapping up
Even starting this wrap-up, I feel like I've written a lot of disjointed stuff that people won't get much out of. I'm putting it out anyway, because I'm trying to get better at challenging my assumptions, and to learn more about myself and my ADHD by sharing with others and getting feedback.
This wandered away from writing a bit. But writing is HARD for ADHD people. I'm trying to do more of it to get better, and to work out how these four things affect my writing and how I can improve it.
I wrote this in one pass, so apologies for any problems with spelling, flow or anything else. (That was true in 2021. This version has been edited since.)
I hope it's interesting or useful to someone.