The Coding Agent I Could Shape Around My Workflow
DEV Community

The Coding Agent I Could Shape Around My Workflow

Claude Code is my daily driver at work. Its defaults are very good, and most things just work. But for a while, I had been hearing plenty of praise for how extensible Pi was. Pi is a small, extensible coding-agent harness that runs in the terminal.

That caught my attention because I am a hardcore Neovim user. I have stayed with Neovim not because it arrives perfectly suited to me, but because I can keep shaping it around the way I work. Start with a small core, add what is missing, change the awkward parts, and eventually the editor feels like yours. Pi seemed to promise something similar for coding agents.

Pi does not officially support using an Anthropic subscription, so I got an OpenAI subscription and ran Pi with Codex.

Starting small

One of the first things I asked Pi was whether it could show my OpenAI usage limits in its status line. It replied that it should be possible with the Codex CLI. So after I installed the Codex CLI, Pi built an extension that displayed both rate-limit windows-the five-hour and seven-day windows-in the footer:

Codex used   5h:32% (resets 8:00 PM)   7d:14%

It launches Codex's app server, requests the current rate limits, and updates the status after turns. I could ask Pi to modify the extension, run /reload, and see the result without restarting the CLI.

It was useful, but it was not yet my "wow" moment. Usage information is hardly unique to Pi. I kept using it on side projects. It felt lighter than my usual setup and gave me more control over the agent. I installed an existing RTK integration, added a few keybindings, and made small changes as annoyances appeared. These were good customizations, but they were mostly things I knew I wanted before I started.

The monitor that blocked the conversation

My real moment with Pi came while working on CI. I had asked it to open a pull request and monitor the checks until they turned green. I left it running, came back later, and tried to ask something else. The monitor was still running in the foreground, so my messages were queued behind it.

I stopped the command and asked whether Pi could monitor the PR in the background instead. It could use shell tricks to launch the process, redirect its output, and save its PID. But the command could finish in the background, yet the agent had no reliable way to wake itself and react. So I asked Pi to build that.

The resulting extension registered a background_monitor tool. It runs a long-lived command asynchronously, captures a bounded tail of its output, and returns immediately so the conversation remains usable. When the process exits, the extension notifies Pi and triggers a follow-up turn. The agent can inspect the exit code and output, report success, or react to failure.

This was the first extension that changed how the harness itself worked for me rather than merely adding information to its interface. It also taught me something related: adding a tool does not guarantee that the model will use it. We had something along the lines of this on our first iteration:

Run background commands whose completion the agent must react to

In the next CI session, Pi reached for another blocking Bash watcher. The new tool was registered properly but Pi was not selecting it. After a few iterations, the routing rule became:

Run slow, finite shell commands asynchronously and wake on completion

That small instruction made the new mechanism part of the agent's normal behavior.

"You have background agents?"

Shortly afterward, I asked Pi to research asynchronous alternatives to a few blocking Vim Fugitive operations. Pi told me the research was running in a background agent. That was news to me. I had recently installed Matt Pocock's research skill, which explicitly asks the parent agent to delegate the reading.

Pi could always launch another pi process through Bash; there was no hidden swarm or built-in multi-agent framework. But before background_monitor, doing so would either block the parent or leave it with an unmanaged detached process. The monitor supplied the missing lifecycle: launch the child, keep working, capture its result, and wake the parent when it finished. One extension had accidentally paved the way for another capability.

The first version was crude, though. The delegated Pi ran in non-interactive print mode, and its output was hidden inside the monitor. I could not see what it was doing or steer it while it worked. That was not the kind of background agent I wanted. If an agent is working on my behalf, I should be able to look at it whenever I want.

From hidden subprocesses to inspectable agents

We built a separate background_agent extension. Each delegated agent runs as an interactive Pi session inside tmux. The tool call returns immediately to the parent conversation, while the child gets a fresh conversation with the parent's model, thinking level, and working directory. I can list the agents, attach to one, inspect or steer it, and return to the parent.

While researching this post, the task list looked something like this:

  • Background tasks running
    • agentMine old conversations running
    • agentInspect extension code and Git history

