I closed a JIRA ticket from the wrong machine and it worked. Merge to develop, push, branch cleanup, JIRA transition, browser verification, all of it, without ever opening a session in the repo where the code actually lived. I drove the whole close from my desktop station over the JIRA REST API and the git CLI, treating the target clone as a thing I poked from the outside.
It worked. It also hurt in seven distinct ways in a single sitting, and by the end of the day that list of papercuts was worth more than the closed ticket it came from.
Here is the reframe that made the day pay off: every one of those frictions is a default waiting to be encoded. The fix is never “be more careful next time.” Careful does not scale and it does not survive a context switch. The fix is to write each correction into the tool as a hardcoded behavior or a halt condition, so the wrong move becomes impossible and you never have to remember the right one.
Let me walk the seven, because each is a real bug class, not a one-off.
Windows has no /tmp
I reached for /tmp/ticket.json to stash the JIRA payload mid-close. On a Linux box that is muscle memory. On Windows it is a path that does not exist, and the write failed. The right location is $TEMP, and the deeper right answer is that a script should never hardcode a tmp path at all. It should ask the OS where temp lives.
This one is trivial and that is the point. The cheapest friction is the one that still costs you a turn every time because it lives in your fingers, not in your tool.
A token-saving filter that lied about state
I run a CLI wrapper that compresses verbose command output to save tokens. On git, curl, and API calls it cuts output 60 to 90 percent, which on a metered plan is real money. It is also structural: it recognizes shapes and summarizes them. When I piped a JIRA REST response through it, it handed my agent a schema sketch, { expand: string[82], fields: { ... } }, instead of the actual field values I needed to verify the transition had taken. The data I was trying to confirm was exactly the data the filter threw away.
The fix was to drop to Python’s urllib for API calls so nothing sat between the agent and the bytes. The rule that fell out: a pretty-printing layer is fine for humans, who read, and a correctness bug for agents, who parse. Anything load-bearing gets the raw bytes.
The same filter hid a merge commit
Right after, I tried to verify HEAD with a compacted git log -1 --oneline, and it returned a non-merge commit. The merge had happened. The tool said otherwise, because its compactor suppressed the merge node as noise. I almost re-ran a merge that was already done.
git rev-parse HEAD gave me the real SHA. Same lesson as the previous one, different blast radius: for SHA verification, payload inspection, anything where you are checking ground truth, the output filter has to be lossless or out of the path entirely.
Heredoc quoting hell
I had inline Python running inside a bash heredoc, and the Python had an f-string, and the f-string had escaped double quotes, and the whole thing was being launched through a Windows shell. That is four layers of quoting, and it died. \" means different things at each layer and they fight.
There is no clever escaping that survives this reliably. The answer is to stop nesting languages in a string. Write the Python to a file and run the file. It parses cleanly every time, and it is the fast path, not the slow one. I now treat “I am about to put code inside a heredoc inside a shell command” as a smell that means: write a script instead.
git branch -d refused to delete, correctly
After the merge I went to clean up the local feature branch with git branch -d and git refused, claiming the branch was not fully merged. It was merged. What git was actually telling me was subtler: my local feature branch carried the develop-into-feature merge commit, and origin’s copy of the feature branch did not. From git’s point of view, deleting the local branch would orphan a commit that existed nowhere else.
The real fix was an ordering rule. Delete the remote branch first, then the local one. Once the remote side is gone there is no asymmetry for git to protect against. This is the kind of thing you “learn” five times before you encode it, because each time the error message sends you hunting for the wrong cause.
A JIRA transition with a hidden four-field screen
The transition I needed to fire was not a simple status flip. It pops a screen with four required fields: a links field, story points, fix version, and sprint. My agent’s instinct was “stop and ask me,” which sounds safe but is actually worse, because it does not know the screen exists until it tries to POST and gets rejected, and by then it is asking about fields it cannot see.
The encoded fix is specific: always fetch the transition with the fields expanded before the POST, then verify every required field is set, then fire. The tool learns the shape of the transition from the API instead of guessing, and “ask” becomes a last resort for genuine ambiguity, not a substitute for reading the screen.
Over-confirmation after one approval
I said “ok, proceed” once. Then the agent asked for a yes or no at every sub-gate after that: before the merge, before the push, before the branch delete, before the transition. One approved operation got chopped into five confirmation prompts, which is exactly the opposite of useful. A close is one decision, not five.
The rule: one approval covers the whole operation. Present the plan once, then run to completion, and only halt on a real surprise, a dirty repo, a missing credential, a ticket already closed, a merge conflict. Re-asking inside an already-approved scope is not caution. It is noise that trains me to stop reading the prompts.
The list is the spec
I did not file the seven as lessons learned. I built a small Python toolkit where each friction became a default or a halt:
- No tmp paths, ask the OS, and no heredocs ever, write Python to a file and run it.
- API calls go through
urllib, never a lossy filter, and SHA checks usegit rev-parse. The raw-subprocess git wrapper carries a comment saying exactly why: scripts need authoritative output. - The develop-into-feature merge order is the only public entry point in the git module, so you cannot do it backwards.
- The transition function always fetches the field screen and verifies before it POSTs.
- The orchestrator presents the plan once and runs to completion, halting only on conditions that are themselves encoded, dirty repo, missing creds, closed ticket, missing branch, unavailable transition, unset required field, merge conflict. On a halt it writes a forensic run-log JSON instead of guessing.
That last part is the difference that matters. A memory is a hope that you will behave. A default in code is a guarantee. A halt condition is the tool failing closed and handing you a forensic record instead of quietly shipping a wrong assumption to production.
On this workflow, I was the QA, the reviewer, and the ops, all at once. Nobody else was going to catch the heredoc that almost broke, or notice that the filter ate the merge commit. The only thing that scales is making the wrong move structurally impossible in the tool, not promising myself I will remember it at 2 PM three weeks from now when I am tired and switching contexts.
The manual workflow was worth doing precisely because it hurt in seven legible, separable, encodable ways. Pain that you can enumerate is a specification. Next time a manual process makes you wince, write down each friction in one line, then classify it as a default the tool should apply automatically or a halt condition it should stop on. Assign each one to the specific script or module that owns that step. Anything you can’t resolve on the spot goes in the run log, not your memory, so it doesn’t quietly disappear.
Related
- Encoding Hard-Won Corrections as Code Defaults Instead of Trusting Yourself to Remember: the same philosophy applied to agent correction loops
- My AI agent’s memory was per-folder, so my four clones never learned from each other: encoding agent knowledge as tracked artifacts rather than ephemeral memory
- Scope Is the Unit, Not Persona: Rethinking How You Load Context Into AI Agents: structuring agent context so friction points become defaults
- The Moment Your Notes Turn Into an Operating System: when manual pain points drive infrastructure decisions
- Teaching the AI to Find the Real Source File: .cursor/rules as a Compiled-Asset Map: encoding workflow friction as a persistent context artifact