The day after we shipped version 4.5.12 — the release that introduced the task delete guard and the explicit "never offer delete for tasks" hard rule in the system prompt — the attorney tried the task walker again. Her input was three lines:

aaicase> please help me go through my tasks one at a time

What came back was three problems in a single response.

First, the system batched two tasks into one card. The Alvarado case and the Morales case, both involving the same employer and possibly the same client, were presented together as a "pair." The skill specification says one task at a time. The model combined them anyway because they looked related.

Second, the options at the bottom included "Delete both" as choice 3. The system prompt has a hard rule against offering delete for tasks. The merus-fetch helper refuses /tasks/del/{id} calls at the HTTP layer. None of that stopped the model from typing the word "Delete" on the screen as an option for the attorney to pick.

Third, when the attorney typed explpain more — a typo of "explain more," asking for additional context on the paired tasks — the model interpreted it as a skip command and jumped to Task 4. An ambiguous input got a silent destructive action.

Three different failures, one underlying cause. The model was running the loop. As long as the model is running the loop, the rules about the loop are requests, not guarantees.

What "the model is running the loop" actually means

In the original task-walking flow, the trigger phrase "process my tasks" matched a skill file. The skill file described a procedure: fetch the open tasks, sort them, walk them one at a time, for each task present a card and wait for the attorney's input, route the input to complete or update or skip, write the change, move to the next task. The model read the skill file, understood the procedure, and executed it as a sequence of API calls and console output.

That sounds reasonable until you realize what "executed it" means at the implementation level. The model is not running a for loop. The model is generating text turn by turn, and each turn it decides what to do next based on its understanding of what came before. The skill file is one of dozens of context items in the model's window. The presence of 178 tasks in the working memory creates pressure toward batching. The attorney's signal of fatigue creates pressure toward speed. The model's general training to be helpful creates pressure toward offering more options.

None of these pressures are stronger than the skill file as written. They are also not weaker. They are different inputs competing for the same output, and on any given turn one of them might win in a way that produces a behavior the skill file would have prohibited.

The skill file said "one task at a time." The model batched anyway. The system prompt said "never offer delete." The model offered it anyway. The pattern at this point is unmistakable: text-level instructions to a probabilistic process produce text-level adherence, which is mostly correct and occasionally wrong, and the wrong cases are exactly the cases that matter.

The architectural answer

If the model running the loop is the problem, the answer is to not have the model run the loop.

In version 4.5.13 we built a new program, bin/aai-task-walk.mjs, that runs the task-walking loop itself. It is several hundred lines of plain Node.js. It has a for loop. The loop variable goes from zero to the length of the task list, one at a time, with no possibility of skipping or batching. When the loop body runs, it prints a single task card to the terminal. The card always has the same shape. The options at the bottom are always C, U, S, Q. There is no code path in this program that prints the word "Delete" as an option.

The program reads from stdin and routes the input deterministically:

The model is involved in exactly one place: answering questions. It cannot decide to skip a task. It cannot decide to combine two tasks into one card. It cannot decide to offer delete. The loop is owned by code. The code refuses.

The handoff

When the attorney types "process my tasks" in the aaicase REPL, the REPL pauses readline and spawns the walker as a child process with inherited stdin and stdout. The walker fully owns the terminal until it exits. The agent loop in the REPL is not running during this time — it is literally paused, waiting for the child to terminate. The model could not interfere with the walker even if it wanted to, because the model is not invoked at all during the walk.

When the walker exits, the REPL resumes. The walker prints its own final summary. The model is not asked to summarize it again, because the model is not running yet — the REPL just re-renders its prompt and waits for the next attorney input.

The handoff is the part that makes the architecture honest. If the walker were a function the model called as part of generating its response, the model could still misuse it (call it twice, call it with the wrong arguments, ignore its output, paraphrase its output incorrectly). By making the walker a separate process that takes over the terminal, the model is structurally locked out of the walk.

Fresh state on every iteration

Before the walker shows each task card, it re-reads state from disk and re-fetches state from Merus. Specifically:

Each iteration begins with a thin gray status line showing what loaded:

[fresh-load] skill ✓ bindings 42 audit 50

This is deliberately verbose. The attorney can see, every single time, that the walker is not operating on stale memory. The slowdown is real — fresh state load adds maybe a second per iteration. For a 178-task walk that adds three minutes. The alternative is operating on memory from when the walker launched, which is exactly the failure mode the rest of this architecture exists to prevent.

What this is not

It is not a removal of AI from the workflow. The model is still used. It is used for the things models are good at: reading PDFs and extracting structured data (via aai-bind), classifying emails against a firm's tag catalog (via aai-bind-message), answering questions about a specific task (the question branch in the walker), drafting letters, summarizing case state, scoring deadlines. None of that changes.

What changes is which entity is responsible for the integrity of multi-item workflows. Before this release, the model was responsible. The rule existed in the skill file, and the rule was followed most of the time, and the times it was not followed produced the bugs that motivated this work. After this release, code is responsible. The model can still misclassify a PDF — that bug exists and aai-bind tries to bound it — but the model can no longer accidentally batch two tasks into one card, because there is no code path that does that.

This is a smaller scope than "make the model better." Making the model better is a research problem with no clear release date. Removing the model from the loop control is a build problem that took about a day. The build problem produces a guarantee. The research problem produces a hope.

What other flows will get the same treatment

The pattern generalizes. Any multi-item workflow with write side-effects is a candidate for a code-driven loop:

We will not move every skill to a code-driven loop. The criterion is: does this flow produce write side-effects across multiple items, where batching or skipping incorrectly causes harm? If yes, the loop belongs in code. If no, the model is fine.

The cost of doing this

A code-driven walker is more work than a skill file. The walker for tasks is about five hundred lines. It has to handle: argument parsing, signal handling, terminal rendering, ANSI color codes, readline state, child process spawning, JSON parsing, error display, the question branch's claude subprocess, the update sub-flow's plain-English parsing, audit logging, summary printing. None of that is hard, but all of it takes time to write correctly.

The skill file gets simpler in trade. The new process-tasks.md is mostly documentation describing what the walker does, with three short rules ("never delete," "never show bare IDs," "re-read state before each operation") that survive into the agent context for moments when the model is briefly involved.

The total system complexity goes up. The total system safety goes up faster. For an AI tool that writes to a law firm's case management system, that is the right trade.

What the attorney sees

The visible change is small. Same prompt ("process my tasks"), same outcome (one task at a time, with options). The differences are subtle:

From the attorney's seat, it looks like a slightly more rigid interface. From the architecture's perspective, it looks like a loop that the model cannot get inside.

Why we keep writing about this

Each of these blog posts describes a small bug, a small fix, and a small piece of an architecture that is becoming clearer over time. The misfile post described moving applicant identity into a code-enforced binding. The eight-year-old task post described moving deletion out of the option list. This post describes moving the loop itself out of the model's hands. Each fix is one shape of "code refuses what prompts ask."

The shape of the architecture is starting to be predictable. Whatever the next failure mode is, the response will be: identify the place where the model is making a decision that should not be the model's to make, encode that decision in code, remove the option from the model's interface, write the rule into the system prompt as documentation for the cases where the model is still involved. The work is not glamorous. It is also the only durable way we have found to make an AI tool that operates safely inside a law firm.

Version 4.5.13 is live. The task walker takes over the terminal when the attorney asks. The model is no longer running the loop.

Postscript: what changed in 4.5.14

The first version of the walker shipped with four options: complete, update, skip, quit. The first attorney to use it asked, reasonably, why she couldn't reassign a task to a paralegal, or look at the documents on the case before deciding what to do with the task, or switch which user's tasks she was walking. Those are not abstract requests. They are the actual things attorneys do when they go through a task list.

The follow-up release expanded the option set to: Complete, Keep, Skip, Reassign, Update, review Docs, review Activities, switch Filter, Quit. The walker also now asks at startup whose tasks to walk — yours, everyone's, or a specific person — and every card shows the active filter in its header so you always know who you're working for. That update is documented in the post "Going Through Tasks the Way Attorneys Actually Do It."