AI Workflow · part 10
[Dev Workflow] Delegating to an AI Coding Agent: The Unit of Work Is a Ticket File, Not a Conversation
❯ cat --toc
- The unit of delegation is a file, not a conversation
- A ticket is four blocks: goal, steps, machine-checkable acceptance, red lines
- Dispatch is a background job, not a foreground chat
- The scars: three flags I paid for in dead time
- A report is not a result — gate it before you trust it
- Takeaways
- The dispatch checklist
TL;DR
Handing a task to an AI coding agent isn't a conversation — it's a work order. I dispatch engineering tasks to Codex as self-contained ticket files (goal / steps / machine-checkable acceptance / red lines), run them detached, and gate the report before trusting it. The rule I paid for: a background worker that hangs never tells you. One Codex run sat dead for 2 hours 17 minutes — hung about five seconds in — because I hadn't pinned three flags or attached a watchdog. Three scars became three flags (no nohup &, -c "mcp_servers={}", < /dev/null) plus a 5-minute liveness check. And a report is not a result: re-run the acceptance before you believe it.
I dispatched a ticket to Codex and went to do something else. Two hours and seventeen minutes later, it was still "running." It wasn't running. It had hung about five seconds in — CPU frozen near zero, its session log stuck at two lines: the prompt, and task_started. I only noticed because a passing "it can't be taking this long?" made me go look.
You notice a crash — it exits, prints a stack trace, something wakes you. A hang in a background worker is silent. It looks exactly like work. That afternoon is why I no longer talk tasks to an AI executor. I hand it a file.
This is Part 10 of the AI Workflow series. Part 9 externalized live state — what's done, what's mid-flight — so a fresh session could pick a task back up. This one externalizes the thing one step earlier: the task itself. Same through-line as the publish-gate hook — take the discipline out of my head and put it somewhere a machine enforces it.
The unit of delegation is a file, not a conversation
When I first started routing work to Codex, I did it the obvious way: I described the task in the chat, the way you'd explain it to a colleague leaning over your desk. It worked when I was sitting there to answer the follow-up questions. It fell apart the moment I wanted to walk away — which is the entire point of delegating.
A conversation is a bad unit of work for three reasons. It isn't self-contained: half the context lives in the scrollback the executor can't see. It has no acceptance criteria: "make it work" isn't something a process can check against itself. And it dies with the session: the moment the window closes, the task is unrecoverable.
So my first rule applies before I dispatch anything. Ask one question: is this an engineering execution ticket? — env setup, a data pipeline, a batch of mechanical edits, a build chore, anything whose acceptance a machine can verify. If yes, it becomes a ticket and goes to the executor. If it's judgment, taste, or a one-off call, it stays with me. (The exception that keeps me honest: a one-line config edit I can verify and roll back in seconds isn't worth a ticket — writing the ticket costs more than the change. The threshold is investigation or cross-file logic, not lines changed.)
The commander doesn't step onto the field. The main session routes, judges, and integrates; the execution happens somewhere else, against a file.
A ticket is four blocks: goal, steps, machine-checkable acceptance, red lines
The format is deliberately plain, because the reader is a fresh process with none of my context:

