Skip to content

Build1 publisher3 min readPublished

Clarifying one requirement retired the best argument for keeping this agent in bash

The reasoning that kept a small agent in bash was sound, and its author still thinks so. The requirement underneath it turned out to be a preference, and the measurement that settled the question took milliseconds.

The Engineer · Build desk

Illustration accompanying Clarifying one requirement retired the best argument for keeping this agent in bash

What happened

  • A small agent that drives command-line tools and one model call went from 2,150 lines of bash across seven files to Python, and the port took a day.
  • A README section titled Why this is still bash had argued that nine tenths of the program is subprocess orchestration and that rewriting 2,000 untested lines loses behaviour silently.
  • The rewrite followed a clarified requirement, when no build step turned out to be a preference for an uncomplicated start instead of a hard rule.
  • Measured startup was 89 milliseconds for Python with its standard library imports against 3 for bash, in a program that waits thirty to a hundred seconds on the model.
  • Total lines rose by 108 to 2,258 while code fell 40 percent to 1,278, with 980 lines of comments and docstrings making up the balance.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint The 89-against-3 figure only transfers to a program that starts the interpreter once and then waits on something slow; a script invoked per file from a loop pays the 86 milliseconds on every call.
  • capability With behaviour held in Markdown prompt files and separate JSON-speaking executables, the glue language becomes a replaceable part, and the agent could move a piece at a time.
  • cost What bash charged was expressiveness: structures were shaped to what jq could state on one line, and that bill shows up in designs the author never considered, not in a profiler.
  • decision A team defending a language choice on one argument has a cheaper first move than reopening the language debate, which is to confirm the requirement that argument depends on is still a requirement.

Behaviour had already left the language before the rewrite started. Prompts live in individual Markdown files with {{placeholder}} holes that the code fills, a choice the author made because prompts get edited constantly and because a stray `$` or backtick has to stay inert instead of being eaten by the shell [17]. After the migration he checked modification times, and the port had not changed a word of any prompt [18]. The tuned criteria and the order in which judgments get made live in those files, so switching languages replaced only the glue that assembles them [19].

The second decision was a workaround. Each kind of task is its own executable: verb on argv, JSON on stdin, JSON on stdout, adopted because bash has no modules and a process boundary beat having the pieces scribble on each other's variables [20]. The author calls that "Pure coping." [21] It also meant there was no big-bang rewrite [22].

Forking was never the complaint. 162 forks cost nothing next to a minute of model latency [10]. Expressiveness was the cost: every structure in the program either fit in a one-line `jq` expression or got split into three pieces, so what got written was the structure `jq` could state on one line [11]. He had three lists, the files a change actually touched, the files the agent itself had written, and the files approved in advance, and needed two differences between them [12]. The Python comprehensions are barely shorter than the `jq`; the difference is that they are what he would have written on the first attempt, while the `jq` took several tries [12].

One run died on line 567 with `1: command not found`, and he never located it; it went away when that section was rewritten for unrelated reasons [13]. The post does not say the agent gained test coverage before or during the port [23]. Rewriting 2,000 untested lines was one of the original arguments for staying [5].

The 86 millisecond difference is about 0.29 percent of the shortest run this program does [1]; one 30-second model call covers roughly 337 Python startups [4]. For that number to transfer, your program has to start the interpreter once and then spend its life waiting on something slow. Invoke the same script once per file from a build loop and a thousand calls add 86 seconds of startup [2].

Comments and docstrings are 43 percent of the Python file [3], and they are the notes recording which specific incident each safety check exists to prevent, which is exactly what the author had been afraid of losing [24]. "Evaluate this rewrite by total line count and it accomplished nothing," he wrote [16].

"My reasoning was valid; its inputs weren't, and I had spent almost no effort checking them," the author wrote [9]. That is the part worth copying. The language question was the less interesting one here, and the bug ledger says so: the expensive bugs were design errors that any language would have permitted [6]. A requirement recorded as hard turned out to be a preference about starting simply [7], and 86 milliseconds was the whole of what it had been protecting [8].

What to watch

  • Whether the JSON boundaries between the per-task executables get folded into Python modules now that the language has them, removing the seam that made this port incremental.
  • Whether test coverage arrives now that the glue is in Python, given that untested rewriting was one of the original arguments for staying in bash.
  • Whether the author reports a bug class introduced by the port that the bash version did not have.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories