The journey of building term2
Disclaimer: I used AI to write this post. Yes, it contains slop. I tried to de-slop it, but it’s not perfect. At least I told you about it. Read at your own risk.
The relay race
Back then, my workflow with AI and the terminal was a relay race between two windows.
The terminal throws an error. I copy it, alt-tab to ChatGPT, paste, wait. ChatGPT hands me a command. I copy it, alt-tab back, paste, run it. New error. Alt-tab again.
I was basically a human clipboard, carrying messages between two programs that should have been talking to each other.
Warp, and the rationing
Then I tried Warp, and the relay race ended. The AI lived right where the error happened. It saw my output, I asked in place, and the command landed where I needed it. After that, going back to copy-paste felt ridiculous.
Then the bill showed up. Warp costs money, and the free plan started to tighten: fewer AI requests, more upgrade nudges.
What really bothered me was the rationing. The whole appeal was asking without thinking. Once I started counting requests, I asked less, and the flow was gone.
term: a terminal is not a text box
I had wanted to build my own agent, something like Claude Code, for a long time. I also suddenly had a lot of free time. Ahem, unemployment.
My first attempt was called term. It was written in Python, and it failed.
A terminal looks like it just prints text. Then you try to build a live, streaming chat UI in one. Redraw the screen for every token and it flickers. Decide whether the chat lives in the terminal’s scrollback or takes over the screen, and pick wrong, and old messages vanish or smear across your history. Use alternate buffer mode like vim does, and now your scrollbar can’t scroll the chat.
I had no idea how any of that worked. term taught me that a terminal is a 1970s protocol with opinions, and it does not care about your streaming tokens.
So the next one was called term2. The sequel.
A fake bot, a safety checker, and a loan
This time I picked Ink, which is basically React for the terminal, because I already knew JavaScript. Ink diffs the output, so I could stop wrestling with redraws.
The git history of day one, November 24, 2025, reads like a diary.
-
6:02 PM, “Init.” The first app was a placeholder chatbot that opened with “Hello! I am a fake bot. Ask me anything.” and answered everything with Fake answer to: ”…”.
-
6:33 PM, “add agent.” Thirty-one minutes later the fake bot got a real brain: 36 lines on top of the OpenAI Agents SDK, which handled the agent loop for me. Remember this one, it comes back later.
-
10:32 PM, a safety checker. Before markdown rendering even existed, a second AI judged each bash command and decided whether it needed my approval. If the checker crashed, the default was to ask me.
-
12:21 AM, still committing. Sleep lost, as usual.
Day two brought streaming, approval prompts and “convert to typescript.” On day three, at noon, came “abstract provider logic.” I was already planning not to depend on a single provider.
Every shortcut is a loan
Warp gave me AI in the terminal until the free plan tightened. Ink handled rendering, and the terminal quirks still bit me. The Agents SDK handled the loop until I needed to change it.
Every shortcut gets you moving today and collects interest the day you need something it wasn’t built for.
The first interest payment came on December 7. I added OpenRouter so I could pick any model, and the very next commit was “enhance OpenRouterModel to comply with Agents SDK response structure.” I was already bending other providers into the shape the SDK demanded.
Meanwhile, term2 kept growing.
-
Christmas Eve, 10:44 PM:
ask_mentor, so the agent can call a smarter model when it’s stuck. Mentor mode followed at 12:19 AM on December 26. -
December 26: Ink’s
<Static>. Finished messages print once and sink into the terminal’s normal history, which fixed scrollback. -
New Year’s Day: a doc called “terminal companion.” Lite mode for everyday terminal tasks followed, then SSH mode at 2:38 AM. That was the part of Warp I loved, AI in my terminal, now on remote servers too.
-
January 9: v0.1.0 on npm, then four “update readme” commits in 50 minutes.
-
February 12: DEC Mode 2026. This escape sequence tells the terminal to buffer a whole frame and draw it at once, so no half-painted flicker. Mode 2026, shipped in 2026.
The terminal quirks still bite me, by the way. Ink only made them rarer.
Safety went through four versions over the year.
-
November: ask an AI whether a command looks dangerous.
-
December: hand-written rules for
sed -i, redirects and risky paths. At one point they blocked commands even after I approved them. -
April: bring the AI back, and measure it with an eval harness.
-
June: stop judging commands and run them in a sandbox.
The silence, and the comeback
After a Valentine’s Day release on February 14, term2 went quiet. March had zero commits.
When I came back in April, the first thing I worked on was safety again. This time an LLM advises whether to auto-approve a shell command, and an eval harness grades it: 717 lines of test commands, a model leaderboard, and severity-weighted scoring with a penalty for wrong approvals.
My favorite rule in that dataset: destructive commands must be rejected even when the user asks for them. I graded models on whether they’d say no to me.
From May on, the pace picked up.
-
May 17: subagent specs written at 3:06 PM, a working subagents tool by 3:54 PM. Plan mode and an orchestrator mode followed within days.
-
May 24: ChatGPT Codex login, with the rate limits shown in the status bar. I’d started this project because I was rationing Warp requests. Now I ran on my own subscription and could see the quota.
-
May 31: agents get a warning before they hit their turn limit, so they can write a status report before they’re cut off.
-
June: skills support, and a sandbox.
-
July 12: 29 commits in one day, including programmable agent workflows, AgentRuntime lifecycle events, a memory system, model tiers with fallbacks, and a “librarian” subagent that manages memories. Most of term2’s current architecture dates from that Sunday.
The bug that ended the relationship
The loan from day one came due through a single bug.
With the Responses API, you can send a previous_response_id instead of the whole conversation, and the server remembers the rest. It’s cheaper and faster until the server forgets. Over a Codex WebSocket that expires and reconnects, the ID can vanish, and you get “previous response not found” or tool outputs with no matching call.
It drove me crazy. Fix after fix, and it still happened.
-
June 4: chaining over WebSocket lands, followed by five fixes that night.
-
June 19: three more between 9:22 and 11:37 PM.
-
June 27, July 11 and July 13: “fix chained continuation recovery,” then “fix stale chained continuation recovery.”
-
July 26 to 28: guards against orphaned tool outputs, then a fix for a blind spot in the guard.
-
July 31, 10:47 PM: “Fix Codex WS orphaned tool outputs,” the night before the breakup.
The SDK owned the loop, so I couldn’t fix this at the right level. I had been customizing it anyway, by picking its locks. My plan doc kept a risk register where every entry was a private-API reach-in: _pendingAgentToolRuns, _mergeApprovals, _generatedItems.
The plan measured progress in reach-ins retired, not lines deleted. It also had a section correcting the original investigation, which assumed the SDK couldn’t save mid-turn state. It could. The real blocker was my own in-memory maps.
On August 1 the commit landed: “decouple from openai agents sdk.” It changed 119 files, adding 2,946 lines and removing 3,337. My own run loop replaced the SDK with fewer lines than before, 4,813 tests passed, and no references to the SDK were left.
The next commit was “Enforce turn budgets in the run loop,” something the SDK had made awkward.
The chaining bug got fixed by changing who’s in charge. The conversation history I keep is now the source of truth, and the provider’s chain is a shortcut that gets thrown away when it doesn’t match. I couldn’t make that rule while the SDK owned the loop.
I never wrote a line of it
Here’s the part I left out: I haven’t written a single line of term2’s code. That includes the run loop that replaced the SDK.
All 4,265 commits were written by agents: Codex, Claude, Copilot, Gemini, and others I honestly can’t remember. Claude signs its work with co-author trailers, on about 160 commits. Everything else was committed under my name, so only a few percent of the history admits to being AI.
My job was writing specs before features, keeping plan docs with a “Resume here” section so the next agent could pick up where the last one stopped, building the harness that grades the safety judge, and deciding what “done” means.
Once term2 could handle it, I put it to work on itself. You can see it in the history:

git log of qduc/term2 · 4,265 commits, Nov 24, 2025 to Oct 18, 2026
The week term2 got its own run loop is the week I started taking myself out of the loop. Weekly commits went from 61 to 297 and stayed high. The busiest week had 540, and the busiest day was September 26, with 157.
The overnight numbers show the same thing. Before August, a dozen or two commits a month landed between midnight and 6 AM, mostly me refusing to go to bed. August had 117 and September had 155. On August 2, the first night after the breakup, term2 shipped 44 commits between midnight and 6 AM.
Today term2 writes most of term2, with Codex and Claude chiming in now and then.
How I stepped out without losing trust in the result is a separate post: Verification Is All You Need.
term2 started as a fake bot that answered every question with “Fake answer to…”. Eleven months later it does most of the work, and my job is telling it where the finish line is.
Where term2 is today
term2 is at v0.33.0, open source under the MIT license. Here’s what you get if you install it today.
-
Coding and terminal work: read a codebase, trace a bug, edit files and run tests. Plan mode keeps it read-only, Lite mode is for everyday terminal help, and
term2 --sshworks on remote projects. -
Any model you like: log in with your ChatGPT account for Codex, or use OpenAI, OpenRouter, Grok, Anthropic, Gemini, or a local model through llama.cpp or any OpenAI-compatible endpoint. Switch with
Ctrl+O. -
Subagents for research, implementation and review, including background tasks you can watch and steer from a task manager on
Ctrl+G. -
Goals: the agent has to report whether the goal is achieved or blocked before it’s allowed to stop.
-
Safety defaults: shell commands run in a sandbox, auto-approval is off, and runs pause at their budget or context limit instead of running past it.
-
Sessions you can resume, fork to try another approach, or rewind to any earlier message.
-
term2 servewith a web client, if you’d rather use it from a browser.
You need Node.js 20 or newer:
npm install --global @qduc/term2
term2 --codex-login # or: export OPENAI_API_KEY=...
cd /path/to/your/project
term2 --model
The docs cover providers, modes and the sandbox, and the code is on GitHub. If you find a bug, there’s a decent chance term2 will be the one fixing it.