Here's a real one, sanitized — a retirement task, the kind where the danger is doing too much:
# Goal
Retire the legacy mirror hook: nothing in the active config should still call it.
# Steps
1. Remove the mirror hook from ~/.config/<tool>/hooks.json
2. Drop the launchd job that triggers it
3. Leave the four archived copies untouched
# Acceptance (machine-checkable)
- `jq empty ~/.config/<tool>/hooks.json` exits 0
- `grep -rl mirror-memory.sh <active-configs>/` returns nothing
- all four archive dirs still exist
# Red lines
- do NOT delete the archives
- do NOT edit the runtime copies directly
- if the topology looks different from this ticket, STOP and report
The block that does the real work is Acceptance. "Do a good job" is not checkable; jq empty … exits 0 and grep … returns nothing are. If the acceptance can't be expressed as something a shell can decide, the ticket isn't ready — and that's a signal about my thinking, not the executor's ability.
I have one test for whether a ticket is done being written. Could a fresh AI, with zero memory of this conversation, get it right on the first try from this ticket and the files I attached — nothing else? If not, I'm the one who under-specified it. I learned that the expensive way: I once sent a task as a narrow, terse work order — just the steps, no context about where it sat in the project or which environment traps to expect — and the executor hit an ssh/tmux/detach edge it couldn't reason its way through. It hung twice and bounced back to me five times. That wasn't a weak executor. That was a bad ticket. The quality of the dispatch is the orchestrator's job, not the executor's.
Dispatch is a background job, not a foreground chat
A ticket runs detached. From inside the agent harness I use its own background primitive — the harness wakes me when the run finishes and hands me the report. In a plain terminal the shape is the same:
codex exec -C <repo> --dangerously-bypass-approvals-and-sandbox \
-c "mcp_servers={}" \
"$(cat ~/tickets/<ticket>.md)" > ~/logs/<name>.log 2>&1 < /dev/null
That command line looks over-decorated. Every flag on it is a scar. Here's how I earned each one.
The scars: three flags I paid for in dead time

