[{"data":1,"prerenderedAt":1283},["ShallowReactive",2],{"page:\u002Fblog\u002Fimproving-your-development-workflow-with-ai":3},{"_tag":4,"page":5,"styles":1282},"Ok",{"id":6,"path":7,"stem":8,"extension":9,"title":10,"description":11,"publishedAt":12,"status":13,"aside":14,"tags":15,"newsletter":14,"prose":18,"breadcrumbs":18,"h1":18,"wide":14,"seo":19,"_source":20,"sitemap":21,"body":22},"pages\u002Fblog\u002Fimproving-your-development-workflow-with-ai.md","\u002Fblog\u002Fimproving-your-development-workflow-with-ai","blog\u002Fimproving-your-development-workflow-with-ai","md","Optimizing your development workflow with AI","What I changed when AI agents wrote broken code, shared the same files, and produced more pull requests than I could review.","2026-09-11","unlisted",false,[16,17],"ai","development",true,{"title":10,"description":11},"\u002Fhome\u002Frunner\u002Fwork\u002Fharlanzw.com\u002Fharlanzw.com\u002Fcontent\u002Fblog\u002Fimproving-your-development-workflow-with-ai.md",{"loc":7},{"frontmatter":23,"meta":25,"nodes":65,"toc":1267},{"title":10,"description":11,"publishedAt":12,"status":13,"aside":14,"tags":24,"newsletter":14},[16,17],{"description":26,"toc":27},"I use AI heavily across my open-source projects and Nuxt SEO Pro. For a while, my workflow was pretty simple. Give an agent a task, let it work on main, commit everything and push.",{"title":28,"searchDepth":29,"depth":29,"links":30},"",3,[31,45,55,62],{"id":32,"depth":33,"text":34,"children":35},"can-i-trust-my-agents-code",2,"Can I trust my agent's code?",[36,39,42],{"id":37,"depth":29,"text":38},"can-i-trust-my-agents-code-where-did-the-error-go","Where did the error go?",{"id":40,"depth":29,"text":41},"can-i-trust-my-agents-code-give-failure-a-shape","Give failure a shape",{"id":43,"depth":29,"text":44},"can-i-trust-my-agents-code-more-tests-didnt-give-me-more-confidence","More tests didn't give me more confidence",{"id":46,"depth":33,"text":47,"children":48},"do-my-agents-work-effectively-in-parallel","Do my agents work effectively in parallel?",[49,52],{"id":50,"depth":29,"text":51},"do-my-agents-work-effectively-in-parallel-share-the-package-store","Share the package store",{"id":53,"depth":29,"text":54},"do-my-agents-work-effectively-in-parallel-separate-checkouts-still-meet-at-the-same-files","Separate checkouts still meet at the same files",{"id":56,"depth":33,"text":57,"children":58},"can-i-keep-up-with-what-they-produce","Can I keep up with what they produce?",[59],{"id":60,"depth":29,"text":61},"can-i-keep-up-with-what-they-produce-make-the-review-worth-reading","Make the review worth reading",{"id":63,"depth":33,"text":64},"start-with-the-problem-you-keep-having","Start with the problem you keep having",[66,69,72,75,78,87,90,93,96,99,108,111,114,122,193,198,201,251,254,262,265,268,275,278,280,283,286,289,633,636,639,642,645,647,650,653,656,659,662,665,857,860,862,865,868,876,884,887,917,920,923,925,928,943,946,954,957,965,967,970,973,976,979,1020,1023,1026,1041,1044,1046,1049,1052,1055,1062,1070,1073,1203,1206,1209,1212,1215,1217,1220,1223,1231,1234,1241,1244,1247,1250,1256,1258,1261,1264],[67,68,26],"p",{},[67,70,71],{},"Fast. Also a good way to send bugs to people using your product.",[67,73,74],{},"I knew pull requests would help. But adopting them brought more work: separate environments, conflicts, reviews I had to remember to start, and CI that couldn't keep up.",[67,76,77],{},"Every improvement let the agents do more. Then something else became the problem.",[67,79,80,81,86],{},"It's tempting to look at someone running a room full of agents and start there. I've ended up ",[82,83,85],"a",{"href":84},"\u002Fblog\u002Fbuilding-my-software-factory-on-github","building my own software factory on GitHub",", but most of the useful changes happened before that.",[67,88,89],{},"They came from three questions.",[91,92,34],"h2",{"id":32},[67,94,95],{},"On 28 August I merged an agent-written Stripe integration into Nuxt SEO. It touched 23 files. The checks were green and the description said the agent had implemented and verified it.",[67,97,98],{},"I merged it without reading the code.",[100,101],"article-figure",{"src":102,"display-width":103,"alt":104,"caption":105,"width":106,"height":107},"\u002Fblog\u002Fai-workflow\u002Fstripe-pr.webp","760","Merged Stripe integration PR 636, showing 23 changed files and the agent disclosure","The PR I merged on 28 August. The payment failure below is a separate teaching example.","1792","1492",[67,109,110],{},"The description looked convincing. Checking every claim would have taken me longer than the agent spent writing it.",[112,113,38],"h3",{"id":37},[67,115,116,117,121],{},"One recurring problem was silent error handling. An older version of my admin code turned a failed Stripe balance request into ",[118,119,120],"code",{},"null",":",[123,124,127],"pre",{"language":125,"class":126},"ts","rangi shiki shj-lang-ts",[118,128,130,169,170],{"class":129},"language-ts",[131,132,135,140,141,145,146,149,150,153,154,156,161,166],"span",{"class":133,"style":134},"line","display: inline",[131,136,139],{"class":137,"style":138},"shj-syn-kwd shj-kwd","--shiki-light:#cf222e;--shiki-default:#cf222e;color:#cf222e;--shiki-dark:#ff7b72","const"," balance ",[131,142,144],{"class":143,"style":138},"shj-syn-oper shj-oper","="," ",[131,147,148],{"class":137,"style":138},"await"," stripe",[131,151,152],{"class":143,"style":138},".","balance",[131,155,152],{"class":143,"style":138},[131,157,160],{"class":158,"style":159},"shj-syn-func shj-func","--shiki-light:#8250df;--shiki-default:#8250df;color:#8250df;--shiki-dark:#d2a8ff","retrieve",[131,162,165],{"class":163,"style":164},"shj-syn-bracket shj-bracket","--shiki-light:#57606a;--shiki-default:#57606a;color:#57606a;--shiki-dark:#8b949e","(",[131,167,168],{"class":163,"style":164},")","\n",[131,171,172,173,175,178,180,182,145,184,145,187,191],{"class":133,"style":134},"  ",[131,174,152],{"class":143,"style":138},[131,176,177],{"class":137,"style":138},"catch",[131,179,165],{"class":163,"style":164},[131,181,165],{"class":163,"style":164},[131,183,168],{"class":163,"style":164},[131,185,186],{"class":137,"style":138},"=>",[131,188,120],{"class":189,"style":190},"shj-syn-num shj-num","--shiki-light:#0550ae;--shiki-default:#0550ae;color:#0550ae;--shiki-dark:#79c0ff",[131,192,168],{"class":163,"style":164},[67,194,195,196,152],{},"The request failed, but the cause disappeared. Whatever happened next had to work with ",[118,197,120],{},[67,199,200],{},"Take this simplified payment handler:",[123,202,203],{"language":125,"class":126},[118,204,205,169,213,169,230,169,235,169,241,169,247],{"class":129},[131,206,207,145,210],{"class":133,"style":134},[131,208,209],{"class":137,"style":138},"try",[131,211,212],{"class":163,"style":164},"{",[131,214,172,215,149,217,219,220,222,225,227,228],{"class":133,"style":134},[131,216,148],{"class":137,"style":138},[131,218,152],{"class":143,"style":138},"paymentIntents",[131,221,152],{"class":143,"style":138},[131,223,224],{"class":158,"style":159},"confirm",[131,226,165],{"class":163,"style":164},"paymentIntentId",[131,229,168],{"class":163,"style":164},[131,231,232],{"class":133,"style":134},[131,233,234],{"class":163,"style":164},"}",[131,236,237,145,239],{"class":133,"style":134},[131,238,177],{"class":137,"style":138},[131,240,212],{"class":163,"style":164},[131,242,172,243],{"class":133,"style":134},[131,244,246],{"class":245,"style":164},"shj-syn-cmnt shj-cmnt","\u002F\u002F Payment failed, let the user try again.",[131,248,249],{"class":133,"style":134},[131,250,234],{"class":163,"style":164},[67,252,253],{},"This is an illustration, not the code from that merged PR. The comment sounds reasonable. It doesn't tell the caller what failed or leave anything useful to investigate.",[67,255,256,257,261],{},"There is another detail hiding in this tiny example. A resolved confirmation request can still require customer authentication. The caller needs to inspect the PaymentIntent status. ",[82,258,260],{"href":259},"https:\u002F\u002Fdocs.stripe.com\u002Fapi\u002Fpayment_intents\u002Fconfirm","Stripe's confirmation documentation"," describes those transitions.",[67,263,264],{},"I started by writing down a standing rule: no silent catches. Expected failures should have an explicit result. Unexpected failures should propagate or be reported with enough context to investigate.",[67,266,267],{},"The next agent could still write the same bad code.",[67,269,270,271,274],{},"So I added a lint rule. It catches shapes like ",[118,272,273],{},".catch(() => null)"," and empty catch blocks. A hook runs lint after edits so the agent gets feedback while it is still working.",[67,276,277],{},"The limits matter here. My rule allows comment-only catches, including the illustrated one above. It can reject a code pattern. It can't decide whether a payment failure reached the right person.",[112,279,41],{"id":40},[67,281,282],{},"My TypeScript preferences borrow from Effect, without requiring Effect as a dependency. Expected failures become tagged values that the caller must handle. Unexpected infrastructure failures remain errors.",[67,284,285],{},"For a payment, that means distinguishing a card decline from a service failure. The caller needs to know whether to ask the customer for another payment method or report a problem.",[67,287,288],{},"Here is the same handler with explicit results for a declined card and a rate limit.",[290,291,293,296],"article-disclosure",{"summary":292},"Show the complete error handling",[67,294,295],{},"Illustrative excerpt. The request-bound logger, Stripe client and result helpers are set up outside this block.\nThe PaymentIntent already exists with its payment method configured. A returned payment still needs its status handled.",[297,298,300],"expand",{"width":299},"960",[123,301,302],{"language":125,"class":126},[118,303,304,169,310,169,329,169,334,169,339,169,362,169,375,169,379,169,390,169,406,169,425,169,430,169,458,169,477,169,512,169,516,169,521,169,542,169,557,169,600,169,604,169,609,169,624,169,629],{"class":129},[131,305,306,145,308],{"class":133,"style":134},[131,307,209],{"class":137,"style":138},[131,309,212],{"class":163,"style":164},[131,311,172,312,314,315,145,317,149,319,219,321,323,325,227,327],{"class":133,"style":134},[131,313,139],{"class":137,"style":138}," payment ",[131,316,144],{"class":143,"style":138},[131,318,148],{"class":137,"style":138},[131,320,152],{"class":143,"style":138},[131,322,152],{"class":143,"style":138},[131,324,224],{"class":158,"style":159},[131,326,165],{"class":163,"style":164},[131,328,168],{"class":163,"style":164},[131,330,172,331],{"class":133,"style":134},[131,332,333],{"class":245,"style":164},"\u002F\u002F The caller must still check the returned payment status.",[131,335,172,336],{"class":133,"style":134},[131,337,338],{"class":245,"style":164},"\u002F\u002F A successful request can require customer authentication.",[131,340,341,342,344,347,349,351,352,354,355,357,358,360],{"class":133,"style":134},"  log",[131,343,152],{"class":143,"style":138},[131,345,346],{"class":158,"style":159},"context",[131,348,165],{"class":163,"style":164},[131,350,212],{"class":163,"style":164}," paymentStatus",[131,353,121],{"class":143,"style":138}," payment",[131,356,152],{"class":143,"style":138},"status ",[131,359,234],{"class":163,"style":164},[131,361,168],{"class":163,"style":164},[131,363,172,364,145,367,370,372,373],{"class":133,"style":134},[131,365,366],{"class":137,"style":138},"return",[131,368,369],{"class":158,"style":159},"ok",[131,371,165],{"class":163,"style":164},"payment",[131,374,168],{"class":163,"style":164},[131,376,377],{"class":133,"style":134},[131,378,234],{"class":163,"style":164},[131,380,381,145,383,385,386,145,388],{"class":133,"style":134},[131,382,177],{"class":137,"style":138},[131,384,165],{"class":163,"style":164},"error",[131,387,168],{"class":163,"style":164},[131,389,212],{"class":163,"style":164},[131,391,341,392,394,397,399,404],{"class":133,"style":134},[131,393,152],{"class":143,"style":138},[131,395,396],{"class":158,"style":159},"level",[131,398,165],{"class":163,"style":164},[131,400,403],{"class":401,"style":402},"shj-syn-str shj-str","--shiki-light:#0a3069;--shiki-default:#0a3069;color:#0a3069;--shiki-dark:#a5d6ff","'error'",[131,405,168],{"class":163,"style":164},[131,407,341,408,410,412,414,351,416,145,418,145,421,423],{"class":133,"style":134},[131,409,152],{"class":143,"style":138},[131,411,346],{"class":158,"style":159},[131,413,165],{"class":163,"style":164},[131,415,212],{"class":163,"style":164},[131,417,121],{"class":143,"style":138},[131,419,420],{"class":401,"style":402},"'failed'",[131,422,234],{"class":163,"style":164},[131,424,168],{"class":163,"style":164},[131,426,172,427],{"class":133,"style":134},[131,428,429],{"class":245,"style":164},"\u002F\u002F A declined card becomes a result the caller can handle.",[131,431,172,432,145,435,437,438,145,441,446,448,449,451,454,145,456],{"class":133,"style":134},[131,433,434],{"class":137,"style":138},"if",[131,436,165],{"class":163,"style":164},"error ",[131,439,440],{"class":137,"style":138},"instanceof",[131,442,445],{"class":443,"style":444},"shj-syn-class shj-class","--shiki-light:#953800;--shiki-default:#953800;color:#953800;--shiki-dark:#ffa657","Stripe",[131,447,152],{"class":143,"style":138},"errors",[131,450,152],{"class":143,"style":138},[131,452,453],{"class":443,"style":444},"StripeCardError",[131,455,168],{"class":163,"style":164},[131,457,212],{"class":163,"style":164},[131,459,460,461,463,466,468,471,474,475],{"class":133,"style":134},"    log",[131,462,152],{"class":143,"style":138},[131,464,465],{"class":158,"style":159},"warn",[131,467,165],{"class":163,"style":164},[131,469,470],{"class":401,"style":402},"'payment.declined'",[131,472,473],{"class":143,"style":138},","," error",[131,476,168],{"class":163,"style":164},[131,478,479,480,145,482,485,487,489,490,145,492,145,495,145,498,500,502,503,474,505,507,508,510],{"class":133,"style":134},"    ",[131,481,366],{"class":137,"style":138},[131,483,484],{"class":158,"style":159},"err",[131,486,165],{"class":163,"style":164},[131,488,212],{"class":163,"style":164}," _tag",[131,491,121],{"class":143,"style":138},[131,493,494],{"class":401,"style":402},"'PaymentDeclined'",[131,496,497],{"class":137,"style":138},"as",[131,499,139],{"class":137,"style":138},[131,501,473],{"class":143,"style":138}," code",[131,504,121],{"class":143,"style":138},[131,506,152],{"class":143,"style":138},"code ",[131,509,234],{"class":163,"style":164},[131,511,168],{"class":163,"style":164},[131,513,172,514],{"class":133,"style":134},[131,515,234],{"class":163,"style":164},[131,517,172,518],{"class":133,"style":134},[131,519,520],{"class":245,"style":164},"\u002F\u002F Keep retry information when Stripe provides it.",[131,522,172,523,145,525,437,527,145,529,531,448,533,535,538,145,540],{"class":133,"style":134},[131,524,434],{"class":137,"style":138},[131,526,165],{"class":163,"style":164},[131,528,440],{"class":137,"style":138},[131,530,445],{"class":443,"style":444},[131,532,152],{"class":143,"style":138},[131,534,152],{"class":143,"style":138},[131,536,537],{"class":443,"style":444},"StripeRateLimitError",[131,539,168],{"class":163,"style":164},[131,541,212],{"class":163,"style":164},[131,543,460,544,546,548,550,553,474,555],{"class":133,"style":134},[131,545,152],{"class":143,"style":138},[131,547,465],{"class":158,"style":159},[131,549,165],{"class":163,"style":164},[131,551,552],{"class":401,"style":402},"'payment.rate_limited'",[131,554,473],{"class":143,"style":138},[131,556,168],{"class":163,"style":164},[131,558,479,559,145,561,563,565,489,567,145,569,145,572,145,574,576,578,579,474,581,583,584,587,590,593,145,596,598],{"class":133,"style":134},[131,560,366],{"class":137,"style":138},[131,562,484],{"class":158,"style":159},[131,564,165],{"class":163,"style":164},[131,566,212],{"class":163,"style":164},[131,568,121],{"class":143,"style":138},[131,570,571],{"class":401,"style":402},"'RateLimited'",[131,573,497],{"class":137,"style":138},[131,575,139],{"class":137,"style":138},[131,577,473],{"class":143,"style":138}," retryAfter",[131,580,121],{"class":143,"style":138},[131,582,152],{"class":143,"style":138},"headers",[131,585,586],{"class":143,"style":138},"?.",[131,588,589],{"class":163,"style":164},"[",[131,591,592],{"class":401,"style":402},"'retry-after'",[131,594,595],{"class":163,"style":164},"]",[131,597,234],{"class":163,"style":164},[131,599,168],{"class":163,"style":164},[131,601,172,602],{"class":133,"style":134},[131,603,234],{"class":163,"style":164},[131,605,172,606],{"class":133,"style":134},[131,607,608],{"class":245,"style":164},"\u002F\u002F Record unexpected failures, then let them propagate.",[131,610,341,611,613,615,617,620,474,622],{"class":133,"style":134},[131,612,152],{"class":143,"style":138},[131,614,385],{"class":158,"style":159},[131,616,165],{"class":163,"style":164},[131,618,619],{"class":401,"style":402},"'payment.failed'",[131,621,473],{"class":143,"style":138},[131,623,168],{"class":163,"style":164},[131,625,172,626,474],{"class":133,"style":134},[131,627,628],{"class":137,"style":138},"throw",[131,630,631],{"class":133,"style":134},[131,632,234],{"class":163,"style":164},[67,634,635],{},"I want sensitive fields redacted before errors leave the application. Nuxt SEO has shared logging rules for names, redaction and where events go. That gives each handler something consistent to use.",[67,637,638],{},"Then there is production. Sentry can surface an exception, but some broken journeys never throw. A user may retry several times and give up.",[67,640,641],{},"Logs and usage signals can help find those cases. A drop in completed payments is a reason to investigate. It doesn't tell you whether the cause is a bug, confusing UI or something else.",[67,643,644],{},"By this point, fixing a catch had taken me from an instruction file to production monitoring.",[112,646,44],{"id":43},[67,648,649],{},"Agents are very happy to write tests. Some of mine checked that a file contained a string, or that the source had the structure the agent had just written.",[67,651,652],{},"Those tests gave me more code to maintain. They didn't tell me much about whether the feature worked.",[67,654,655],{},"My testing skill now asks for concrete input, a call to an exported function, and an observable result. For a bug fix, I want to see the test fail before the fix.",[67,657,658],{},"I also use gitignored scratch tests. An agent can write a probe to understand a problem, run it, then throw it away. If it captures behaviour that needs protecting, it belongs in the maintained suite.",[67,660,661],{},"A rank-check bug in Nuxt SEO made that distinction painfully concrete. The data provider's spending limit stopped a request. My code treated the missing result as a missing ranking and emailed a customer about a drop that hadn't been measured.",[67,663,664],{},"This regression test starts with a keyword at position 21. When the fetch returns nothing, the saved position must stay at 21. This is an excerpt from the test, with its fixture helpers left out:",[123,666,667],{"language":125,"class":126},[118,668,669,169,688,169,690,169,709,169,719,169,729,169,740,169,759,169,769,169,775,169,777,169,805,169,830,169,851],{"class":129},[131,670,671,673,674,145,676,145,678,681,683,686],{"class":133,"style":134},[131,672,139],{"class":137,"style":138}," db ",[131,675,144],{"class":143,"style":138},[131,677,148],{"class":137,"style":138},[131,679,680],{"class":158,"style":159},"seed",[131,682,165],{"class":163,"style":164},[131,684,685],{"class":189,"style":190},"21",[131,687,168],{"class":163,"style":164},[131,689],{"class":133,"style":134},[131,691,692,694,695,145,697,145,699,702,704,705,145,707],{"class":133,"style":134},[131,693,139],{"class":137,"style":138}," outcome ",[131,696,144],{"class":143,"style":138},[131,698,148],{"class":137,"style":138},[131,700,701],{"class":158,"style":159},"recordRankCheck",[131,703,165],{"class":163,"style":164},"db",[131,706,473],{"class":143,"style":138},[131,708,212],{"class":163,"style":164},[131,710,711,712,145,714,717],{"class":133,"style":134},"  keywordId",[131,713,121],{"class":143,"style":138},[131,715,716],{"class":189,"style":190},"KEYWORD",[131,718,473],{"class":143,"style":138},[131,720,721,722,145,724,727],{"class":133,"style":134},"  siteUrl",[131,723,121],{"class":143,"style":138},[131,725,726],{"class":401,"style":402},"'https:\u002F\u002Franknm.example'",[131,728,473],{"class":143,"style":138},[131,730,731,732,145,734,736,738],{"class":133,"style":134},"  competitorDomains",[131,733,121],{"class":143,"style":138},[131,735,589],{"class":163,"style":164},[131,737,595],{"class":163,"style":164},[131,739,473],{"class":143,"style":138},[131,741,742,743,145,745,145,748,750,145,752,145,754,757],{"class":133,"style":134},"  fetchSerp",[131,744,121],{"class":143,"style":138},[131,746,747],{"class":137,"style":138},"async",[131,749,165],{"class":163,"style":164},[131,751,168],{"class":163,"style":164},[131,753,186],{"class":137,"style":138},[131,755,756],{"class":189,"style":190},"undefined",[131,758,473],{"class":143,"style":138},[131,760,761,762,145,764,767],{"class":133,"style":134},"  now",[131,763,121],{"class":143,"style":138},[131,765,766],{"class":189,"style":190},"NOW",[131,768,473],{"class":143,"style":138},[131,770,771,773],{"class":133,"style":134},[131,772,234],{"class":163,"style":164},[131,774,168],{"class":163,"style":164},[131,776],{"class":133,"style":134},[131,778,779,782,784,785,787,789,792,794,489,796,145,798,145,801,803],{"class":133,"style":134},[131,780,781],{"class":158,"style":159},"expect",[131,783,165],{"class":163,"style":164},"outcome",[131,786,168],{"class":163,"style":164},[131,788,152],{"class":143,"style":138},[131,790,791],{"class":158,"style":159},"toEqual",[131,793,165],{"class":163,"style":164},[131,795,212],{"class":163,"style":164},[131,797,121],{"class":143,"style":138},[131,799,800],{"class":401,"style":402},"'not_measured'",[131,802,234],{"class":163,"style":164},[131,804,168],{"class":163,"style":164},[131,806,807,809,811,145,813,816,704,818,820,822,824,826,828],{"class":133,"style":134},[131,808,781],{"class":158,"style":159},[131,810,165],{"class":163,"style":164},[131,812,148],{"class":137,"style":138},[131,814,815],{"class":158,"style":159},"snapshots",[131,817,165],{"class":163,"style":164},[131,819,168],{"class":163,"style":164},[131,821,168],{"class":163,"style":164},[131,823,152],{"class":143,"style":138},[131,825,791],{"class":158,"style":159},[131,827,165],{"class":163,"style":164},[131,829,589],{"class":163,"style":164},[131,831,172,832,834,835,145,837,840,842,843,145,845,145,847,849],{"class":133,"style":134},[131,833,212],{"class":163,"style":164}," day",[131,836,121],{"class":143,"style":138},[131,838,839],{"class":401,"style":402},"'2026-08-27'",[131,841,473],{"class":143,"style":138}," position",[131,844,121],{"class":143,"style":138},[131,846,685],{"class":189,"style":190},[131,848,234],{"class":163,"style":164},[131,850,473],{"class":143,"style":138},[131,852,853,855],{"class":133,"style":134},[131,854,595],{"class":163,"style":164},[131,856,168],{"class":163,"style":164},[67,858,859],{},"Checking for a string in the source wouldn't have caught that email. This test checks what the application writes.",[91,861,47],{"id":46},[67,863,864],{},"Asking four agents to work at once is easy. Giving them four usable environments took more effort.",[67,866,867],{},"A branch alone doesn't give an agent a separate directory. A worktree does. Each task gets its own checkout, while the primary checkout stays clean on main.",[100,869],{"src":870,"display-width":871,"alt":872,"caption":873,"width":874,"height":875},"\u002Fblog\u002Fai-workflow\u002Foriginal-worktrees.webp","1100","Worktree diagram showing four agents, shared package files and private task state","Each agent gets a checkout and private task state. Branches and preview names are illustrative.","1808","649",[67,877,878,879,883],{},"I use ",[82,880,882],{"href":881},"https:\u002F\u002Fgithub.com\u002Fmax-sixty\u002Fworktrunk","Worktrunk"," to manage that setup. Before a task starts, the checkout needs its dependencies, local configuration and writable state ready.",[67,885,886],{},"For example, I can start a task from the current main branch like this:",[123,888,891],{"language":889,"class":890},"sh","rangi shiki shj-lang-sh",[118,892,894,169,900,169,909],{"class":893},"language-sh",[131,895,896,899],{"class":133,"style":134},[131,897,898],{"class":158,"style":159},"wt"," switch --create fix\u002Fpayment-errors --base origin\u002Fmain",[131,901,902,145,905,908],{"class":133,"style":134},[131,903,904],{"class":158,"style":159},"pnpm",[131,906,907],{"class":137,"style":138},"install"," --frozen-lockfile",[131,910,911,145,913,916],{"class":133,"style":134},[131,912,904],{"class":158,"style":159},[131,914,915],{"class":137,"style":138},"exec"," nuxt prepare",[67,918,919],{},"Worktrunk creates the checkout. The next two commands prepare that checkout's dependencies and Nuxt files. Project-specific setup still has to provide local configuration and a private database.",[67,921,922],{},"Otherwise, the agent spends the first part of the task fixing its environment. Sometimes it changes application code to compensate for an environment that was wrong to begin with.",[112,924,51],{"id":50},[67,926,927],{},"The useful distinction is between sharing package files and sharing an installed application.",[67,929,930,934,935,938,939,942],{},[82,931,933],{"href":932},"https:\u002F\u002Fpnpm.io\u002Fsymlinked-node-modules-structure","pnpm's package layout"," lets worktrees share package data while keeping a separate dependency graph in each checkout. Each task gets its own ",[118,936,937],{},"node_modules",". Nuxt generates ",[118,940,941],{},".nuxt"," inside that task's checkout too, because generated files can contain paths specific to it.",[67,944,945],{},"Local databases need their own writable state. Sharing a database between two tasks makes it much harder to know what either agent actually tested.",[67,947,948,949,953],{},"There are less interesting details that still make a difference. I use ",[82,950,952],{"href":951},"https:\u002F\u002Fgithub.com\u002Fvercel-labs\u002Fportless","Portless"," for stable local preview names, and named browser pages for each task. I also built a JetBrains worktree plugin so I could see the checkouts in my IDE.",[67,955,956],{},"I needed the separate checkouts to be easy to find, otherwise I'd work around the setup.",[100,958],{"src":959,"display-width":960,"alt":961,"caption":962,"width":963,"height":964},"\u002Fblog\u002Fai-workflow\u002Fworktree-ide.webp","700","JetBrains worktree panel listing task branches and their latest activity","My JetBrains worktree panel. The 86 entries are worktrees, not concurrent agents.","1282","1042",[112,966,54],{"id":53},[67,968,969],{},"Worktrees didn't fix my architecture.",[67,971,972],{},"Independent features still passed through shared configuration and registration points. Two tasks could work perfectly well in isolation, then arrive with competing edits to the same block.",[67,974,975],{},"I had a real pair of Nuxt SEO PRs that both rewrote the sitemap-failure handling in one crawl job. They needed an explicit landing order and a rebase.",[67,977,978],{},"PR #728 added a retry flag. PR #733 changed the event name on the same line. The repository is private, so I've included shortened excerpts to show the overlap:",[297,980,981],{"width":871},[123,982,985],{"language":983,"class":984},"diff","rangi shiki shj-lang-diff",[118,986,988,169,991,169,998,169,1005,169,1007,169,1010,169,1015],{"class":987},"language-diff",[131,989,990],{"class":133,"style":134},"# PR #728",[131,992,993],{"class":133,"style":134},[131,994,997],{"class":995,"style":996},"shj-syn-deleted shj-deleted","--shiki-light:#82071e;--shiki-default:#82071e;color:#82071e;--shiki-dark:#ffa198","- logWarn('crawl_audit.degraded', error, { stage, ...context })",[131,999,1000],{"class":133,"style":134},[131,1001,1004],{"class":1002,"style":1003},"shj-syn-insert shj-insert","--shiki-light:#116329;--shiki-default:#116329;color:#116329;--shiki-dark:#7ee787","+ logWarn('crawl_audit.degraded', error, { stage, ...context, retryable: sitemap.retryable })",[131,1006],{"class":133,"style":134},[131,1008,1009],{"class":133,"style":134},"# PR #733",[131,1011,1012],{"class":133,"style":134},[131,1013,1014],{"class":995,"style":996},"- logWarn('crawl_audit.degraded', error, { stage, ...context, retryable: sitemap.retryable })",[131,1016,1017],{"class":133,"style":134},[131,1018,1019],{"class":1002,"style":1003},"+ logWarn('crawl_audit.target_site_refused', error, { stage, ...context, retryable: sitemap.retryable })",[67,1021,1022],{},"Sometimes that's unavoidable. If it keeps happening, I look at the boundary. Can the feature own its configuration? Can the shared module expose a smaller interface? Are the tasks actually independent?",[67,1024,1025],{},"For related work, stacked PRs can make the dependency explicit. For repeated mechanical edits, I use Ripast to preview and apply structured renames and moves, then check the result. Repeating a more detailed prompt wasn't fixing broken refactors in large Vue files.",[67,1027,1028,1029,1032,1033,1036,1037,1040],{},"The same applies to context. ",[118,1030,1031],{},"VISION.md"," explains what the product is trying to do. ",[118,1034,1035],{},"GLOSSARY.md"," keeps the names consistent. ",[118,1038,1039],{},"DESIGN.md"," gives agents shared UI decisions to follow.",[67,1042,1043],{},"Two agents can use different files and still make incompatible product decisions. Separate directories won't catch that.",[91,1045,57],{"id":56},[67,1047,1048],{},"Eventually the agents could produce work faster than I could land it. You can see the current workload below: agents doing work, PRs still open, and CI jobs running or waiting.",[1050,1051],"article-live-stats",{},[67,1053,1054],{},"Some of the delay was CI. I was paying for repeated setup and waiting for checks on commits I had already replaced.",[100,1056],{"src":1057,"display-width":871,"alt":1058,"caption":1059,"width":1060,"height":1061},"\u002Fblog\u002Fai-workflow\u002Foriginal-ci-ideas.webp","CI diagram showing fast PR checks, cheap checks first, superseded runs and scratch tests","Four ways to reduce CI waiting. A smaller PR gate moves some failure detection until after merge.","1760","560",[67,1063,1064,1065,1069],{},"Lint and type checks can reject a change before an expensive build starts. ",[82,1066,1068],{"href":1067},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions\u002Fconcepts\u002Fworkflows-and-actions\u002Fconcurrency","GitHub Actions concurrency"," can cancel superseded checks for the same PR. A deployment that needs to finish requires a different policy.",[67,1071,1072],{},"For a PR-only validation workflow, this is enough to cancel an older run when a new commit arrives:",[123,1074,1077],{"language":1075,"class":1076},"yaml","rangi shiki shj-lang-yaml",[118,1078,1080,169,1089,169,1097,169,1099,169,1106,169,1127,169,1135,169,1137,169,1144,169,1151,169,1162,169,1169,169,1175,169,1185,169,1194],{"class":1079},"language-yaml",[131,1081,1082,1086,1088],{"class":133,"style":134},[131,1083,1085],{"class":1084,"style":190},"shj-syn-var shj-var","name",[131,1087,121],{"class":143,"style":138}," Validate",[131,1090,1091,1094,1096],{"class":133,"style":134},[131,1092,1093],{"class":1084,"style":190},"on",[131,1095,121],{"class":143,"style":138}," pull_request",[131,1098],{"class":133,"style":134},[131,1100,1101,1104],{"class":133,"style":134},[131,1102,1103],{"class":1084,"style":190},"concurrency",[131,1105,121],{"class":143,"style":138},[131,1107,172,1108,1111,1113,1114,1117,1118,1120,1122,1123,1125],{"class":133,"style":134},[131,1109,1110],{"class":1084,"style":190},"group",[131,1112,121],{"class":143,"style":138}," validate",[131,1115,1116],{"class":143,"style":138},"-","$",[131,1119,212],{"class":163,"style":164},[131,1121,212],{"class":163,"style":164}," github.event.pull_request.number ",[131,1124,234],{"class":163,"style":164},[131,1126,234],{"class":163,"style":164},[131,1128,172,1129,1132,1134],{"class":133,"style":134},[131,1130,1131],{"class":1084,"style":190},"cancel-in-progress",[131,1133,121],{"class":143,"style":138}," true",[131,1136],{"class":133,"style":134},[131,1138,1139,1142],{"class":133,"style":134},[131,1140,1141],{"class":1084,"style":190},"jobs",[131,1143,121],{"class":143,"style":138},[131,1145,172,1146,1149],{"class":133,"style":134},[131,1147,1148],{"class":1084,"style":190},"validate",[131,1150,121],{"class":143,"style":138},[131,1152,479,1153,1156,1158,1159,1161],{"class":133,"style":134},[131,1154,1155],{"class":1084,"style":190},"runs-on",[131,1157,121],{"class":143,"style":138}," ubuntu",[131,1160,1116],{"class":143,"style":138},"latest",[131,1163,479,1164,1167],{"class":133,"style":134},[131,1165,1166],{"class":1084,"style":190},"steps",[131,1168,121],{"class":143,"style":138},[131,1170,1171,1172],{"class":133,"style":134},"      ",[131,1173,1174],{"class":245,"style":164},"# Checkout and install dependencies first.",[131,1176,1171,1177,145,1179,1182,1184],{"class":133,"style":134},[131,1178,1116],{"class":143,"style":138},[131,1180,1181],{"class":1084,"style":190},"run",[131,1183,121],{"class":143,"style":138}," pnpm lint",[131,1186,1171,1187,145,1189,1191,1193],{"class":133,"style":134},[131,1188,1116],{"class":143,"style":138},[131,1190,1181],{"class":1084,"style":190},[131,1192,121],{"class":143,"style":138}," pnpm typecheck",[131,1195,1171,1196,145,1198,1200,1202],{"class":133,"style":134},[131,1197,1116],{"class":143,"style":138},[131,1199,1181],{"class":1084,"style":190},[131,1201,121],{"class":143,"style":138}," pnpm build",[67,1204,1205],{},"This is a configuration excerpt. Checkout and dependency setup depend on the project. The order means a failed lint step stops the job before the build starts.",[67,1207,1208],{},"A smaller PR gate is also a tradeoff. Moving checks until after merge means some failures reach main before you discover them. I need to know what I'm giving up and how those failures get reported.",[67,1210,1211],{},"Caching needs the same scrutiny. I removed pnpm caching from one CI setup because it was slower there. A shared package store on my machine and downloading an archive on a hosted runner have different costs.",[67,1213,1214],{},"But faster CI only solves part of it. My attention is still finite.",[112,1216,61],{"id":60},[67,1218,1219],{},"I want an agent review to try to disprove the change. Check the premise, follow the failure cases, and show what was actually verified.",[67,1221,1222],{},"This review comment shows the actual navigation the agent checked in two layouts. It also leaves a request error visible in one screenshot.",[100,1224],{"src":1225,"display-width":1226,"alt":1227,"caption":1228,"width":1229,"height":1230},"\u002Fblog\u002Fai-workflow\u002Fbrowser-check.webp","1000","Browser verification comment with fleet and one-site navigation screenshots","The one-site screenshot contains a request error. The comment shows what the agent checked and what still needs attention.","1784","1038",[67,1232,1233],{},"A diagram helps when the change crosses several boundaries. This PR shows where the Bing data comes from and where credentials enter the request.",[100,1235],{"src":1236,"display-width":1226,"alt":1237,"caption":1238,"width":1239,"height":1240},"\u002Fblog\u002Fai-workflow\u002Fbing-flow.webp","Bing PR diagram connecting views, Site credentials, bounded requests and the public Bing API","The Bing integration PR. The diagram gives the reviewer a route through the change.","1626","864",[67,1242,1243],{},"A confidence score helps only if it says what remains untested. It doesn't authorize a merge.",[67,1245,1246],{},"With Unhead, I still need to think about framework integrations, breaking changes and bundle overhead. An agent may correctly implement its task while missing a reason I don't want the change at all.",[67,1248,1249],{},"If review is full, another running agent can just produce another waiting PR.",[100,1251],{"src":1252,"display-width":1226,"alt":1253,"caption":1254,"width":1060,"height":1255},"\u002Fblog\u002Fai-workflow\u002Foriginal-review-ideas.webp","Review diagram showing independent reviewers, selective auto-merge and a bounded review queue","Independent review, selective auto-merge and a bounded queue. The services and queue slots are illustrative.","597",[91,1257,64],{"id":63},[67,1259,1260],{},"If you keep correcting the same mistake, write down the rule. If the mistake has a detectable shape, make a check. If two agents keep editing the same block, look at the task split and the architecture.",[67,1262,1263],{},"You can do any of that with one agent and a normal development workflow.",[67,1265,1266],{},"I built the factory when I wanted trusted maintenance work to start without opening my laptop. I still have to decide what is worth doing and what should ship.",{"title":28,"searchDepth":29,"depth":29,"links":1268},[1269,1274,1278,1281],{"id":32,"depth":33,"text":34,"children":1270},[1271,1272,1273],{"id":37,"depth":29,"text":38},{"id":40,"depth":29,"text":41},{"id":43,"depth":29,"text":44},{"id":46,"depth":33,"text":47,"children":1275},[1276,1277],{"id":50,"depth":29,"text":51},{"id":53,"depth":29,"text":54},{"id":56,"depth":33,"text":57,"children":1279},[1280],{"id":60,"depth":29,"text":61},{"id":63,"depth":33,"text":64},[],1789145440043]