Tmux also gives these agents a useful durability property: they survive /reload. The extension can rebuild its watchers from metadata stored with the tmux sessions, while completed children leave records waiting to be consumed.

Then we had to decide what "finished" meant. Waiting for the child process to exit would be wrong because I wanted the interactive session to remain available. Instead, the extension watched Pi's agent_settled event, reporting when the initial task had settled while keeping the tmux session alive for inspection. That seemed like the right boundary-until I used the agents to research this post.

"Settled" is not always "done"

I asked Pi to gather context for this post. It launched two background agents: one to mine old conversations and another to inspect the extension code and Git history. Each child loaded the same research skill as its parent. That skill told it to delegate the reading, but children deliberately do not have the background_agent tool. So they improvised: each launched another Pi process through background_monitor, then replied that research was underway.

The parent immediately told me both children had finished and went looking for reports that did not exist yet. At first glance, this might look like a race in the tmux machinery, but Pi had correctly emitted agent_settled: each child's own loop had no tool call, retry, or queued follow-up left at that instant. But each child still owned asynchronous work outside the loop. "Settled" was local. "Done" was transitive.

The fix was to let the extensions coordinate. background_monitor now announces when asynchronous activity starts and finishes. A child agent tracks the activity it owns and refuses to report completion while any remains active. Only after the monitors finish, their follow-up turns run, and the child settles again does the parent receive the result.

A regression test with two concurrent monitors locks down the behavior: neither initial settlement nor the first monitor finishing is enough. Completion happens only after both jobs and the final turn are done. The protocol does not pretend to discover every process on the machine. Background work has to declare itself. But its definition now matches what I mean much more closely: Done means the agent is settled and no asynchronous work it owns is still running.

There was also a simpler problem underneath all this. The children should not have delegated their own research in the first place. By loading the same skill, the workers were following an instruction written for a coordinator. The extension now adds one worker-scoped instruction to every delegated task:

Delegated task: Complete all work-including skill delegation steps-in this session.

The two fixes solve different problems. Activity tracking makes completion correct when a child legitimately owns asynchronous work. The worker prompt prevents orchestration instructions from recursively creating that work. The satisfying part is how we found both: by using the extension for real work, watching it fail, and asking Pi to diagnose and extend itself again.

The Neovim feeling

This is what finally made Pi click for me. I did not install a predetermined "background agent system" and adapt my workflow to it.

  • A foreground watcher blocked the conversation.
  • Shell backgrounding removed the block but lost completion notification.
  • A monitor connected process completion back to the agent.
  • Running Pi through that monitor made delegation possible.
  • Hidden delegation created a need for inspectability.
  • Dogfooding then exposed that an agent could settle before its asynchronous work was done.

Each solution revealed the next problem. That feels similar to what attracted me to Neovim, with one important difference. Much of my Neovim setup consists of extensions other people built, plus smaller customizations of my own. AI makes writing the missing pieces yourself dramatically more approachable.

Pi has an ecosystem too. I use existing packages where they fit: pi-vim for Vim mode, RTK integration for token-efficient shell commands, and skills for workflows such as research and code review. But when something feels wrong, I can ask Pi how it works, change it, reload, and keep going in the same conversation. If that becomes an extension-building detour, /tree lets me return to the original conversation branch as though the divergence never happened.

There are costs. My extension directory eventually became a real TypeScript project with pinned types, strict compiler settings, dependency scripts, and regression tests. Tmux-backed agents create lifecycle and cleanup questions that hidden subprocesses avoid. But those costs are visible and understandable. Pi is small enough that I can inspect what it produced and test the exact failure that motivated a change. That sense of control matters more to me than having every feature built in.

The best part, though, is simpler: this is fun. Pi feels less like a fixed coding product and more like a tool I am gradually shaping around my own requirements. Its gaps are not always shortcomings to work around. Sometimes they are invitations to teach the harness a new trick-and Pi is remarkably good at helping build the trick itself.

Comments

No comments yet. Start the discussion.