nohup … & — no. When I dispatch from inside the harness, nohup detaches the process from it. The run finishes, but nothing wakes me: the report just sits in a log file, uncollected, while I assume it's still working. I did this three times before the rule stuck. Use the harness's own background mechanism so completion actually notifies you. (From a bare terminal with a human watching, nohup is fine — the rule is specifically about running inside something that's supposed to wake you.)
-c "mcp_servers={}" — always. An execution ticket needs a shell and nothing else. Loading MCP servers into it is pure downside: if a transport fails to start — an OAuth token that isn't there, an AuthRequired — the whole process can hang permanently, right after task_started. That was the root cause of the 2-hour-17-minute corpse. Pass an empty MCP config and this whole class of hang goes away — an exec ticket has no reason to pull in MCP servers in the first place. (It clears the servers you configured; it won't disable an MCP source some host or plugin injects, so it's a strong default, not an absolute guarantee.)
< /dev/null — always. codex exec with a non-TTY stdin that isn't wired to a pipe intermittently blocks waiting on input that will never come. Redirect stdin from /dev/null and it can't.
And the flag that isn't a flag: a watchdog. A hung run never exits, so the harness's "wake me when it's done" never fires — the one failure mode the background primitive can't catch is the one where nothing ever completes. So every ticket gets a ~5-minute log-freshness check running alongside it. The hang has a clean fingerprint: CPU near zero despite tens of minutes of elapsed time, a session log frozen at one or two events, a log file that stops growing. Silence is not progress. A background worker with no liveness signal is a bet, and the 2h17m run is what losing that bet costs.
A report is not a result — gate it before you trust it
The executor is both the worker and the narrator of its own work, which violates the oldest rule in the book: the producer shouldn't certify their own high-risk output. So a completion report doesn't close a ticket. It opens a gate:
- Spot-check one red line. The report says it didn't touch the archives — I
lsthem myself. Claimed ≠ true. - Re-run the acceptance, line by line. Not the summary sentence "all done." The actual
jq, the actualgrep, against the actual state. - Scan for conflicts with what I already know. If the report contradicts my notes, that's a high-value signal, not noise — maybe my notes are stale, maybe the report is confabulating. I find out before I decide, not after.
- Check one artifact for real. The file exists, the test actually ran, the change is live in the running system — not just green in the repo.
Only then does it resolve, and only into one of three verdicts: go, re-issue the ticket with the failure trace attached, or escalate to a human. This is the same instinct as Part 9's handoff rule — never inherit "it works" on someone's word; re-verify the last step before building on it. A backgrounded agent that hits its session limit mid-task and returns a report is a special hazard here: I've opened a "finished" module to find a function returning three values while its one caller still unpacked two — a ValueError waiting on any success path. The report said done. The acceptance check said otherwise. That gap is the whole reason the gate exists.
Takeaways
Where the time went. Not the tickets — those are quick once the format is muscle memory. It was the hangs: 2h17m on a single dead run before the watchdog existed, and a separate under-specified ticket that hung twice and bounced back five times. Both were costs of silence — a background worker that fails quietly burns wall-clock you never notice you're spending.
Reusable diagnostics. A liveness signal and a completion signal are different things — build the first explicitly, because the second lies by omission when a process hangs. Machine-checkable acceptance is the load-bearing part of any delegation: if you can't write the check, you don't yet understand the task well enough to hand it off. And a report is a claim; the acceptance re-run is the evidence.
The general principle. The commander doesn't step onto the field. Externalize the task into a file that can be verified without you, dispatch it, and gate the report — the same move as externalizing live state, one step earlier in the pipeline.
The dispatch checklist
- Is it an engineering execution ticket with machine-checkable acceptance? If not, keep it.
- Four blocks: goal, steps, acceptance (a shell can decide it), red lines.
- Fresh-AI test: could a context-free agent do it right first try from this file alone?
- Fire it detached, with
-c "mcp_servers={}"and< /dev/null; nevernohup &inside the harness. - Attach a ~5-minute liveness watchdog — a hang won't report itself.
- Report received ≠ done: re-run the acceptance, spot-check a red line, then rule go / re-issue / escalate.
FAQ
- How do you delegate a coding task to an AI executor like Codex?
- Not as a conversation — as a self-contained ticket file. Four blocks: the goal (what 'done' means), the steps, machine-checkable acceptance (exit codes, grep-returns-nothing, a file exists and contains X), and red lines (what not to touch). If a fresh AI with none of your chat history couldn't do it right from the ticket alone, the ticket is underspecified — that's your job to fix, not the executor's.
- Why does a backgrounded Codex run sometimes hang forever?
- Two common causes. One: an MCP transport fails to start (a missing OAuth token, an AuthRequired) and hangs the whole process right after task_started — pass `-c "mcp_servers={}"` so an exec ticket loads no MCP at all. Two: a non-TTY stdin that isn't wired to a pipe blocks — always redirect `< /dev/null`. A hung run never exits, so it never notifies you; attach a ~5-minute log-freshness watchdog.
- Is receiving a completion report from an AI agent the same as the task being done?
- No. The producer shouldn't be the one who certifies its own high-risk work. Treat the report as a claim, not a result: re-run the ticket's acceptance checks yourself, spot-check one red line it claimed not to cross, and only then call it done. A report says 'I think it works'; the acceptance checks say whether it does.
- When should work go to a background executor versus staying in the main session?
- Route by a single question: is this an engineering execution ticket with machine-verifiable acceptance — env setup, a data pipeline, a batch edit, a build chore? If yes, it goes to the executor as a ticket. Judgment, taste, and one-off decisions stay with the orchestrator. A one-line config edit you can verify and roll back in seconds isn't worth a ticket — just do it and check it.
Read next
- 2026-07-11[Dev Workflow] When Your Quota Runs Out Mid-Task: A Live-State Handoff Protocol for Claude Code and Codex
When an AI coding CLI runs out of quota mid-task, what dies isn't your knowledge base — it's the live state trapped in the session. A file-based handoff protocol lets Claude Code and Codex resume each other's half-finished work.
- 2026-07-12[Dev Workflow] Retiring an AI Agent's Memory System: Three Traps When Doctrine Drifts From Runtime
I set out to retire a 'dead' shared-memory folder my knowledge base had replaced. It was writing to itself at 2:30 AM. Three traps in doctrine-vs-runtime drift, and why you map a memory system's topology from the hub, not the spoke.
- 2026-02-26[Dev Workflow] I Made Two AIs Argue. The Disagreements Are the Point.
A custom /debate command that pits Codex CLI against Gemini CLI on architecture, code, and decisions. Different training data, different blind spots — and the disagreements between them are usually the most useful output.
- 2026-05-19[Claude Code] Rules I'd Skip, Hooks I Can't — I Wrote a Hook That Blocks My Own Blog Commits
I had a rule called 'fact-check before publishing.' I still shipped three fabrications. The problem wasn't the rule — it was where I put it. This is how I promoted it from skill to hook: a small script guarding the moment I press 'send,' so I can't even try without verification.
Don't miss the next one
Subscribe, and you won't.
One-click unsubscribe anytime.