[{"data":1,"prerenderedAt":4641},["ShallowReactive",2],{"writing-posts":3},[4,539,2119,2348,4581],{"id":5,"title":6,"body":7,"date":531,"description":532,"extension":533,"meta":534,"navigation":99,"path":535,"seo":536,"stem":537,"__hash__":538},"writing\u002Fwriting\u002F20260524.good-engineers-worse-with-ai.md","Why Good Engineers Become Worse With AI",{"type":8,"value":9,"toc":525},"minimark",[10,18,26,39,42,47,50,57,371,380,391,394,398,408,411,414,418,425,432,435,439,521],[11,12,13,17],"p",{},[14,15,16],"em",{},"The 10x engineer is regressing towards the mean",".",[11,19,20,21,25],{},"Francis Galton named this effect in 1886 ",[22,23],"content-cite",{"n":24},"1",", when he noticed that exceptionally tall parents had children closer to average. LLMs are regression machines by construction. The decoding step samples the most-probable continuation of your prompt. That's the mean of the training distribution, conditioned on what you typed.",[27,28,29,30,29,35],"figure",{},"\n  ",[31,32],"img",{"src":33,"alt":34},"\u002Fwriting\u002F20260524.good-engineers-worse-with-ai\u002Fregression-to-mean.png","Four code samples arranged around a regression curve, converging on a center.",[36,37,38],"figcaption",{},"Regression to the mean. Standard patterns get lifted up to it; novel algorithms get dragged down to it. Same mechanism, opposite outcomes.",[11,40,41],{},"The effect is asymmetric. On common work, the 10x engineer becomes 100x. On novel work, the same engineer gets dragged to the mean and ships code that looks right and isn't. The model doesn't know which one you are.",[43,44,46],"h2",{"id":45},"what-the-failure-looks-like","What the Failure Looks Like",[11,48,49],{},"A docstring describes behavior. You can only specify what you already know.",[11,51,52,53,56],{},"I used a paper from ICML 2026 ",[22,54],{"n":55},"2"," whose contribution is one attention kernel formula. I stripped the implementation and sent DeepSeek V4 Pro the signature and docstring, then captured the logprobs on the completion.",[58,59,64],"pre",{"className":60,"code":61,"language":62,"meta":63,"style":63},"language-python shiki shiki-themes github-dark","import torch\nimport torch.nn.functional as F\n\ndef spherical_attention(Q, K, V):\n    \"\"\"\n    Attention with spherical-constrained Q, K and positive scoring kernel.\n\n    Queries and keys are normalized to the unit sphere. A positive kernel\n    function maps the cosine similarity between query and key directions\n    to an attention score. Scores are normalized per query and used to\n    weight V.\n\n    Args:\n        Q, K, V: (batch, heads, seq, head_dim) tensors.\n\n    Returns:\n        Attention output of shape (batch, heads, seq, head_dim).\n    \"\"\"\n    Q = F.normalize(Q, dim=-1)\n    K = F.normalize(K, dim=-1)\n    S = torch.einsum('bhqd,bhkd->bhqk', Q, K)\n    C = 2.0 + 1e-6\n    S = S**2 \u002F (C - 2*S)                      # Yat-kernel\n    A = S \u002F S.sum(dim=-1, keepdim=True)\n    O = torch.einsum('bhqk,bhkd->bhqd', A, V)\n    return O\n","python","",[65,66,67,80,94,101,114,121,127,132,138,144,150,156,161,167,173,178,184,190,195,220,239,256,273,310,346,362],"code",{"__ignoreMap":63},[68,69,72,76],"span",{"class":70,"line":71},"line",1,[68,73,75],{"class":74},"snl16","import",[68,77,79],{"class":78},"s95oV"," torch\n",[68,81,83,85,88,91],{"class":70,"line":82},2,[68,84,75],{"class":74},[68,86,87],{"class":78}," torch.nn.functional ",[68,89,90],{"class":74},"as",[68,92,93],{"class":78}," F\n",[68,95,97],{"class":70,"line":96},3,[68,98,100],{"emptyLinePlaceholder":99},true,"\n",[68,102,104,107,111],{"class":70,"line":103},4,[68,105,106],{"class":74},"def",[68,108,110],{"class":109},"svObZ"," spherical_attention",[68,112,113],{"class":78},"(Q, K, V):\n",[68,115,117],{"class":70,"line":116},5,[68,118,120],{"class":119},"sU2Wk","    \"\"\"\n",[68,122,124],{"class":70,"line":123},6,[68,125,126],{"class":119},"    Attention with spherical-constrained Q, K and positive scoring kernel.\n",[68,128,130],{"class":70,"line":129},7,[68,131,100],{"emptyLinePlaceholder":99},[68,133,135],{"class":70,"line":134},8,[68,136,137],{"class":119},"    Queries and keys are normalized to the unit sphere. A positive kernel\n",[68,139,141],{"class":70,"line":140},9,[68,142,143],{"class":119},"    function maps the cosine similarity between query and key directions\n",[68,145,147],{"class":70,"line":146},10,[68,148,149],{"class":119},"    to an attention score. Scores are normalized per query and used to\n",[68,151,153],{"class":70,"line":152},11,[68,154,155],{"class":119},"    weight V.\n",[68,157,159],{"class":70,"line":158},12,[68,160,100],{"emptyLinePlaceholder":99},[68,162,164],{"class":70,"line":163},13,[68,165,166],{"class":119},"    Args:\n",[68,168,170],{"class":70,"line":169},14,[68,171,172],{"class":119},"        Q, K, V: (batch, heads, seq, head_dim) tensors.\n",[68,174,176],{"class":70,"line":175},15,[68,177,100],{"emptyLinePlaceholder":99},[68,179,181],{"class":70,"line":180},16,[68,182,183],{"class":119},"    Returns:\n",[68,185,187],{"class":70,"line":186},17,[68,188,189],{"class":119},"        Attention output of shape (batch, heads, seq, head_dim).\n",[68,191,193],{"class":70,"line":192},18,[68,194,120],{"class":119},[68,196,198,201,204,207,211,214,217],{"class":70,"line":197},19,[68,199,200],{"class":78},"    Q ",[68,202,203],{"class":74},"=",[68,205,206],{"class":78}," F.normalize(Q, ",[68,208,210],{"class":209},"s9osk","dim",[68,212,213],{"class":74},"=-",[68,215,24],{"class":216},"sDLfK",[68,218,219],{"class":78},")\n",[68,221,223,226,228,231,233,235,237],{"class":70,"line":222},20,[68,224,225],{"class":78},"    K ",[68,227,203],{"class":74},[68,229,230],{"class":78}," F.normalize(K, ",[68,232,210],{"class":209},[68,234,213],{"class":74},[68,236,24],{"class":216},[68,238,219],{"class":78},[68,240,242,245,247,250,253],{"class":70,"line":241},21,[68,243,244],{"class":78},"    S ",[68,246,203],{"class":74},[68,248,249],{"class":78}," torch.einsum(",[68,251,252],{"class":119},"'bhqd,bhkd->bhqk'",[68,254,255],{"class":78},", Q, K)\n",[68,257,259,262,264,267,270],{"class":70,"line":258},22,[68,260,261],{"class":78},"    C ",[68,263,203],{"class":74},[68,265,266],{"class":216}," 2.0",[68,268,269],{"class":74}," +",[68,271,272],{"class":216}," 1e-6\n",[68,274,276,278,280,283,286,288,291,294,297,300,303,306],{"class":70,"line":275},23,[68,277,244],{"class":78},[68,279,203],{"class":74},[68,281,282],{"class":78}," S",[68,284,285],{"class":74},"**",[68,287,55],{"class":216},[68,289,290],{"class":74}," \u002F",[68,292,293],{"class":78}," (C ",[68,295,296],{"class":74},"-",[68,298,299],{"class":216}," 2",[68,301,302],{"class":74},"*",[68,304,305],{"class":78},"S)                      ",[68,307,309],{"class":308},"sAwPA","# Yat-kernel\n",[68,311,313,316,318,321,324,327,329,331,333,336,339,341,344],{"class":70,"line":312},24,[68,314,315],{"class":78},"    A ",[68,317,203],{"class":74},[68,319,320],{"class":78}," S ",[68,322,323],{"class":74},"\u002F",[68,325,326],{"class":78}," S.sum(",[68,328,210],{"class":209},[68,330,213],{"class":74},[68,332,24],{"class":216},[68,334,335],{"class":78},", ",[68,337,338],{"class":209},"keepdim",[68,340,203],{"class":74},[68,342,343],{"class":216},"True",[68,345,219],{"class":78},[68,347,349,352,354,356,359],{"class":70,"line":348},25,[68,350,351],{"class":78},"    O ",[68,353,203],{"class":74},[68,355,249],{"class":78},[68,357,358],{"class":119},"'bhqk,bhkd->bhqd'",[68,360,361],{"class":78},", A, V)\n",[68,363,365,368],{"class":70,"line":364},26,[68,366,367],{"class":74},"    return",[68,369,370],{"class":78}," O\n",[27,372,29,373,29,377],{},[31,374],{"src":375,"alt":376},"\u002Fwriting\u002F20260524.good-engineers-worse-with-ai\u002Fcode-completion.svg","Model completion with per-token uncertainty heatmap. The kernel line torch.relu(S) + 1e-6 is highlighted red.",[36,378,379],{},"The model's completion. Deeper red indicates lower confidence.",[11,381,382,383,386,387,390],{},"Seven identical lines. One different: where the paper writes ",[65,384,385],{},"S**2 \u002F (C - 2*S)"," (the Yat-kernel, the paper's contribution), the model wrote ",[65,388,389],{},"torch.relu(S) + 1e-6",". The model was sampling from the common positive functions: ReLU, softplus, exp. The Yat-kernel wasn't in the candidate set.",[11,392,393],{},"The model gets it right when given the formula. Know the formula and you don't need the model. Structurally correct code with the wrong formula on the load-bearing line.",[43,395,397],{"id":396},"where-it-doesnt-fail","Where It Doesn't Fail",[11,399,400,401,404,405,17],{},"In May 2026, OpenAI's reasoning model disproved the Erdős unit distance conjecture ",[22,402],{"n":403},"4",", a combinatorics problem open since 1946. DeepMind's AlphaProof Nexus solved nine of the 353 open Erdős problems the same week ",[22,406],{"n":407},"5",[11,409,410],{},"Both used the same structure: the model generates candidate constructions; Lean, a formal proof checker, verifies each one. A proof compiles or it doesn't. What looks like AI solving novel mathematics is search over a space with a ground-truth oracle.",[11,412,413],{},"The kernel experiment has no oracle. The model generated one completion, nothing verified it, and the most probable token was ReLU. The logprobs show uncertainty on that line; the model knew it was in the tail. But uncertainty with no verifier downstream collapses into the modal token.",[43,415,417],{"id":416},"whats-permanent","What's Permanent",[11,419,420,421,424],{},"You might expect this to fix itself: publish the paper, the next model trains on it, the gap closes. Some of it does. But the frontier always sits past the cutoff, and the highest-value work never publishes at all. HFT pricing logic, FAANG infrastructure, bank risk systems stay behind corporate firewalls ",[22,422],{"n":423},"6",". There's always a tail, and the best engineers work in it.",[11,426,427,428,431],{},"Rarity is the diagnostic. Standard application code sits near the center of the distribution, and the model lifts it. Rare patterns sit in the tail, where models underlearn them ",[22,429],{"n":430},"3"," and produce something same-shape and confidently wrong.",[11,433,434],{},"Engineers who stay sharp know which lines carry the contribution. The model doesn't know. If you've been delegating the judgment of which lines matter, you're the one regressing.",[43,436,438],{"id":437},"references","References",[440,441,442],"content-references",{},[443,444,445,462,474,486,498,509],"ol",{},[446,447,448,449,458,459,17],"li",{},"Wikipedia. ",[450,451,457],"a",{"href":452,"rel":453,"target":456},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRegression_toward_the_mean#Discovery",[454,455],"noopener","noreferrer","_blank","Regression toward the mean | Discovery",". ",[14,460,461],{},"Wikipedia",[446,463,464,465,458,470,473],{},"Luna, Bouhsine, and Choromanski. ",[450,466,469],{"href":467,"rel":468,"target":456},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2602.04915",[454,455],"SLAY: Geometry-Aware Spherical Linearized Attention with Yat-Kernel",[14,471,472],{},"arXiv:2602.04915",", 2026. ICML 2026.",[446,475,476,477,458,482,485],{},"Kandpal et al. ",[450,478,481],{"href":479,"rel":480,"target":456},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2211.08411",[454,455],"Large Language Models Struggle to Learn Long-Tail Knowledge",[14,483,484],{},"arXiv:2211.08411",", 2023. ICML 2023.",[446,487,488,489,458,494,497],{},"OpenAI. ",[450,490,493],{"href":491,"rel":492,"target":456},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2605.20695",[454,455],"Remarks on the Disproof of the Unit Distance Conjecture",[14,495,496],{},"arXiv:2605.20695",", 2026.",[446,499,500,501,458,506,497],{},"Google DeepMind. ",[450,502,505],{"href":503,"rel":504,"target":456},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2605.22763",[454,455],"AlphaProof Nexus",[14,507,508],{},"arXiv:2605.22763",[446,510,511,512,458,517,520],{},"Ahmed et al. ",[450,513,516],{"href":514,"rel":515,"target":456},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2402.15100",[454,455],"Studying LLM Performance on Closed- and Open-source Data",[14,518,519],{},"arXiv:2402.15100",", 2024.",[522,523,524],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":63,"searchDepth":82,"depth":82,"links":526},[527,528,529,530],{"id":45,"depth":82,"text":46},{"id":396,"depth":82,"text":397},{"id":416,"depth":82,"text":417},{"id":437,"depth":82,"text":438},"2026-05-24","AI coding tools are mean-seeking by construction. The effect runs in opposite directions depending on where you started.","md",{},"\u002Fwriting\u002Fgood-engineers-worse-with-ai",{"title":6,"description":532},"writing\u002F20260524.good-engineers-worse-with-ai","GpT40YcFUh-oQFITHIj82FW0vXbN3ShunsDg3XgALSo",{"id":540,"title":541,"body":542,"date":2112,"description":2113,"extension":533,"meta":2114,"navigation":99,"path":2115,"seo":2116,"stem":2117,"__hash__":2118},"writing\u002Fwriting\u002F20260510.runtime-subagents-orchestration.md","Runtime Subagents: Orchestration as Code",{"type":8,"value":543,"toc":2102},[544,564,567,573,576,590,594,597,613,629,839,842,846,861,866,877,952,965,971,992,997,1001,1020,1025,1031,1035,1041,1048,1069,1075,1078,1086,1100,1932,1936,1942,1986,1994,2000,2006,2009,2019,2023,2054,2087,2093,2099],[545,546,547],"content-alert",{},[11,548,549,553,554,559,560,17],{},[550,551,552],"strong",{},"June 2, 2026:"," Anthropic recently shipped ",[450,555,558],{"href":556,"rel":557,"target":456},"https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fworkflows",[454,455],"Dynamic Workflows"," in Claude Code, built on the same core idea as this post. I added a ",[450,561,563],{"href":562},"#the-pattern-converged","section on the parallels",[11,565,566],{},"I often ask coding agents to do work that sounds like this:",[568,569,570],"blockquote",{},[11,571,572],{},"Check each of these five areas in parallel. Follow up on anything that looks risky. Compare the results, and make a recommendation.",[11,574,575],{},"There is a fan-out. There is a join. There is a conditional second pass. There is aggregation at the end. If I describe that in prose, the model has to hold the whole orchestration plan in its head while it is also reading code, choosing tools, tracking partial results, deciding what to launch next, and remembering how the branches relate to each other.",[11,577,578,579,335,582,585,586,589],{},"The point of runtime subagents is to make that orchestration explicit: give the model ",[65,580,581],{},"run()",[65,583,584],{},"join()",", and ",[65,587,588],{},"cancel()",", and let it write the workflow as JavaScript.",[43,591,593],{"id":592},"prose-is-the-wrong-runtime","Prose Is The Wrong Runtime",[11,595,596],{},"Ask a model to narrate a ten-branch, two-wave, race-and-cancel workflow and it will try. But prose is just a bad medium for concurrent control flow. Code has already solved this, and the model knows code.",[11,598,599,600,605,606,335,609,612],{},"Cloudflare's ",[450,601,604],{"href":602,"rel":603,"target":456},"https:\u002F\u002Fblog.cloudflare.com\u002Fcode-mode\u002F",[454,455],"Code Mode"," made the same observation at the tool layer: a model writes code more naturally than it calls tools. They know ",[65,607,608],{},"Promise.all",[65,610,611],{},"async\u002Fawait",", typed interfaces; the tool-call format is comparatively synthetic. Their fix was to convert the MCP schema into a TypeScript API and let the model write code.",[11,614,615,616,618,619,621,622,624,625,628],{},"The same argument extends to orchestration. The model writes an async body with three primitives: ",[65,617,581],{}," to spawn a child, ",[65,620,584],{}," to collect its result, and ",[65,623,588],{}," to stop work. Once ",[65,626,627],{},"run"," is non-blocking, basic fan-out is just JavaScript:",[58,630,634],{"className":631,"code":632,"language":633,"meta":63,"style":63},"language-js shiki shiki-themes github-dark","const agents = await Promise.all([\n  run({ prompt: \"Find security risks\" }),\n  run({ prompt: \"Find database risks\" }),\n  run({ prompt: \"Find frontend risks\" }),\n]);\n\ntry {\n  \u002F\u002F Collect all outputs within a strict 15-second deadline\n  const pendingJoins = agents.map(a => join(a.id, { timeout: 15000 }));\n  return await Promise.all(pendingJoins);\n} catch (timeout) {\n  \u002F\u002F Kill all branches past deadline to save tokens\n  await Promise.all(agents.map(a => cancel(a.id)));\n  throw new Error(\"Scan timed out.\");\n}\n","js",[65,635,636,661,675,686,697,702,706,714,719,755,771,782,787,815,834],{"__ignoreMap":63},[68,637,638,641,644,647,650,653,655,658],{"class":70,"line":71},[68,639,640],{"class":74},"const",[68,642,643],{"class":216}," agents",[68,645,646],{"class":74}," =",[68,648,649],{"class":74}," await",[68,651,652],{"class":216}," Promise",[68,654,17],{"class":78},[68,656,657],{"class":109},"all",[68,659,660],{"class":78},"([\n",[68,662,663,666,669,672],{"class":70,"line":82},[68,664,665],{"class":109},"  run",[68,667,668],{"class":78},"({ prompt: ",[68,670,671],{"class":119},"\"Find security risks\"",[68,673,674],{"class":78}," }),\n",[68,676,677,679,681,684],{"class":70,"line":96},[68,678,665],{"class":109},[68,680,668],{"class":78},[68,682,683],{"class":119},"\"Find database risks\"",[68,685,674],{"class":78},[68,687,688,690,692,695],{"class":70,"line":103},[68,689,665],{"class":109},[68,691,668],{"class":78},[68,693,694],{"class":119},"\"Find frontend risks\"",[68,696,674],{"class":78},[68,698,699],{"class":70,"line":116},[68,700,701],{"class":78},"]);\n",[68,703,704],{"class":70,"line":123},[68,705,100],{"emptyLinePlaceholder":99},[68,707,708,711],{"class":70,"line":129},[68,709,710],{"class":74},"try",[68,712,713],{"class":78}," {\n",[68,715,716],{"class":70,"line":134},[68,717,718],{"class":308},"  \u002F\u002F Collect all outputs within a strict 15-second deadline\n",[68,720,721,724,727,729,732,735,738,740,743,746,749,752],{"class":70,"line":140},[68,722,723],{"class":74},"  const",[68,725,726],{"class":216}," pendingJoins",[68,728,646],{"class":74},[68,730,731],{"class":78}," agents.",[68,733,734],{"class":109},"map",[68,736,737],{"class":78},"(",[68,739,450],{"class":209},[68,741,742],{"class":74}," =>",[68,744,745],{"class":109}," join",[68,747,748],{"class":78},"(a.id, { timeout: ",[68,750,751],{"class":216},"15000",[68,753,754],{"class":78}," }));\n",[68,756,757,760,762,764,766,768],{"class":70,"line":146},[68,758,759],{"class":74},"  return",[68,761,649],{"class":74},[68,763,652],{"class":216},[68,765,17],{"class":78},[68,767,657],{"class":109},[68,769,770],{"class":78},"(pendingJoins);\n",[68,772,773,776,779],{"class":70,"line":152},[68,774,775],{"class":78},"} ",[68,777,778],{"class":74},"catch",[68,780,781],{"class":78}," (timeout) {\n",[68,783,784],{"class":70,"line":158},[68,785,786],{"class":308},"  \u002F\u002F Kill all branches past deadline to save tokens\n",[68,788,789,792,794,796,798,801,803,805,807,809,812],{"class":70,"line":163},[68,790,791],{"class":74},"  await",[68,793,652],{"class":216},[68,795,17],{"class":78},[68,797,657],{"class":109},[68,799,800],{"class":78},"(agents.",[68,802,734],{"class":109},[68,804,737],{"class":78},[68,806,450],{"class":209},[68,808,742],{"class":74},[68,810,811],{"class":109}," cancel",[68,813,814],{"class":78},"(a.id)));\n",[68,816,817,820,823,826,828,831],{"class":70,"line":169},[68,818,819],{"class":74},"  throw",[68,821,822],{"class":74}," new",[68,824,825],{"class":109}," Error",[68,827,737],{"class":78},[68,829,830],{"class":119},"\"Scan timed out.\"",[68,832,833],{"class":78},");\n",[68,835,836],{"class":70,"line":175},[68,837,838],{"class":78},"}\n",[11,840,841],{},"The plan is now in code and not working memory. Each branch runs in an isolated context. The parent collects clean outputs instead of absorbing every intermediate token from every child.",[43,843,845],{"id":844},"the-tiny-api","The Tiny API",[11,847,848,849,854,855,860],{},"I tested this in ",[450,850,853],{"href":851,"rel":852,"target":456},"https:\u002F\u002Fpi.dev\u002F",[454,455],"Pi",", a minimal agent-harness, similar to Claude Code. It's intentionally small, inspectable, and easy to extend (Mario Zechner's ",[450,856,859],{"href":857,"rel":858,"target":456},"https:\u002F\u002Fmariozechner.at\u002Fposts\u002F2025-11-30-pi-coding-agent\u002F",[454,455],"design post"," covers the architecture). This extensibility makes it a good place to try my weird orchestration idea.",[862,863,865],"h3",{"id":864},"the-extension","The Extension",[11,867,868,869,872,873,876],{},"The extension (called ",[65,870,871],{},"pi-dispatch"," hereafter) registers a single tool that accepts a code string. The model writes an async JavaScript body, and the runtime evaluates it with one injected object, ",[65,874,875],{},"sa",", which exposes five methods:",[58,878,880],{"className":631,"code":879,"language":633,"meta":63,"style":63},"sa.run({ prompt, model?, ...})   \u002F\u002F -> { id }\nsa.join(id)                      \u002F\u002F -> { ..., output?, error? }\nsa.cancel(id)                    \u002F\u002F kill a running child\nsa.status(id)                    \u002F\u002F inspect one run\nsa.list()                        \u002F\u002F inspect all runs\n",[65,881,882,901,914,927,939],{"__ignoreMap":63},[68,883,884,887,889,892,895,898],{"class":70,"line":71},[68,885,886],{"class":78},"sa.",[68,888,627],{"class":109},[68,890,891],{"class":78},"({ prompt, model?, ",[68,893,894],{"class":74},"...",[68,896,897],{"class":78},"})   ",[68,899,900],{"class":308},"\u002F\u002F -> { id }\n",[68,902,903,905,908,911],{"class":70,"line":82},[68,904,886],{"class":78},[68,906,907],{"class":109},"join",[68,909,910],{"class":78},"(id)                      ",[68,912,913],{"class":308},"\u002F\u002F -> { ..., output?, error? }\n",[68,915,916,918,921,924],{"class":70,"line":96},[68,917,886],{"class":78},[68,919,920],{"class":109},"cancel",[68,922,923],{"class":78},"(id)                    ",[68,925,926],{"class":308},"\u002F\u002F kill a running child\n",[68,928,929,931,934,936],{"class":70,"line":103},[68,930,886],{"class":78},[68,932,933],{"class":109},"status",[68,935,923],{"class":78},[68,937,938],{"class":308},"\u002F\u002F inspect one run\n",[68,940,941,943,946,949],{"class":70,"line":116},[68,942,886],{"class":78},[68,944,945],{"class":109},"list",[68,947,948],{"class":78},"()                        ",[68,950,951],{"class":308},"\u002F\u002F inspect all runs\n",[11,953,954,955,958,959,961,962,964],{},"When ",[65,956,957],{},"sa.run(spec)"," fires, it launches a child Pi process in JSON-event mode. The parent reads that process's stdout, tracks the events, and resolves a handle when the child exits cleanly. ",[65,960,627],{}," is non-blocking by design. That's what makes ",[65,963,608],{}," over a set of handles ordinary JavaScript rather than a special case.",[11,966,967,970],{},[65,968,969],{},"sa.cancel"," is the primitive that changes what the parent can express. Killing a running process is a small thing to implement, but without it, a parallel dispatch is just fan-out and wait. With it, the model can write a race: launch several approaches, take the first answer that comes back good, and stop paying tokens on everything else.",[11,972,973,974,977,978,980,981,984,985,335,988,991],{},"The orchestration body runs inside a ",[65,975,976],{},"new AsyncFunction(\"sa\", code)"," call, which means ",[65,979,875],{}," is the only named parameter and ",[65,982,983],{},"await"," works natively throughout. The scoping is deliberate but not a security boundary: the model's code simply has no access to ",[65,986,987],{},"require",[65,989,990],{},"process",", or anything else from the surrounding extension, just the one interface it actually needs.",[11,993,994],{},[14,995,996],{},"Five methods and a code string. That's the whole runtime.",[43,998,1000],{"id":999},"why-not-just-use-the-built-in-subagent-tools","Why Not Just Use The Built-In Subagent Tools?",[11,1002,1003,1004,1007,1008,1011,1012,1015,1016,1019],{},"Both Claude & Codex provide the pieces: an ",[65,1005,1006],{},"Agent"," tool for spawning subagents, ",[65,1009,1010],{},"run_in_background"," for concurrent dispatch, ",[65,1013,1014],{},"TaskStop"," to kill a running agent, ",[65,1017,1018],{},"Monitor"," to watch process output. You could approximate a race-and-cancel: spin up background agents, monitor their output, stop the losers when one wins. The right operations exist.",[11,1021,1022],{},[14,1023,1024],{},"But look at what that requires.",[11,1026,1027,1028,1030],{},"Between each step, the parent has to hold the orchestration state as running text between turns: read what the monitor surfaced, decide which branch won, call ",[65,1029,1014],{},", reconcile the result. The workflow isn't written anywhere. It lives as working memory across turns. If a branch fails at step four, there is no checkpoint.",[43,1032,1034],{"id":1033},"benchmark-same-task-two-harnesses","Benchmark: Same Task, Two Harnesses",[11,1036,1037,1038,1040],{},"Codex is the control: native tool-call orchestration, no external runtime. ",[65,1039,871],{}," is the runtime harness: the model writes JS, fan-out and join happen server-side, only the final return comes back to the parent. The variable is how much context the parent holds while the orchestration runs.",[11,1042,1043,1044,1047],{},"We send the same task to both harnesses, running ",[65,1045,1046],{},"gpt-5.5:medium",": evaluate five strategies in parallel, stop early on hard blockers, and synthesize a ranked recommendation from the strategies that survive.",[1049,1050,1052],"content-collapsible",{"title":1051},"Full Prompt",[568,1053,1054,1057,1060,1063,1066],{},[11,1055,1056],{},"You are orchestrating an analysis. Spawn five concurrent subagents (one per strategy) and synthesize their results into a ranked recommendation.",[11,1058,1059],{},"Context: an engineering team needs a query-answering assistant for their monorepo. The codebase is 500k tokens across approximately 3k files. The constraints are fixed: the available model has a 200k-token context window, every answer must cite exact file paths and line numbers (approximate or summarized citations disqualify the answer), query latency must be ≤ 30 seconds p95, and cost must be ≤ $0.10 per query at 10k queries\u002Fday. Pricing: $3\u002FM input tokens, $15\u002FM output tokens, $0.02\u002FM tokens for embeddings.",[11,1061,1062],{},"The five strategies, one subagent per strategy: RAG (chunk-embed-retrieve), map-reduce summarization, long-context single-shot, hierarchical two-stage retrieval, agentic search (grep and file tools).",[11,1064,1065],{},"Each subagent begins with a feasibility check. If a strategy has a fatal blocker (a hard constraint violation, not a risk or caveat), it reports the blocker and stops. Otherwise it elaborates a full six-section analysis covering implementation, failure modes, rollout plan, latency and cost projections, fallback strategy, and final recommendation.",[11,1067,1068],{},"Once subagents complete, synthesize a ranked recommendation from the surviving analyses.",[11,1070,1071,1072,1074],{},"The ",[65,1073,871],{}," orchestration follows a small shape: fan out five runs, join them, parse one verdict line from each result, and synthesize only the survivors. The interesting part is the contract the parent creates with its children.",[11,1076,1077],{},"Each subagent is told to end with exactly one machine-readable line:",[58,1079,1084],{"className":1080,"code":1082,"language":1083,"meta":63},[1081],"language-text","FINAL_VERDICT: BLOCKED\nor\nFINAL_VERDICT: PROCEED\n","text",[65,1085,1082],{"__ignoreMap":63},[11,1087,1088,1089,1092,1093,1096,1097,1099],{},"The parent parses that line, splits the results into ",[65,1090,1091],{},"blocked"," and ",[65,1094,1095],{},"surviving",", then sends only the surviving analyses into the synthesis pass. Codex arrives at the same split by reading prose output and deciding what sounds blocked. ",[65,1098,871],{}," makes the split deterministic code.",[1049,1101,1103],{"title":1102},"Full Orchestration Body",[58,1104,1106],{"className":631,"code":1105,"language":633,"meta":63,"style":63},"const strategies = [\n  \"RAG (chunk-embed-retrieve)\",\n  \"map-reduce summarization\",\n  \"long-context single-shot\",\n  \"hierarchical two-stage retrieval\",\n  \"agentic search (grep and file tools)\"\n];\n\nconst basePrompt = `\nEvaluate this strategy for a monorepo query-answering assistant.\n\nContext:\n- Codebase: 500,000 tokens across ~3,000 files\n- Model context: 200,000 tokens\n- Answers must cite exact file paths and line numbers\n- Latency: \u003C= 30s p95\n- Cost: \u003C= $0.10\u002Fquery at 10,000 queries\u002Fday\n- Pricing: $3\u002FM input tokens, $15\u002FM output tokens, $0.02\u002FM embeddings\n\nStrategy: {{STRATEGY}}\n\nBegin with a feasibility check.\nIf there is a fatal hard-constraint blocker, explain it and stop.\nOtherwise provide:\n1. Implementation\n2. Failure modes\n3. Rollout plan\n4. Latency and cost projections\n5. Fallback strategy\n6. Final recommendation\n\nEnd with exactly one line:\nFINAL_VERDICT: BLOCKED\nor\nFINAL_VERDICT: PROCEED\n`.trim();\n\nconst runs = await Promise.all(\n  strategies.map(strategy =>\n    sa.run({\n      prompt: basePrompt.replace(\"{{STRATEGY}}\", strategy)\n    })\n  )\n);\n\nconst results = await Promise.all(runs.map(r => sa.join(r.id)));\n\nconst analyses = results.map((r, i) => {\n  const output = r.output || \"\";\n  const verdict = output.match(\n    \u002FFINAL_VERDICT:\\s*(BLOCKED|PROCEED)\\b\u002Fi\n  )?.[1]?.toUpperCase() ||\"BLOCKED\";\n\n  return { strategy: strategies[i], verdict, output };\n});\n\nconst blocked = analyses.filter(a => a.verdict === \"BLOCKED\");\nconst surviving = analyses.filter(a => a.verdict === \"PROCEED\");\n\nconst synthPrompt = `\nSynthesize a ranked recommendation.\n\nRank only surviving strategies.\nList blocked strategies separately with their disqualifying constraint.\n\nSurviving:\n${surviving.map(a => `\\n## ${a.strategy}\\n${a.output}`).join(\"\\n\")}\n\nBlocked:\n${blocked.map(a => `\\n## ${a.strategy}\\n${a.output}`).join(\"\\n\")}\n`.trim();\n\nconst synthRun = await sa.run({ prompt: synthPrompt });\nconst synth = await sa.join(synthRun.id);\n\nreturn synth.output;\n",[65,1107,1108,1120,1128,1135,1142,1149,1154,1159,1163,1175,1180,1184,1189,1194,1199,1204,1209,1214,1219,1223,1228,1232,1237,1242,1247,1252,1257,1263,1269,1275,1281,1286,1292,1298,1304,1310,1324,1329,1350,1366,1377,1394,1400,1406,1411,1416,1454,1459,1492,1514,1532,1564,1588,1593,1601,1607,1612,1645,1674,1679,1691,1697,1702,1708,1714,1719,1725,1794,1799,1805,1864,1875,1880,1899,1918,1923],{"__ignoreMap":63},[68,1109,1110,1112,1115,1117],{"class":70,"line":71},[68,1111,640],{"class":74},[68,1113,1114],{"class":216}," strategies",[68,1116,646],{"class":74},[68,1118,1119],{"class":78}," [\n",[68,1121,1122,1125],{"class":70,"line":82},[68,1123,1124],{"class":119},"  \"RAG (chunk-embed-retrieve)\"",[68,1126,1127],{"class":78},",\n",[68,1129,1130,1133],{"class":70,"line":96},[68,1131,1132],{"class":119},"  \"map-reduce summarization\"",[68,1134,1127],{"class":78},[68,1136,1137,1140],{"class":70,"line":103},[68,1138,1139],{"class":119},"  \"long-context single-shot\"",[68,1141,1127],{"class":78},[68,1143,1144,1147],{"class":70,"line":116},[68,1145,1146],{"class":119},"  \"hierarchical two-stage retrieval\"",[68,1148,1127],{"class":78},[68,1150,1151],{"class":70,"line":123},[68,1152,1153],{"class":119},"  \"agentic search (grep and file tools)\"\n",[68,1155,1156],{"class":70,"line":129},[68,1157,1158],{"class":78},"];\n",[68,1160,1161],{"class":70,"line":134},[68,1162,100],{"emptyLinePlaceholder":99},[68,1164,1165,1167,1170,1172],{"class":70,"line":140},[68,1166,640],{"class":74},[68,1168,1169],{"class":216}," basePrompt",[68,1171,646],{"class":74},[68,1173,1174],{"class":119}," `\n",[68,1176,1177],{"class":70,"line":146},[68,1178,1179],{"class":119},"Evaluate this strategy for a monorepo query-answering assistant.\n",[68,1181,1182],{"class":70,"line":152},[68,1183,100],{"emptyLinePlaceholder":99},[68,1185,1186],{"class":70,"line":158},[68,1187,1188],{"class":119},"Context:\n",[68,1190,1191],{"class":70,"line":163},[68,1192,1193],{"class":119},"- Codebase: 500,000 tokens across ~3,000 files\n",[68,1195,1196],{"class":70,"line":169},[68,1197,1198],{"class":119},"- Model context: 200,000 tokens\n",[68,1200,1201],{"class":70,"line":175},[68,1202,1203],{"class":119},"- Answers must cite exact file paths and line numbers\n",[68,1205,1206],{"class":70,"line":180},[68,1207,1208],{"class":119},"- Latency: \u003C= 30s p95\n",[68,1210,1211],{"class":70,"line":186},[68,1212,1213],{"class":119},"- Cost: \u003C= $0.10\u002Fquery at 10,000 queries\u002Fday\n",[68,1215,1216],{"class":70,"line":192},[68,1217,1218],{"class":119},"- Pricing: $3\u002FM input tokens, $15\u002FM output tokens, $0.02\u002FM embeddings\n",[68,1220,1221],{"class":70,"line":197},[68,1222,100],{"emptyLinePlaceholder":99},[68,1224,1225],{"class":70,"line":222},[68,1226,1227],{"class":119},"Strategy: {{STRATEGY}}\n",[68,1229,1230],{"class":70,"line":241},[68,1231,100],{"emptyLinePlaceholder":99},[68,1233,1234],{"class":70,"line":258},[68,1235,1236],{"class":119},"Begin with a feasibility check.\n",[68,1238,1239],{"class":70,"line":275},[68,1240,1241],{"class":119},"If there is a fatal hard-constraint blocker, explain it and stop.\n",[68,1243,1244],{"class":70,"line":312},[68,1245,1246],{"class":119},"Otherwise provide:\n",[68,1248,1249],{"class":70,"line":348},[68,1250,1251],{"class":119},"1. Implementation\n",[68,1253,1254],{"class":70,"line":364},[68,1255,1256],{"class":119},"2. Failure modes\n",[68,1258,1260],{"class":70,"line":1259},27,[68,1261,1262],{"class":119},"3. Rollout plan\n",[68,1264,1266],{"class":70,"line":1265},28,[68,1267,1268],{"class":119},"4. Latency and cost projections\n",[68,1270,1272],{"class":70,"line":1271},29,[68,1273,1274],{"class":119},"5. Fallback strategy\n",[68,1276,1278],{"class":70,"line":1277},30,[68,1279,1280],{"class":119},"6. Final recommendation\n",[68,1282,1284],{"class":70,"line":1283},31,[68,1285,100],{"emptyLinePlaceholder":99},[68,1287,1289],{"class":70,"line":1288},32,[68,1290,1291],{"class":119},"End with exactly one line:\n",[68,1293,1295],{"class":70,"line":1294},33,[68,1296,1297],{"class":119},"FINAL_VERDICT: BLOCKED\n",[68,1299,1301],{"class":70,"line":1300},34,[68,1302,1303],{"class":119},"or\n",[68,1305,1307],{"class":70,"line":1306},35,[68,1308,1309],{"class":119},"FINAL_VERDICT: PROCEED\n",[68,1311,1313,1316,1318,1321],{"class":70,"line":1312},36,[68,1314,1315],{"class":119},"`",[68,1317,17],{"class":78},[68,1319,1320],{"class":109},"trim",[68,1322,1323],{"class":78},"();\n",[68,1325,1327],{"class":70,"line":1326},37,[68,1328,100],{"emptyLinePlaceholder":99},[68,1330,1332,1334,1337,1339,1341,1343,1345,1347],{"class":70,"line":1331},38,[68,1333,640],{"class":74},[68,1335,1336],{"class":216}," runs",[68,1338,646],{"class":74},[68,1340,649],{"class":74},[68,1342,652],{"class":216},[68,1344,17],{"class":78},[68,1346,657],{"class":109},[68,1348,1349],{"class":78},"(\n",[68,1351,1353,1356,1358,1360,1363],{"class":70,"line":1352},39,[68,1354,1355],{"class":78},"  strategies.",[68,1357,734],{"class":109},[68,1359,737],{"class":78},[68,1361,1362],{"class":209},"strategy",[68,1364,1365],{"class":74}," =>\n",[68,1367,1369,1372,1374],{"class":70,"line":1368},40,[68,1370,1371],{"class":78},"    sa.",[68,1373,627],{"class":109},[68,1375,1376],{"class":78},"({\n",[68,1378,1380,1383,1386,1388,1391],{"class":70,"line":1379},41,[68,1381,1382],{"class":78},"      prompt: basePrompt.",[68,1384,1385],{"class":109},"replace",[68,1387,737],{"class":78},[68,1389,1390],{"class":119},"\"{{STRATEGY}}\"",[68,1392,1393],{"class":78},", strategy)\n",[68,1395,1397],{"class":70,"line":1396},42,[68,1398,1399],{"class":78},"    })\n",[68,1401,1403],{"class":70,"line":1402},43,[68,1404,1405],{"class":78},"  )\n",[68,1407,1409],{"class":70,"line":1408},44,[68,1410,833],{"class":78},[68,1412,1414],{"class":70,"line":1413},45,[68,1415,100],{"emptyLinePlaceholder":99},[68,1417,1419,1421,1424,1426,1428,1430,1432,1434,1437,1439,1441,1444,1446,1449,1451],{"class":70,"line":1418},46,[68,1420,640],{"class":74},[68,1422,1423],{"class":216}," results",[68,1425,646],{"class":74},[68,1427,649],{"class":74},[68,1429,652],{"class":216},[68,1431,17],{"class":78},[68,1433,657],{"class":109},[68,1435,1436],{"class":78},"(runs.",[68,1438,734],{"class":109},[68,1440,737],{"class":78},[68,1442,1443],{"class":209},"r",[68,1445,742],{"class":74},[68,1447,1448],{"class":78}," sa.",[68,1450,907],{"class":109},[68,1452,1453],{"class":78},"(r.id)));\n",[68,1455,1457],{"class":70,"line":1456},47,[68,1458,100],{"emptyLinePlaceholder":99},[68,1460,1462,1464,1467,1469,1472,1474,1477,1479,1481,1484,1487,1490],{"class":70,"line":1461},48,[68,1463,640],{"class":74},[68,1465,1466],{"class":216}," analyses",[68,1468,646],{"class":74},[68,1470,1471],{"class":78}," results.",[68,1473,734],{"class":109},[68,1475,1476],{"class":78},"((",[68,1478,1443],{"class":209},[68,1480,335],{"class":78},[68,1482,1483],{"class":209},"i",[68,1485,1486],{"class":78},") ",[68,1488,1489],{"class":74},"=>",[68,1491,713],{"class":78},[68,1493,1495,1497,1500,1502,1505,1508,1511],{"class":70,"line":1494},49,[68,1496,723],{"class":74},[68,1498,1499],{"class":216}," output",[68,1501,646],{"class":74},[68,1503,1504],{"class":78}," r.output ",[68,1506,1507],{"class":74},"||",[68,1509,1510],{"class":119}," \"\"",[68,1512,1513],{"class":78},";\n",[68,1515,1517,1519,1522,1524,1527,1530],{"class":70,"line":1516},50,[68,1518,723],{"class":74},[68,1520,1521],{"class":216}," verdict",[68,1523,646],{"class":74},[68,1525,1526],{"class":78}," output.",[68,1528,1529],{"class":109},"match",[68,1531,1349],{"class":78},[68,1533,1535,1538,1542,1545,1547,1550,1553,1556,1559,1561],{"class":70,"line":1534},51,[68,1536,1537],{"class":119},"    \u002F",[68,1539,1541],{"class":1540},"sns5M","FINAL_VERDICT:",[68,1543,1544],{"class":216},"\\s",[68,1546,302],{"class":74},[68,1548,1549],{"class":1540},"(BLOCKED",[68,1551,1552],{"class":74},"|",[68,1554,1555],{"class":1540},"PROCEED)",[68,1557,1558],{"class":74},"\\b",[68,1560,323],{"class":119},[68,1562,1563],{"class":74},"i\n",[68,1565,1567,1570,1572,1575,1578,1581,1583,1586],{"class":70,"line":1566},52,[68,1568,1569],{"class":78},"  )?.[",[68,1571,24],{"class":216},[68,1573,1574],{"class":78},"]?.",[68,1576,1577],{"class":109},"toUpperCase",[68,1579,1580],{"class":78},"() ",[68,1582,1507],{"class":74},[68,1584,1585],{"class":119},"\"BLOCKED\"",[68,1587,1513],{"class":78},[68,1589,1591],{"class":70,"line":1590},53,[68,1592,100],{"emptyLinePlaceholder":99},[68,1594,1596,1598],{"class":70,"line":1595},54,[68,1597,759],{"class":74},[68,1599,1600],{"class":78}," { strategy: strategies[i], verdict, output };\n",[68,1602,1604],{"class":70,"line":1603},55,[68,1605,1606],{"class":78},"});\n",[68,1608,1610],{"class":70,"line":1609},56,[68,1611,100],{"emptyLinePlaceholder":99},[68,1613,1615,1617,1620,1622,1625,1628,1630,1632,1634,1637,1640,1643],{"class":70,"line":1614},57,[68,1616,640],{"class":74},[68,1618,1619],{"class":216}," blocked",[68,1621,646],{"class":74},[68,1623,1624],{"class":78}," analyses.",[68,1626,1627],{"class":109},"filter",[68,1629,737],{"class":78},[68,1631,450],{"class":209},[68,1633,742],{"class":74},[68,1635,1636],{"class":78}," a.verdict ",[68,1638,1639],{"class":74},"===",[68,1641,1642],{"class":119}," \"BLOCKED\"",[68,1644,833],{"class":78},[68,1646,1648,1650,1653,1655,1657,1659,1661,1663,1665,1667,1669,1672],{"class":70,"line":1647},58,[68,1649,640],{"class":74},[68,1651,1652],{"class":216}," surviving",[68,1654,646],{"class":74},[68,1656,1624],{"class":78},[68,1658,1627],{"class":109},[68,1660,737],{"class":78},[68,1662,450],{"class":209},[68,1664,742],{"class":74},[68,1666,1636],{"class":78},[68,1668,1639],{"class":74},[68,1670,1671],{"class":119}," \"PROCEED\"",[68,1673,833],{"class":78},[68,1675,1677],{"class":70,"line":1676},59,[68,1678,100],{"emptyLinePlaceholder":99},[68,1680,1682,1684,1687,1689],{"class":70,"line":1681},60,[68,1683,640],{"class":74},[68,1685,1686],{"class":216}," synthPrompt",[68,1688,646],{"class":74},[68,1690,1174],{"class":119},[68,1692,1694],{"class":70,"line":1693},61,[68,1695,1696],{"class":119},"Synthesize a ranked recommendation.\n",[68,1698,1700],{"class":70,"line":1699},62,[68,1701,100],{"emptyLinePlaceholder":99},[68,1703,1705],{"class":70,"line":1704},63,[68,1706,1707],{"class":119},"Rank only surviving strategies.\n",[68,1709,1711],{"class":70,"line":1710},64,[68,1712,1713],{"class":119},"List blocked strategies separately with their disqualifying constraint.\n",[68,1715,1717],{"class":70,"line":1716},65,[68,1718,100],{"emptyLinePlaceholder":99},[68,1720,1722],{"class":70,"line":1721},66,[68,1723,1724],{"class":119},"Surviving:\n",[68,1726,1728,1731,1733,1735,1737,1739,1741,1743,1746,1749,1752,1754,1756,1758,1761,1763,1765,1767,1769,1772,1775,1778,1780,1782,1785,1787,1789,1792],{"class":70,"line":1727},67,[68,1729,1730],{"class":119},"${",[68,1732,1095],{"class":78},[68,1734,17],{"class":119},[68,1736,734],{"class":109},[68,1738,737],{"class":119},[68,1740,450],{"class":216},[68,1742,742],{"class":74},[68,1744,1745],{"class":119}," `",[68,1747,1748],{"class":216},"\\n",[68,1750,1751],{"class":119},"## ${",[68,1753,450],{"class":78},[68,1755,17],{"class":119},[68,1757,1362],{"class":78},[68,1759,1760],{"class":119},"}",[68,1762,1748],{"class":216},[68,1764,1730],{"class":119},[68,1766,450],{"class":78},[68,1768,17],{"class":119},[68,1770,1771],{"class":78},"output",[68,1773,1774],{"class":119},"}`",[68,1776,1777],{"class":119},").",[68,1779,907],{"class":109},[68,1781,737],{"class":119},[68,1783,1784],{"class":119},"\"",[68,1786,1748],{"class":216},[68,1788,1784],{"class":119},[68,1790,1791],{"class":119},")",[68,1793,838],{"class":119},[68,1795,1797],{"class":70,"line":1796},68,[68,1798,100],{"emptyLinePlaceholder":99},[68,1800,1802],{"class":70,"line":1801},69,[68,1803,1804],{"class":119},"Blocked:\n",[68,1806,1808,1810,1812,1814,1816,1818,1820,1822,1824,1826,1828,1830,1832,1834,1836,1838,1840,1842,1844,1846,1848,1850,1852,1854,1856,1858,1860,1862],{"class":70,"line":1807},70,[68,1809,1730],{"class":119},[68,1811,1091],{"class":78},[68,1813,17],{"class":119},[68,1815,734],{"class":109},[68,1817,737],{"class":119},[68,1819,450],{"class":216},[68,1821,742],{"class":74},[68,1823,1745],{"class":119},[68,1825,1748],{"class":216},[68,1827,1751],{"class":119},[68,1829,450],{"class":78},[68,1831,17],{"class":119},[68,1833,1362],{"class":78},[68,1835,1760],{"class":119},[68,1837,1748],{"class":216},[68,1839,1730],{"class":119},[68,1841,450],{"class":78},[68,1843,17],{"class":119},[68,1845,1771],{"class":78},[68,1847,1774],{"class":119},[68,1849,1777],{"class":119},[68,1851,907],{"class":109},[68,1853,737],{"class":119},[68,1855,1784],{"class":119},[68,1857,1748],{"class":216},[68,1859,1784],{"class":119},[68,1861,1791],{"class":119},[68,1863,838],{"class":119},[68,1865,1867,1869,1871,1873],{"class":70,"line":1866},71,[68,1868,1315],{"class":119},[68,1870,17],{"class":78},[68,1872,1320],{"class":109},[68,1874,1323],{"class":78},[68,1876,1878],{"class":70,"line":1877},72,[68,1879,100],{"emptyLinePlaceholder":99},[68,1881,1883,1885,1888,1890,1892,1894,1896],{"class":70,"line":1882},73,[68,1884,640],{"class":74},[68,1886,1887],{"class":216}," synthRun",[68,1889,646],{"class":74},[68,1891,649],{"class":74},[68,1893,1448],{"class":78},[68,1895,627],{"class":109},[68,1897,1898],{"class":78},"({ prompt: synthPrompt });\n",[68,1900,1902,1904,1907,1909,1911,1913,1915],{"class":70,"line":1901},74,[68,1903,640],{"class":74},[68,1905,1906],{"class":216}," synth",[68,1908,646],{"class":74},[68,1910,649],{"class":74},[68,1912,1448],{"class":78},[68,1914,907],{"class":109},[68,1916,1917],{"class":78},"(synthRun.id);\n",[68,1919,1921],{"class":70,"line":1920},75,[68,1922,100],{"emptyLinePlaceholder":99},[68,1924,1926,1929],{"class":70,"line":1925},76,[68,1927,1928],{"class":74},"return",[68,1930,1931],{"class":78}," synth.output;\n",[43,1933,1935],{"id":1934},"what-the-runtime-buys","What The Runtime Buys",[11,1937,1938,1939,1941],{},"With Codex, every subagent return lands in the parent's context. It's in the data path for every intermediate value. With ",[65,1940,871],{},", the orchestration body runs server-side: subagent outputs flow JS-variable to JS-variable, and only the final synthesis comes back.",[1943,1944,1945,1960],"table",{},[1946,1947,1948],"thead",{},[1949,1950,1951,1955,1957],"tr",{},[1952,1953,1954],"th",{},"Metric",[1952,1956,871],{},[1952,1958,1959],{},"Codex",[1961,1962,1963,1975],"tbody",{},[1949,1964,1965,1969,1972],{},[1966,1967,1968],"td",{},"Parent context used",[1966,1970,1971],{},"2.2%",[1966,1973,1974],{},"5%",[1949,1976,1977,1980,1983],{},[1966,1978,1979],{},"Wall clock",[1966,1981,1982],{},"1m 47s",[1966,1984,1985],{},"2m 32s",[11,1987,1990],{"className":1988},[1989],"text-center",[1991,1992,1993],"small",{},"Results are aggregated over three runs.",[11,1995,1996,1997,1999],{},"That is why ",[65,1998,871],{}," holds less than half the context: the parent sees the JS body plus one synthesis return, not five subagent returns and the prose reasoning between them.",[11,2001,2002,2003,2005],{},"The wall clock results also surprised me. I expected parity since both fan out in parallel, but the Codex loop spends turns deciding what to do next between returns; ",[65,2004,871],{}," fans out once and waits.",[11,2007,2008],{},"The useful part is how small the runtime is: any harness with subagents is one small layer away from making orchestration executable.",[11,2010,2011],{},[14,2012,2013,2014],{},"The full extension is just 200 lines of TypeScript. ",[450,2015,2018],{"href":2016,"rel":2017,"target":456},"https:\u002F\u002Fgist.github.com\u002Fnidhishs\u002Fb15bb8513652f0a1cf8b0250065d9738",[454,455],"(link)",[43,2020,2022],{"id":2021},"the-pattern-converged","The Pattern Converged",[11,2024,2025,2026,2029,2030,2033,2034,2037,2038,2041,2042,2045,2046,2048,2049,2048,2051,2053],{},"A few weeks after I wrote this post, Anthropic shipped ",[450,2027,558],{"href":556,"rel":2028,"target":456},[454,455]," in Claude Code, and it's the same idea. Claude writes a script against a small set of primitives: ",[65,2031,2032],{},"agent()"," spawns a subagent and hands back its result, ",[65,2035,2036],{},"parallel()"," runs a batch at once and waits for all of them, ",[65,2039,2040],{},"pipeline()"," streams items through stages without stopping at each one, and ",[65,2043,2044],{},"phase()"," groups the run into labeled stages you can watch. It's a higher-level take on my ",[65,2047,581],{}," \u002F ",[65,2050,584],{},[65,2052,588],{},", but the design lines up:",[2055,2056,2057,2063,2077],"ul",{},[446,2058,2059,2062],{},[550,2060,2061],{},"Plan."," It lives in code, not the transcript, so the model doesn't have to carry it in its head from one turn to the next.",[446,2064,2065,2068,2069,2072,2073,2076],{},[550,2066,2067],{},"Context."," Intermediate results stay out of it. Their docs describe them living in ",[14,2070,2071],{},"“script variables,”"," so ",[14,2074,2075],{},"“Claude's context holds only the final answer.”"," Same variable-to-variable flow that helped shrink the parent context here.",[446,2078,2079,2082,2083,2086],{},[550,2080,2081],{},"Runtime."," It ",[14,2084,2085],{},"“executes the script in an isolated environment, separate from your conversation,”"," which is just the orchestration body running harness-side.",[11,2088,2089,2090,2092],{},"It's not a perfect match. There's no script-level ",[65,2091,588],{},", so you can't write the race-and-cancel I made a fuss about earlier. That part didn't make the jump.",[11,2094,2095,2096,2098],{},"None of this was a prediction. Once you feel the friction of bloated context, you end up here anyway. ",[65,2097,871],{}," was 200 lines on a hobby harness; Dynamic Workflows is that idea built at scale.",[522,2100,2101],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}",{"title":63,"searchDepth":82,"depth":82,"links":2103},[2104,2105,2108,2109,2110,2111],{"id":592,"depth":82,"text":593},{"id":844,"depth":82,"text":845,"children":2106},[2107],{"id":864,"depth":96,"text":865},{"id":999,"depth":82,"text":1000},{"id":1033,"depth":82,"text":1034},{"id":1934,"depth":82,"text":1935},{"id":2021,"depth":82,"text":2022},"2026-05-10","Making multi-agent orchestration explicit: give the model a small async runtime and let it write concurrent control flow as JavaScript instead of narrating it in prose.",{},"\u002Fwriting\u002Fruntime-subagents-orchestration",{"title":541,"description":2113},"writing\u002F20260510.runtime-subagents-orchestration","6jNHmSGGUOFwHUEfYmWYzm8WWDgTtZUTpP106XVwV0c",{"id":2120,"title":2121,"body":2122,"date":2341,"description":2342,"extension":533,"meta":2343,"navigation":99,"path":2344,"seo":2345,"stem":2346,"__hash__":2347},"writing\u002Fwriting\u002F20260420.ai-is-a-platform-shift.md","AI Is a Platform Shift. Act Like It.",{"type":8,"value":2123,"toc":2333},[2124,2127,2136,2139,2142,2145,2149,2152,2158,2168,2177,2180,2184,2187,2197,2206,2209,2212,2221,2231,2235,2238,2241,2244,2248,2251,2258,2261,2267,2271,2274,2277,2280,2283,2285,2288,2291,2300,2303,2305,2307],[11,2125,2126],{},"I have this thought sometimes that unsettles me. What if I'm standing inside one of the biggest career opportunities of my lifetime and still treating it like background noise?",[27,2128,29,2129,29,2133],{},[31,2130],{"src":2131,"alt":2132},"\u002Fwriting\u002F20260420.ai-is-a-platform-shift\u002Fhero.webp","AI Is a Platform Shift.",[36,2134,2135],{},"I'm already here. I don't want to be the person who stood still anyway.",[11,2137,2138],{},"That's what AI feels like to me right now. And what makes it hard to shake: I already work in AI. I see the releases, the products, the model updates, the whole thing up close. And I've spent stretches of that time feeling like a very informed bystander.",[11,2140,2141],{},"So drifting through this half-awake is just a mistake. The specific kind where you're close enough to see what's happening and still don't move.",[2143,2144],"hr",{},[43,2146,2148],{"id":2147},"the-pattern-is-real-even-if-the-details-differ","The pattern is real, even if the details differ",[11,2150,2151],{},"Every few decades, a new general-purpose technology appears and reorganizes everything around it. Electricity. The internal combustion engine. The computer. The internet.",[11,2153,2154,2155,2157],{},"Economists Timothy Bresnahan and Manuel Trajtenberg coined the term \"general-purpose technologies\" in a 1995 paper ",[22,2156],{"n":24}," to describe exactly these kinds of innovations: ones that improve over time, spread across sectors, and unlock a cascade of complementary innovations. The acronym, GPT, has since been hijacked by ChatGPT (which is, fittingly, also a general-purpose technology).",[11,2159,2160,2161,2164,2165,2167],{},"What makes these shifts so disorienting from the inside is that the value creation happens in stages. Carlota Perez, in ",[14,2162,2163],{},"Technological Revolutions and Financial Capital"," ",[22,2166],{"n":55},", describes two distinct phases: an installation phase, where the technology gets built out and financial capital piles in (this is where you get bubbles), followed by a deployment phase, where it embeds into the real economy and productive capital takes over. The installation phase is loud and full of bad bets. The deployment phase is where most of the actual wealth gets made. I find this framing more useful than most, because it tells you where you are, not what to feel.",[27,2169,29,2170,29,2174],{},[31,2171],{"src":2172,"alt":2173},"\u002Fwriting\u002F20260420.ai-is-a-platform-shift\u002Fperez-phases.webp","Chart illustrating Carlota Perez's two-phase model.",[36,2175,2176],{},"Perez's Two-Phase Model. Speculative investment peaks and crashes through the installation phase. Real productive value lags, then accumulates through deployment, after the crash, when most people have already looked away.",[11,2178,2179],{},"The dot-com bubble fits this model almost exactly. The late 1990s were frenzied, stuffed with companies that couldn't survive contact with reality. But the internet was still real. Amazon, Google, and others built during and after the crash, into the deployment phase, when the infrastructure was already laid. The hype and the real opportunity coexisted.",[43,2181,2183],{"id":2182},"why-smart-people-misread-this-moment","Why smart people misread this moment",[11,2185,2186],{},"There's a specific failure mode I keep seeing among technically literate people. They can identify what's weak: the shallow demos, the inflated valuations, the companies that raised too much for too little. And from that, they draw the wrong conclusion: that they still have time.",[11,2188,2189,2190,2192,2193,2196],{},"Amara put it plainly: \"We tend to overestimate the effect of a technology in the short run and underestimate it in the long run.\" ",[22,2191],{"n":430}," Everyone quotes the first half. Almost nobody internalizes the second; which is, ",[14,2194,2195],{},"itself",", a demonstration of the second half.",[27,2198,29,2199,29,2203],{},[31,2200],{"src":2201,"alt":2202},"\u002Fwriting\u002F20260420.ai-is-a-platform-shift\u002Famara-law.webp","Chart illustrating Amara's Law.",[36,2204,2205],{},"Amara's Law. Perceived impact rises fast and plateaus; actual long-run impact lags, then surpasses.",[11,2207,2208],{},"The logic goes: I can see the hype. I can see what's weak. So I must still be early enough to wait. But spotting noise isn't evidence of good timing. The internet boom had years of obvious nonsense before the structural shift became undeniable. The people who cited \"too noisy\" as a reason to disengage mostly missed it.",[11,2210,2211],{},"People who work in or near this industry face a specific trap, and I've been inside it. When you see enough model releases, enough product launches, enough hot takes, the whole thing starts to feel routine. The strangeness wears off. What was remarkable six months ago is now just background noise. And without noticing it, familiarity starts to read as insignificance.",[27,2213,29,2214,29,2218],{},[31,2215],{"src":2216,"alt":2217},"\u002Fwriting\u002F20260420.ai-is-a-platform-shift\u002Fwatching-vs-participating.webp","A museum visitor studies a painting while someone outside is actually painting.",[36,2219,2220],{},"I could describe every brushstroke in detail. I hadn't picked up a brush.",[11,2222,2223,2224,2227,2228,17],{},"Watching is not participating. The people accumulating real advantage right now aren't the ones with the sharpest takes. They're shipping things, learning from actual use, developing taste through exposure rather than consumption. Following the discourse closely ",[14,2225,2226],{},"feels"," like work. It mostly ",[14,2229,2230],{},"isn't",[43,2232,2234],{"id":2233},"where-the-opportunity-actually-is","Where the opportunity actually is",[11,2236,2237],{},"Most people, when they hear \"AI opportunity\", imagine they need to build the next foundation model. That's almost never how these shifts pay out.",[11,2239,2240],{},"The internet created Amazon and Google, yes. But it also built an enormous surrounding industry: e-commerce operators, web agencies, SaaS companies, infrastructure providers. Shopify didn't invent the internet. It built around it, at the right time, in a vertical where the shift created a real opening. YouTube created a production economy beyond the celebrity creators: editors, thumbnail designers, channel managers, sponsorship businesses. People who became useful in ways that hadn't been possible before.",[11,2242,2243],{},"AI has more of this than any of those shifts. The opportunities worth betting on are in the surrounding layers: vertical applications in law, medicine, and engineering where AI gives a capability edge; becoming unusually good at deploying these systems where most organizations are still fumbling; building the trust and distribution that hardens before the window gets crowded.",[43,2245,2247],{"id":2246},"counterarguments-i-take-seriously","Counterarguments I take seriously",[11,2249,2250],{},"The strongest version of the skeptical case isn't \"AI is overhyped.\" It's that the value commoditizes faster than you think. If the models become cheap utilities, building on top of them means building on top of a commodity. Application layers on commodity infrastructure tend to get squeezed. That's a real risk, particularly for businesses whose entire pitch is \"we wrapped a good model.\"",[11,2252,2253,2254,2257],{},"I held a version of this view for a while. It felt like the intellectually honest position. It's ",[14,2255,2256],{},"also",", in retrospect, a good way to stay comfortable without doing anything.",[11,2259,2260],{},"But that misreads where the actual advantages accumulate. The models themselves may commoditize. The organizational knowledge, the customer relationships, the earned trust: none of that runs on the same clock.",[11,2262,2263,2264],{},"The risk of a wrong specific bet is much smaller than the risk of sitting out the whole shift. ",[14,2265,2266],{},"(Not financial advice 👀.)",[43,2268,2270],{"id":2269},"what-moving-early-actually-looks-like","What \"moving early\" actually looks like",[11,2272,2273],{},"Moving early doesn't require quitting your job, raising a round, or making some loud public declaration about being an AI person.",[11,2275,2276],{},"The quieter version is more durable. Pick one workflow in your actual work and learn to do it better with AI tools until the improvement is obvious to everyone around you. Build something small and ship it. Write about what you're learning in a way that helps people in your field catch up, because that gap is still enormous in most industries.",[11,2278,2279],{},"The people I see in the strongest position right now didn't make one grand move. They made a series of small ones, each of which taught them something, and they've now developed a read on this space that most people don't have. Taste and judgment in a new domain accumulate through exposure. You can't read your way to them.",[11,2281,2282],{},"What matters is whether you're converting proximity into learning, and learning into things that compound.",[2143,2284],{},[11,2286,2287],{},"The version of this story that would bother me isn't that I missed the AI moment from far away.",[11,2289,2290],{},"It's that I was already close to it. Already inside the domain. Already watching it happen up close. And still stood still. Not because the window wasn't real, but because it never felt clear enough, clean enough, obvious enough.",[27,2292,29,2293,29,2297],{},[31,2294],{"src":2295,"alt":2296},"\u002Fwriting\u002F20260420.ai-is-a-platform-shift\u002Fday-one.webp","A notebook open to a page that reads Day 1, written and crossed out fourteen times.",[36,2298,2299],{},"Each of these felt like the real start. The most recent one still does.",[11,2301,2302],{},"That's the story I do not want.",[2143,2304],{},[43,2306,438],{"id":437},[440,2308,2309],{},[443,2310,2311,2323,2330],{},[446,2312,2313,2314,2164,2319,2322],{},"Timothy F. Bresnahan and Manuel Trajtenberg. ",[450,2315,2318],{"href":2316,"rel":2317,"target":456},"https:\u002F\u002Fdoi.org\u002F10.1016\u002F0304-4076(94)01598-T",[454,455],"General purpose technologies 'engines of growth'?",[14,2320,2321],{},"Journal of Econometrics",", 65(1):83–108, 1995.",[446,2324,2325,2326,2329],{},"Carlota Perez. ",[14,2327,2328],{},"Technological Revolutions and Financial Capital: The Dynamics of Bubbles and Golden Ages",". Edward Elgar, 2002.",[446,2331,2332],{},"Roy Amara. Commonly attributed statement on technology forecasting. Institute for the Future, c. 1970s.",{"title":63,"searchDepth":82,"depth":82,"links":2334},[2335,2336,2337,2338,2339,2340],{"id":2147,"depth":82,"text":2148},{"id":2182,"depth":82,"text":2183},{"id":2233,"depth":82,"text":2234},{"id":2246,"depth":82,"text":2247},{"id":2269,"depth":82,"text":2270},{"id":437,"depth":82,"text":438},"2026-04-20","On what the economics of general-purpose technologies tell us about where AI sits in history, and why passive proximity to the shift is not the same as acting on it.",{},"\u002Fwriting\u002Fai-is-a-platform-shift",{"title":2121,"description":2342},"writing\u002F20260420.ai-is-a-platform-shift","bZs392JZ29JkUJrH5TdjnfM3cEv1eLoiao6JJ5DEqSE",{"id":2349,"title":2350,"body":2351,"date":4574,"description":4575,"extension":533,"meta":4576,"navigation":99,"path":4577,"seo":4578,"stem":4579,"__hash__":4580},"writing\u002Fwriting\u002F20250219.adding-haptics-to-your-kb.md","Adding Haptic Feedback to Your Keyboard",{"type":8,"value":2352,"toc":4557},[2353,2356,2365,2368,2372,2434,2437,2440,2443,2446,2472,2475,2478,2503,2506,2526,2535,2538,2556,2582,2587,2615,2619,2622,2625,2684,2687,2696,2699,2748,2752,2763,2766,2777,2780,2790,2793,2796,2825,2832,3047,3054,3057,3099,3116,3127,3436,3442,3526,3539,3718,3729,3903,3906,3919,4271,4274,4528,4544,4548,4551,4554],[11,2354,2355],{},"Most modern phones give you a subtle buzz with each tap on the screen. This haptic feedback makes typing feel more precise and satisfying, yet it's notably absent from most custom keyboards. While the keyboard community has created countless tutorials on nearly every aspect of switches, resources on adding haptic feedback don't exist.",[27,2357,29,2358,29,2362],{},[31,2359],{"src":2360,"alt":2361},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002Feos.webp","Eos, a 30-key split ergonomic keyboard.",[36,2363,2364],{},"The Eos Keyboard, a 30-key split ergonomic board, designed by me!",[11,2366,2367],{},"This guide covers the full process of adding a haptic system to a custom keyboard. I'll walk through component selection, installation, and writing the ZMK firmware to control the linear resonant actuators (LRAs). You'll learn what it takes to add this dimension to your typing experience, whether you're building a new board or just exploring what's possible.",[43,2369,2371],{"id":2370},"table-of-contents","Table of Contents",[443,2373,2374,2380,2394,2414],{},[446,2375,2376],{},[450,2377,2379],{"href":2378},"#why-add-haptic-feedback","Why add Haptic Feedback?",[446,2381,2382,2386],{},[450,2383,2385],{"href":2384},"#understanding-the-schematics","Understanding the Schematics",[2055,2387,2388],{},[446,2389,2390],{},[450,2391,2393],{"href":2392},"#component-breakdown","Component Breakdown",[446,2395,2396,2400],{},[450,2397,2399],{"href":2398},"#laying-down-the-circuit-pcb-layout-essentials","PCB Layout Essentials",[2055,2401,2402,2408],{},[446,2403,2404],{},[450,2405,2407],{"href":2406},"#footprints","Footprints",[446,2409,2410],{},[450,2411,2413],{"href":2412},"#design-tips","Design Tips",[446,2415,2416,2420],{},[450,2417,2419],{"href":2418},"#the-firmware","The Firmware",[2055,2421,2422,2428],{},[446,2423,2424],{},[450,2425,2427],{"href":2426},"#shield-definition","Shield Definition",[446,2429,2430],{},[450,2431,2433],{"href":2432},"#keymap-definition","Keymap Definition",[43,2435,2379],{"id":2436},"why-add-haptic-feedback",[11,2438,2439],{},"If you've used a split mechanical keyboard, you're probably familiar with layers and combos. These aren't just convenient features; they're essential for maintaining efficiency with a reduced key count. However, there's a subtle usability cost: they often lack immediate feedback.",[11,2441,2442],{},"Think about it. When you hold a home row mod key or activate a layer, there's no immediate visual indication on your screen. You only discover if the mod or layer activated correctly after you press the next key.",[11,2444,2445],{},"This is most useful in a few places:",[2055,2447,2448,2454,2460,2466],{},[446,2449,2450,2453],{},[550,2451,2452],{},"Layer activation:"," Confirm that you've switched layers before hitting the next key.",[446,2455,2456,2459],{},[550,2457,2458],{},"Home row mods:"," Tell when a hold registered as a modifier instead of a tap.",[446,2461,2462,2465],{},[550,2463,2464],{},"Combos:"," Confirm that the key combination actually fired.",[446,2467,2468,2471],{},[550,2469,2470],{},"Timing-sensitive keys:"," Make hold-tap behavior easier to learn and adjust.",[11,2473,2474],{},"The beauty of custom haptics is that you can tune the feedback to your needs. You can set light vibrations for layer changes, stronger ones for combo activations, or different patterns for specific events.",[43,2476,2385],{"id":2477},"understanding-the-schematics",[545,2479,2480,2486],{},[2481,2482,2483],"template",{"v-slot:title":63},[11,2484,2485],{},"Heads up!",[11,2487,2488,2489,1092,2493,2497,2498,2502],{},"Before diving into custom schematics, consider that ",[450,2490,2492],{"href":2491,"target":456},"https:\u002F\u002Fshop.pimoroni.com\u002Fproducts\u002Fdrv2605l-linear-actuator-haptic-breakout?variant=27859486867539","Pimoroni",[450,2494,2496],{"href":2495,"target":456},"https:\u002F\u002Fwww.adafruit.com\u002Fproduct\u002F2305","Adafruit"," offer DRV2605L haptic breakout boards that are perfect for most builds. If you're using these boards, you can skip straight to the ",[450,2499,2501],{"href":2500},"#firmware","firmware"," section.\nThe custom implementation we'll cover here is mainly for builds with tight space constraints, those who want a single-board solution, or if you just enjoy building things from scratch (like me).",[11,2504,2505],{},"The haptic feedback system consists of three main components:",[443,2507,2508,2514,2520],{},[446,2509,2510,2513],{},[550,2511,2512],{},"MCU:"," The brain of the operation, handling I2C communication and timing control. Lately I've been enjoying the Seeeduino XIAO nRF52840.",[446,2515,2516,2519],{},[550,2517,2518],{},"DRV2605LDGS Driver:"," A specialized haptic driver IC that generates the precise waveforms needed for the LRA.",[446,2521,2522,2525],{},[550,2523,2524],{},"Linear Resonant Actuator:"," The physical component that creates the vibration. I prefer using the ELV1411A, but other LRAs work just fine.",[27,2527,29,2528,29,2532],{},[31,2529],{"src":2530,"alt":2531},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002Fhaptic-schematics.webp","Haptic Schematics.",[36,2533,2534],{},"Schematic showing the haptic feedback circuit. A DRV2605L haptic driver interfaces with the Seeeduino XIAO MCU via I2C and drives an ELV1411A linear resonant actuator (LRA).",[862,2536,2393],{"id":2537},"component-breakdown",[11,2539,2540,2541,1092,2544,2547,2548,2551,2552,2555],{},"The DRV2605L is connected to the MCU via I2C. For the XIAO, the default ",[65,2542,2543],{},"SDA",[65,2545,2546],{},"SCL"," lines are at pin ",[65,2549,2550],{},"P0.04-D4"," and pin ",[65,2553,2554],{},"P0.05-D5"," respectively. The power connections are as follows:",[2055,2557,2558,2564,2570,2576],{},[446,2559,2560,2563],{},[65,2561,2562],{},"VDD\u002FVNC"," connects to +3.3V with a 1µF decoupling capacitor (C1).",[446,2565,2566,2569],{},[65,2567,2568],{},"REG"," pin has a 1µF capacitor (C2) to ground.",[446,2571,2572,2575],{},[65,2573,2574],{},"IN\u002FTRIG"," is grounded since we're using I2C mode.",[446,2577,2578,2581],{},[65,2579,2580],{},"EN"," pin is pulled high to enable the device.",[11,2583,2584],{},[550,2585,2586],{},"Key Design Considerations:",[2055,2588,2589,2598],{},[446,2590,2591,2592,2594,2595,2597],{},"We connect the ",[65,2593,2580],{}," pin to +3.3V instead of a GPIO. Currently, the ZMK Driver for DRV2605L doesn't support the ",[65,2596,2580],{}," pin (yet?), so save yourself the extra GPIO 😉. The power draw is also minimal (~2µA).",[446,2599,2600,2601,2605,2606,2609,2610,1092,2612,2614],{},"While the ",[450,2602,2604],{"href":2603,"target":456},"https:\u002F\u002Fwww.ti.com\u002Flit\u002Fds\u002Fsymlink\u002Fdrv2605.pdf","datasheet"," specifies a 0.1µF capacitor for ",[65,2607,2608],{},"VDD",", this is a minimum requirement. We can use 1µF capacitors for both ",[65,2611,2608],{},[65,2613,2568],{}," pins to simplify our bill of materials and make hand-soldering easier.",[43,2616,2618],{"id":2617},"laying-down-the-circuit-pcb-layout-essentials","Laying Down the Circuit: PCB Layout Essentials",[11,2620,2621],{},"After finalizing our schematic, we need to carefully consider how to arrange these components on our PCB. While the circuit itself is straightforward, a few key layout decisions can make the difference between crisp, reliable haptic feedback and inconsistent performance.",[862,2623,2407],{"id":2624},"footprints",[2055,2626,2627,2641,2656,2671],{},[446,2628,2629,2632,2633,2636,2637,17],{},[550,2630,2631],{},"DRV2605LDGS",": KiCad's default ",[65,2634,2635],{},"VSSOP-10_3x3mm_P0.5mm"," footprint works well. If you need reversible PCBs, use this ",[450,2638,2640],{"href":2639,"target":456},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FVSSOP-10_3x3mm_P0.5mm_Reversible.kicad_mod","reversible version",[446,2642,2643,2646,2647,1092,2651,2655],{},[550,2644,2645],{},"ELV1411A LRA",": You can find both ",[450,2648,2650],{"href":2649,"target":456},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FELV1411A.kicad_mod","standard",[450,2652,2654],{"href":2653,"target":456},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FELV1411A_Reversible.kicad_mod","reversible"," footprints in my repository.",[446,2657,2658,2661,2662,2665,2666,2670],{},[550,2659,2660],{},"Seeeduino XIAO nRF52840",": The XIAO has underside battery pins which require some creativity. Use this ",[450,2663,2640],{"href":2664,"target":456},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FSeeeduino_Xiao_nRF52840_Reversible.kicad_mod"," (requires hot-plate) for reversible PCBs, or this through-hole ",[450,2667,2669],{"href":2668,"target":456},"https:\u002F\u002Fgithub.com\u002Fcrides\u002Fkleeb\u002Fblob\u002Fmaster\u002Fmcu.pretty\u002Fxiao-ble-smd-cutout.kicad_mod","standard version"," for traditional layouts.",[446,2672,2673,2676,2677,2680,2681,17],{},[550,2674,2675],{},"Capacitors",": KiCad's standard ",[65,2678,2679],{},"C_1206_3216Metric"," footprints work perfectly here, but here's a ",[450,2682,2640],{"href":2683,"target":456},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FC_1206_3216Metric_Reversible.kicad_mod",[11,2685,2686],{},"The reversible footprints are designed for PCBs that use the same design for both left and right halves. For keyboards with dedicated left\u002Fright PCBs or single-piece designs, use the standard versions.",[27,2688,29,2689,29,2693],{},[31,2690],{"src":2691,"alt":2692},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002Fhaptic-pcb-routing.webp","Haptic Kicad Routing of DRV2605L.",[36,2694,2695],{},"PCB layout example showing key component placement. The DRV2605L (U1) has its decoupling capacitors (C1 and C2) placed close by, with short traces connecting them. On the right is a tightly packed ground pad layout for the LRA, ensuring good mechanical contact and signal integrity.",[862,2697,2413],{"id":2698},"design-tips",[443,2700,2701,2725,2738],{},[446,2702,2703,2706],{},[550,2704,2705],{},"Component Placement",[2055,2707,2708,2715],{},[446,2709,2710,2711,1092,2713,1777],{},"The DRV2605L's decoupling capacitors must be placed as close as possible to their respective pins (",[65,2712,2608],{},[65,2714,2568],{},[446,2716,2717,2718,1092,2721,2724],{},"Keep the ",[65,2719,2720],{},"LRA+",[65,2722,2723],{},"LRA-"," traces symmetrical (as much as possible) and avoid crossing them.",[446,2726,2727,2730],{},[550,2728,2729],{},"Power Routing",[2055,2731,2732,2735],{},[446,2733,2734],{},"If space allows, use wider traces (e.g 0.3mm) for ground and power connections.",[446,2736,2737],{},"Keep the traces connecting the DRV2605L to its capacitors as short and direct as possible.",[446,2739,2740,2743],{},[550,2741,2742],{},"LRA Mounting",[2055,2744,2745],{},[446,2746,2747],{},"The LRA must have solid mechanical contact with the case\u002Fplate to effectively transfer vibrations.",[862,2749,2751],{"id":2750},"assembly","Assembly",[545,2753,2755,2760],{"variant":2754},"destructive",[2481,2756,2757],{"v-slot:title":63},[11,2758,2759],{},"Attention!",[11,2761,2762],{},"A hot plate is mandatory for this assembly. It is essential for the ELV1411A LRA and potentially for the XIAO, depending on the footprint choice.",[11,2764,2765],{},"The DRV2605L's tiny VSSOP-10 package makes assembly challenging. Using a stencil and hot plate is strongly recommended, but hand-soldering is possible with generous flux and the drag soldering technique.",[27,2767,29,2770,29,2774],{"className":2768},[2769],"w-4\u002F5",[31,2771],{"src":2772,"alt":2773},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002FDRV2605L-Solder.webp","Circuit board closeup showing DRV2605L haptic driver IC.",[36,2775,2776],{},"Close-up of a PCB showing the DRV2605L haptic driver IC in the VSSOP-10 package.",[11,2778,2779],{},"The other components (capacitors, LRA, and XIAO) are relatively straightforward to solder. After assembly, use a multimeter to verify there are no shorts between power and ground, and check continuity of the I2C lines.",[545,2781,2782,2787],{},[2481,2783,2784],{"v-slot:title":63},[11,2785,2786],{},"Tip",[11,2788,2789],{},"Work under good lighting with magnification. A microscope, or even your phone's camera can be incredibly helpful for inspecting your joints.",[43,2791,2419],{"id":2792},"the-firmware",[11,2794,2795],{},"Since ZMK mainline doesn't have haptic support yet, we need to use the following three modules.",[2055,2797,2798,2807,2816],{},[446,2799,2800],{},[450,2801,2804],{"href":2802,"rel":2803,"target":456},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-drv2605-driver",[454,455],[65,2805,2806],{},"zmk-drv2605-driver",[446,2808,2809],{},[450,2810,2813],{"href":2811,"rel":2812,"target":456},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-output-behavior-listener",[454,455],[65,2814,2815],{},"zmk-output-behavior-listener",[446,2817,2818],{},[450,2819,2822],{"href":2820,"rel":2821,"target":456},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-split-peripheral-output-relay",[454,455],[65,2823,2824],{},"zmk-split-peripheral-output-relay",[11,2826,2827,2828,2831],{},"To add the modules to your ZMK config, update your ",[65,2829,2830],{},"config\u002Fwest.yml"," file.",[58,2833,2837],{"className":2834,"code":2835,"language":2836,"meta":63,"style":63},"language-yaml shiki shiki-themes github-dark","manifest:\n  remotes:\n    - name: zmkfirmware\n      url-base: https:\u002F\u002Fgithub.com\u002Fzmkfirmware\n    - name: badjeff\n      url-base: https:\u002F\u002Fgithub.com\u002Fbadjeff\n  projects:\n    - name: zmk\n      remote: zmkfirmware\n      revision: v0.1\n      import: app\u002Fwest.yml\n    - name: zmk-output-behavior-listener\n      remote: badjeff\n      revision: d6f2f4c\n    - name: zmk-split-peripheral-output-relay\n      remote: badjeff\n      revision: 014b549\n    - name: zmk-drv2605-driver\n      remote: badjeff\n      revision: 81a386a\n  self:\n    path: config\n","yaml",[65,2838,2839,2848,2855,2869,2879,2890,2899,2906,2917,2926,2936,2946,2957,2965,2974,2985,2993,3002,3013,3021,3030,3037],{"__ignoreMap":63},[68,2840,2841,2845],{"class":70,"line":71},[68,2842,2844],{"class":2843},"s4JwU","manifest",[68,2846,2847],{"class":78},":\n",[68,2849,2850,2853],{"class":70,"line":82},[68,2851,2852],{"class":2843},"  remotes",[68,2854,2847],{"class":78},[68,2856,2857,2860,2863,2866],{"class":70,"line":96},[68,2858,2859],{"class":78},"    - ",[68,2861,2862],{"class":2843},"name",[68,2864,2865],{"class":78},": ",[68,2867,2868],{"class":119},"zmkfirmware\n",[68,2870,2871,2874,2876],{"class":70,"line":103},[68,2872,2873],{"class":2843},"      url-base",[68,2875,2865],{"class":78},[68,2877,2878],{"class":119},"https:\u002F\u002Fgithub.com\u002Fzmkfirmware\n",[68,2880,2881,2883,2885,2887],{"class":70,"line":116},[68,2882,2859],{"class":78},[68,2884,2862],{"class":2843},[68,2886,2865],{"class":78},[68,2888,2889],{"class":119},"badjeff\n",[68,2891,2892,2894,2896],{"class":70,"line":123},[68,2893,2873],{"class":2843},[68,2895,2865],{"class":78},[68,2897,2898],{"class":119},"https:\u002F\u002Fgithub.com\u002Fbadjeff\n",[68,2900,2901,2904],{"class":70,"line":129},[68,2902,2903],{"class":2843},"  projects",[68,2905,2847],{"class":78},[68,2907,2908,2910,2912,2914],{"class":70,"line":134},[68,2909,2859],{"class":78},[68,2911,2862],{"class":2843},[68,2913,2865],{"class":78},[68,2915,2916],{"class":119},"zmk\n",[68,2918,2919,2922,2924],{"class":70,"line":140},[68,2920,2921],{"class":2843},"      remote",[68,2923,2865],{"class":78},[68,2925,2868],{"class":119},[68,2927,2928,2931,2933],{"class":70,"line":146},[68,2929,2930],{"class":2843},"      revision",[68,2932,2865],{"class":78},[68,2934,2935],{"class":119},"v0.1\n",[68,2937,2938,2941,2943],{"class":70,"line":152},[68,2939,2940],{"class":2843},"      import",[68,2942,2865],{"class":78},[68,2944,2945],{"class":119},"app\u002Fwest.yml\n",[68,2947,2948,2950,2952,2954],{"class":70,"line":158},[68,2949,2859],{"class":78},[68,2951,2862],{"class":2843},[68,2953,2865],{"class":78},[68,2955,2956],{"class":119},"zmk-output-behavior-listener\n",[68,2958,2959,2961,2963],{"class":70,"line":163},[68,2960,2921],{"class":2843},[68,2962,2865],{"class":78},[68,2964,2889],{"class":119},[68,2966,2967,2969,2971],{"class":70,"line":169},[68,2968,2930],{"class":2843},[68,2970,2865],{"class":78},[68,2972,2973],{"class":119},"d6f2f4c\n",[68,2975,2976,2978,2980,2982],{"class":70,"line":175},[68,2977,2859],{"class":78},[68,2979,2862],{"class":2843},[68,2981,2865],{"class":78},[68,2983,2984],{"class":119},"zmk-split-peripheral-output-relay\n",[68,2986,2987,2989,2991],{"class":70,"line":180},[68,2988,2921],{"class":2843},[68,2990,2865],{"class":78},[68,2992,2889],{"class":119},[68,2994,2995,2997,2999],{"class":70,"line":186},[68,2996,2930],{"class":2843},[68,2998,2865],{"class":78},[68,3000,3001],{"class":119},"014b549\n",[68,3003,3004,3006,3008,3010],{"class":70,"line":192},[68,3005,2859],{"class":78},[68,3007,2862],{"class":2843},[68,3009,2865],{"class":78},[68,3011,3012],{"class":119},"zmk-drv2605-driver\n",[68,3014,3015,3017,3019],{"class":70,"line":197},[68,3016,2921],{"class":2843},[68,3018,2865],{"class":78},[68,3020,2889],{"class":119},[68,3022,3023,3025,3027],{"class":70,"line":222},[68,3024,2930],{"class":2843},[68,3026,2865],{"class":78},[68,3028,3029],{"class":119},"81a386a\n",[68,3031,3032,3035],{"class":70,"line":241},[68,3033,3034],{"class":2843},"  self",[68,3036,2847],{"class":78},[68,3038,3039,3042,3044],{"class":70,"line":258},[68,3040,3041],{"class":2843},"    path",[68,3043,2865],{"class":78},[68,3045,3046],{"class":119},"config\n",[545,3048,3049],{},[11,3050,1071,3051,3053],{},[65,3052,2824],{}," module is used for communication of output events between the central and peripheral side. For unibody builds, this module is not required.",[862,3055,2427],{"id":3056},"shield-definition",[11,3058,3059,3060,1092,3062,3064,3065,3068,3069,3072,3073,3075,3076,3072,3079,3081,3082,3084,3085,3087,3088,3091,3092,3084,3094,3087,3096,17],{},"To set up the DRV2605L driver, we need to configure the ",[65,3061,2543],{},[65,3063,2546],{}," lines. For a pin labeled ",[65,3066,3067],{},"PX.Y",", the configuration format is ",[65,3070,3071],{},"\u003CNRF_PSEL(TWIM_SDA, X, Y)>"," for the ",[65,3074,2543],{}," line and ",[65,3077,3078],{},"\u003CNRF_PSEL(TWIM_SCL, X, Y)>",[65,3080,2546],{}," line. In our case, the ",[65,3083,2543],{}," line is connected to pin ",[65,3086,2550],{},", so we use ",[65,3089,3090],{},"\u003CNRF_PSEL(TWIM_SDA, 0, 4)>",". Similarly, the ",[65,3093,2546],{},[65,3095,2554],{},[65,3097,3098],{},"\u003CNRF_PSEL(TWIM_SCL, 0, 5)>",[11,3100,3101,3102,3105,3106,3109,3110,3115],{},"Next, we must set up the I2C bus with the DRV2605L driver, specifying it's address. For the XIAO, the ",[65,3103,3104],{},"&xiao_i2c"," node label is exposed. Similarly, for Pro-Micro compatible boards, you may use the ",[65,3107,3108],{},"&pro_micro_i2c"," node. Check the ",[450,3111,3114],{"href":3112,"rel":3113,"target":456},"https:\u002F\u002Fzmk.dev\u002Fdocs\u002Fdevelopment\u002Fhardware-integration\u002Fpinctrl#predefined-nodes",[454,455],"ZMK documentation"," for more details.",[11,3117,3118,3119,3122,3123,3126],{},"Thus, the following code snippet can be added to your ",[65,3120,3121],{},"board.overlay"," file (for unibody builds), or the ",[65,3124,3125],{},"board.dtsi"," file (for split keyboards).",[58,3128,3132],{"className":3129,"code":3130,"language":3131,"meta":63,"style":63},"language-c shiki shiki-themes github-dark","&pinctrl {\n    i2c0_default: i2c0_default {\n        group1 {\n            psels = \u003CNRF_PSEL(TWIM_SDA, 0, 4)>, \u002F\u002F define your SDA pin.\n                    \u003CNRF_PSEL(TWIM_SCL, 0, 5)>; \u002F\u002F define your SCL pin.\n        };\n    };\n\n    i2c0_sleep: i2c0_sleep {\n        group1 {\n            psels = \u003CNRF_PSEL(TWIM_SDA, 0, 4)>, \u002F\u002F define your SDA pin.\n                    \u003CNRF_PSEL(TWIM_SCL, 0, 5)>; \u002F\u002F define your SCL pin.\n            low-power-enable;\n        };\n    };\n};\n\n&xiao_i2c {\n    status = \"okay\";\n    compatible = \"nordic,nrf-twim\";\n\n    drv2605_0: drv2605@5a {\n        compatible = \"ti,drv2605\";\n        reg = \u003C0x5a>;\n        library = \u003C6>;  \u002F\u002F LRA\n        standby-ms = \u003C1000>;\n    };\n};\n","c",[65,3133,3134,3142,3147,3152,3186,3212,3217,3222,3226,3231,3235,3261,3283,3298,3302,3306,3311,3315,3322,3334,3346,3350,3361,3373,3389,3407,3428,3432],{"__ignoreMap":63},[68,3135,3136,3139],{"class":70,"line":71},[68,3137,3138],{"class":74},"&",[68,3140,3141],{"class":78},"pinctrl {\n",[68,3143,3144],{"class":70,"line":82},[68,3145,3146],{"class":78},"    i2c0_default: i2c0_default {\n",[68,3148,3149],{"class":70,"line":96},[68,3150,3151],{"class":78},"        group1 {\n",[68,3153,3154,3157,3159,3162,3165,3168,3171,3173,3175,3177,3180,3183],{"class":70,"line":103},[68,3155,3156],{"class":78},"            psels ",[68,3158,203],{"class":74},[68,3160,3161],{"class":74}," \u003C",[68,3163,3164],{"class":109},"NRF_PSEL",[68,3166,3167],{"class":78},"(TWIM_SDA, ",[68,3169,3170],{"class":216},"0",[68,3172,335],{"class":78},[68,3174,403],{"class":216},[68,3176,1791],{"class":78},[68,3178,3179],{"class":74},">",[68,3181,3182],{"class":78},",",[68,3184,3185],{"class":308}," \u002F\u002F define your SDA pin.\n",[68,3187,3188,3191,3193,3196,3198,3200,3202,3204,3206,3209],{"class":70,"line":116},[68,3189,3190],{"class":74},"                    \u003C",[68,3192,3164],{"class":109},[68,3194,3195],{"class":78},"(TWIM_SCL, ",[68,3197,3170],{"class":216},[68,3199,335],{"class":78},[68,3201,407],{"class":216},[68,3203,1791],{"class":78},[68,3205,3179],{"class":74},[68,3207,3208],{"class":78},";",[68,3210,3211],{"class":308}," \u002F\u002F define your SCL pin.\n",[68,3213,3214],{"class":70,"line":123},[68,3215,3216],{"class":78},"        };\n",[68,3218,3219],{"class":70,"line":129},[68,3220,3221],{"class":78},"    };\n",[68,3223,3224],{"class":70,"line":134},[68,3225,100],{"emptyLinePlaceholder":99},[68,3227,3228],{"class":70,"line":140},[68,3229,3230],{"class":78},"    i2c0_sleep: i2c0_sleep {\n",[68,3232,3233],{"class":70,"line":146},[68,3234,3151],{"class":78},[68,3236,3237,3239,3241,3243,3245,3247,3249,3251,3253,3255,3257,3259],{"class":70,"line":152},[68,3238,3156],{"class":78},[68,3240,203],{"class":74},[68,3242,3161],{"class":74},[68,3244,3164],{"class":109},[68,3246,3167],{"class":78},[68,3248,3170],{"class":216},[68,3250,335],{"class":78},[68,3252,403],{"class":216},[68,3254,1791],{"class":78},[68,3256,3179],{"class":74},[68,3258,3182],{"class":78},[68,3260,3185],{"class":308},[68,3262,3263,3265,3267,3269,3271,3273,3275,3277,3279,3281],{"class":70,"line":158},[68,3264,3190],{"class":74},[68,3266,3164],{"class":109},[68,3268,3195],{"class":78},[68,3270,3170],{"class":216},[68,3272,335],{"class":78},[68,3274,407],{"class":216},[68,3276,1791],{"class":78},[68,3278,3179],{"class":74},[68,3280,3208],{"class":78},[68,3282,3211],{"class":308},[68,3284,3285,3288,3290,3293,3295],{"class":70,"line":163},[68,3286,3287],{"class":78},"            low",[68,3289,296],{"class":74},[68,3291,3292],{"class":78},"power",[68,3294,296],{"class":74},[68,3296,3297],{"class":78},"enable;\n",[68,3299,3300],{"class":70,"line":169},[68,3301,3216],{"class":78},[68,3303,3304],{"class":70,"line":175},[68,3305,3221],{"class":78},[68,3307,3308],{"class":70,"line":180},[68,3309,3310],{"class":78},"};\n",[68,3312,3313],{"class":70,"line":186},[68,3314,100],{"emptyLinePlaceholder":99},[68,3316,3317,3319],{"class":70,"line":192},[68,3318,3138],{"class":74},[68,3320,3321],{"class":78},"xiao_i2c {\n",[68,3323,3324,3327,3329,3332],{"class":70,"line":197},[68,3325,3326],{"class":78},"    status ",[68,3328,203],{"class":74},[68,3330,3331],{"class":119}," \"okay\"",[68,3333,1513],{"class":78},[68,3335,3336,3339,3341,3344],{"class":70,"line":222},[68,3337,3338],{"class":78},"    compatible ",[68,3340,203],{"class":74},[68,3342,3343],{"class":119}," \"nordic,nrf-twim\"",[68,3345,1513],{"class":78},[68,3347,3348],{"class":70,"line":241},[68,3349,100],{"emptyLinePlaceholder":99},[68,3351,3352,3355,3359],{"class":70,"line":258},[68,3353,3354],{"class":78},"    drv2605_0: drv2605@",[68,3356,3358],{"class":3357},"s6RL2","5a",[68,3360,713],{"class":78},[68,3362,3363,3366,3368,3371],{"class":70,"line":275},[68,3364,3365],{"class":78},"        compatible ",[68,3367,203],{"class":74},[68,3369,3370],{"class":119}," \"ti,drv2605\"",[68,3372,1513],{"class":78},[68,3374,3375,3378,3380,3383,3385,3387],{"class":70,"line":312},[68,3376,3377],{"class":78},"        reg ",[68,3379,203],{"class":74},[68,3381,3382],{"class":74}," \u003C0x",[68,3384,3358],{"class":216},[68,3386,3179],{"class":74},[68,3388,1513],{"class":78},[68,3390,3391,3394,3396,3398,3400,3402,3404],{"class":70,"line":348},[68,3392,3393],{"class":78},"        library ",[68,3395,203],{"class":74},[68,3397,3161],{"class":74},[68,3399,423],{"class":216},[68,3401,3179],{"class":74},[68,3403,3208],{"class":78},[68,3405,3406],{"class":308},"  \u002F\u002F LRA\n",[68,3408,3409,3412,3414,3417,3419,3421,3424,3426],{"class":70,"line":364},[68,3410,3411],{"class":78},"        standby",[68,3413,296],{"class":74},[68,3415,3416],{"class":78},"ms ",[68,3418,203],{"class":74},[68,3420,3161],{"class":74},[68,3422,3423],{"class":216},"1000",[68,3425,3179],{"class":74},[68,3427,1513],{"class":78},[68,3429,3430],{"class":70,"line":1259},[68,3431,3221],{"class":78},[68,3433,3434],{"class":70,"line":1265},[68,3435,3310],{"class":78},[11,3437,3438,3439,3441],{},"Next we define the devices. For unibody builds, add the following snippet to ",[65,3440,3121],{},":",[58,3443,3445],{"className":3129,"code":3444,"language":3131,"meta":63,"style":63},"\u002F {\n    haptic: haptic {\n        compatible = \"zmk,output-haptic-feedback\";\n        #binding-cells = \u003C0>;\n        driver = \"drv2605\";\n        device = \u003C&drv2605_0>;\n    };\n};\n",[65,3446,3447,3453,3458,3469,3489,3501,3518,3522],{"__ignoreMap":63},[68,3448,3449,3451],{"class":70,"line":71},[68,3450,323],{"class":74},[68,3452,713],{"class":78},[68,3454,3455],{"class":70,"line":82},[68,3456,3457],{"class":78},"    haptic: haptic {\n",[68,3459,3460,3462,3464,3467],{"class":70,"line":96},[68,3461,3365],{"class":78},[68,3463,203],{"class":74},[68,3465,3466],{"class":119}," \"zmk,output-haptic-feedback\"",[68,3468,1513],{"class":78},[68,3470,3471,3474,3476,3479,3481,3483,3485,3487],{"class":70,"line":103},[68,3472,3473],{"class":78},"        #binding",[68,3475,296],{"class":74},[68,3477,3478],{"class":78},"cells ",[68,3480,203],{"class":74},[68,3482,3161],{"class":74},[68,3484,3170],{"class":216},[68,3486,3179],{"class":74},[68,3488,1513],{"class":78},[68,3490,3491,3494,3496,3499],{"class":70,"line":116},[68,3492,3493],{"class":78},"        driver ",[68,3495,203],{"class":74},[68,3497,3498],{"class":119}," \"drv2605\"",[68,3500,1513],{"class":78},[68,3502,3503,3506,3508,3511,3514,3516],{"class":70,"line":123},[68,3504,3505],{"class":78},"        device ",[68,3507,203],{"class":74},[68,3509,3510],{"class":74}," \u003C&",[68,3512,3513],{"class":78},"drv2605_0",[68,3515,3179],{"class":74},[68,3517,1513],{"class":78},[68,3519,3520],{"class":70,"line":129},[68,3521,3221],{"class":78},[68,3523,3524],{"class":70,"line":134},[68,3525,3310],{"class":78},[11,3527,3528,3529,3531,3532,3534,3535,3538],{},"For split builds, ensure that the ",[65,3530,2824],{}," module is included in your ",[65,3533,2830],{},". Add the following to ",[65,3536,3537],{},"board-left.overlay"," (central side):",[58,3540,3542],{"className":3129,"code":3541,"language":3131,"meta":63,"style":63},"\u002F{\n    haptic_l: haptic_l {\n        compatible = \"zmk,output-haptic-feedback\";\n        #binding-cells = \u003C0>;\n        driver = \"drv2605\";\n        device = \u003C&drv2605_0>;\n    };\n\n    haptic_r: haptic_r {\n        compatible = \"zmk,output-split-output-relay\";\n        #binding-cells = \u003C0>;\n    };\n\n    output_relay_config_201_l {\n        compatible = \"zmk,split-peripheral-output-relay\";\n        device = \u003C&haptic_r>;\n        relay-channel = \u003C201>;\n    };\n};\n",[65,3543,3544,3551,3556,3566,3584,3594,3608,3612,3616,3621,3632,3650,3654,3658,3663,3674,3689,3710,3714],{"__ignoreMap":63},[68,3545,3546,3548],{"class":70,"line":71},[68,3547,323],{"class":74},[68,3549,3550],{"class":78},"{\n",[68,3552,3553],{"class":70,"line":82},[68,3554,3555],{"class":78},"    haptic_l: haptic_l {\n",[68,3557,3558,3560,3562,3564],{"class":70,"line":96},[68,3559,3365],{"class":78},[68,3561,203],{"class":74},[68,3563,3466],{"class":119},[68,3565,1513],{"class":78},[68,3567,3568,3570,3572,3574,3576,3578,3580,3582],{"class":70,"line":103},[68,3569,3473],{"class":78},[68,3571,296],{"class":74},[68,3573,3478],{"class":78},[68,3575,203],{"class":74},[68,3577,3161],{"class":74},[68,3579,3170],{"class":216},[68,3581,3179],{"class":74},[68,3583,1513],{"class":78},[68,3585,3586,3588,3590,3592],{"class":70,"line":116},[68,3587,3493],{"class":78},[68,3589,203],{"class":74},[68,3591,3498],{"class":119},[68,3593,1513],{"class":78},[68,3595,3596,3598,3600,3602,3604,3606],{"class":70,"line":123},[68,3597,3505],{"class":78},[68,3599,203],{"class":74},[68,3601,3510],{"class":74},[68,3603,3513],{"class":78},[68,3605,3179],{"class":74},[68,3607,1513],{"class":78},[68,3609,3610],{"class":70,"line":129},[68,3611,3221],{"class":78},[68,3613,3614],{"class":70,"line":134},[68,3615,100],{"emptyLinePlaceholder":99},[68,3617,3618],{"class":70,"line":140},[68,3619,3620],{"class":78},"    haptic_r: haptic_r {\n",[68,3622,3623,3625,3627,3630],{"class":70,"line":146},[68,3624,3365],{"class":78},[68,3626,203],{"class":74},[68,3628,3629],{"class":119}," \"zmk,output-split-output-relay\"",[68,3631,1513],{"class":78},[68,3633,3634,3636,3638,3640,3642,3644,3646,3648],{"class":70,"line":152},[68,3635,3473],{"class":78},[68,3637,296],{"class":74},[68,3639,3478],{"class":78},[68,3641,203],{"class":74},[68,3643,3161],{"class":74},[68,3645,3170],{"class":216},[68,3647,3179],{"class":74},[68,3649,1513],{"class":78},[68,3651,3652],{"class":70,"line":158},[68,3653,3221],{"class":78},[68,3655,3656],{"class":70,"line":163},[68,3657,100],{"emptyLinePlaceholder":99},[68,3659,3660],{"class":70,"line":169},[68,3661,3662],{"class":78},"    output_relay_config_201_l {\n",[68,3664,3665,3667,3669,3672],{"class":70,"line":175},[68,3666,3365],{"class":78},[68,3668,203],{"class":74},[68,3670,3671],{"class":119}," \"zmk,split-peripheral-output-relay\"",[68,3673,1513],{"class":78},[68,3675,3676,3678,3680,3682,3685,3687],{"class":70,"line":180},[68,3677,3505],{"class":78},[68,3679,203],{"class":74},[68,3681,3510],{"class":74},[68,3683,3684],{"class":78},"haptic_r",[68,3686,3179],{"class":74},[68,3688,1513],{"class":78},[68,3690,3691,3694,3696,3699,3701,3703,3706,3708],{"class":70,"line":186},[68,3692,3693],{"class":78},"        relay",[68,3695,296],{"class":74},[68,3697,3698],{"class":78},"channel ",[68,3700,203],{"class":74},[68,3702,3161],{"class":74},[68,3704,3705],{"class":216},"201",[68,3707,3179],{"class":74},[68,3709,1513],{"class":78},[68,3711,3712],{"class":70,"line":192},[68,3713,3221],{"class":78},[68,3715,3716],{"class":70,"line":197},[68,3717,3310],{"class":78},[11,3719,3720,3721,3724,3725,3728],{},"Similarly, we add the following to ",[65,3722,3723],{},"board-right.overlay"," (peripheral side). Here, you must create a dummy node ",[65,3726,3727],{},"&haptic_l"," to assist in compiling the firmware:",[58,3730,3732],{"className":3129,"code":3731,"language":3131,"meta":63,"style":63},"\u002F{\n    \u002F\u002F dummy device to make the overlay compile\n    haptic_l: haptic_l {\n        compatible = \"zmk,output-split-output-relay\";\n        #binding-cells = \u003C0>;\n    };\n\n    haptic_r: haptic_r {\n        compatible = \"zmk,output-haptic-feedback\";\n        #binding-cells = \u003C0>;\n        driver = \"drv2605\";\n        device = \u003C&drv2605_0>;\n    };\n\n    output_relay_config_201_l {\n        compatible = \"zmk,split-peripheral-output-relay\";\n        device = \u003C&haptic_r>;\n        relay-channel = \u003C201>;\n    };\n};\n",[65,3733,3734,3740,3745,3749,3759,3777,3781,3785,3789,3799,3817,3827,3841,3845,3849,3853,3863,3877,3895,3899],{"__ignoreMap":63},[68,3735,3736,3738],{"class":70,"line":71},[68,3737,323],{"class":74},[68,3739,3550],{"class":78},[68,3741,3742],{"class":70,"line":82},[68,3743,3744],{"class":308},"    \u002F\u002F dummy device to make the overlay compile\n",[68,3746,3747],{"class":70,"line":96},[68,3748,3555],{"class":78},[68,3750,3751,3753,3755,3757],{"class":70,"line":103},[68,3752,3365],{"class":78},[68,3754,203],{"class":74},[68,3756,3629],{"class":119},[68,3758,1513],{"class":78},[68,3760,3761,3763,3765,3767,3769,3771,3773,3775],{"class":70,"line":116},[68,3762,3473],{"class":78},[68,3764,296],{"class":74},[68,3766,3478],{"class":78},[68,3768,203],{"class":74},[68,3770,3161],{"class":74},[68,3772,3170],{"class":216},[68,3774,3179],{"class":74},[68,3776,1513],{"class":78},[68,3778,3779],{"class":70,"line":123},[68,3780,3221],{"class":78},[68,3782,3783],{"class":70,"line":129},[68,3784,100],{"emptyLinePlaceholder":99},[68,3786,3787],{"class":70,"line":134},[68,3788,3620],{"class":78},[68,3790,3791,3793,3795,3797],{"class":70,"line":140},[68,3792,3365],{"class":78},[68,3794,203],{"class":74},[68,3796,3466],{"class":119},[68,3798,1513],{"class":78},[68,3800,3801,3803,3805,3807,3809,3811,3813,3815],{"class":70,"line":146},[68,3802,3473],{"class":78},[68,3804,296],{"class":74},[68,3806,3478],{"class":78},[68,3808,203],{"class":74},[68,3810,3161],{"class":74},[68,3812,3170],{"class":216},[68,3814,3179],{"class":74},[68,3816,1513],{"class":78},[68,3818,3819,3821,3823,3825],{"class":70,"line":152},[68,3820,3493],{"class":78},[68,3822,203],{"class":74},[68,3824,3498],{"class":119},[68,3826,1513],{"class":78},[68,3828,3829,3831,3833,3835,3837,3839],{"class":70,"line":158},[68,3830,3505],{"class":78},[68,3832,203],{"class":74},[68,3834,3510],{"class":74},[68,3836,3513],{"class":78},[68,3838,3179],{"class":74},[68,3840,1513],{"class":78},[68,3842,3843],{"class":70,"line":163},[68,3844,3221],{"class":78},[68,3846,3847],{"class":70,"line":169},[68,3848,100],{"emptyLinePlaceholder":99},[68,3850,3851],{"class":70,"line":175},[68,3852,3662],{"class":78},[68,3854,3855,3857,3859,3861],{"class":70,"line":180},[68,3856,3365],{"class":78},[68,3858,203],{"class":74},[68,3860,3671],{"class":119},[68,3862,1513],{"class":78},[68,3864,3865,3867,3869,3871,3873,3875],{"class":70,"line":186},[68,3866,3505],{"class":78},[68,3868,203],{"class":74},[68,3870,3510],{"class":74},[68,3872,3684],{"class":78},[68,3874,3179],{"class":74},[68,3876,1513],{"class":78},[68,3878,3879,3881,3883,3885,3887,3889,3891,3893],{"class":70,"line":192},[68,3880,3693],{"class":78},[68,3882,296],{"class":74},[68,3884,3698],{"class":78},[68,3886,203],{"class":74},[68,3888,3161],{"class":74},[68,3890,3705],{"class":216},[68,3892,3179],{"class":74},[68,3894,1513],{"class":78},[68,3896,3897],{"class":70,"line":197},[68,3898,3221],{"class":78},[68,3900,3901],{"class":70,"line":222},[68,3902,3310],{"class":78},[862,3904,2433],{"id":3905},"keymap-definition",[11,3907,1071,3908,3910,3911,3918],{},[65,3909,2815],{}," module has excellent documentation and examples in its ",[450,3912,3915],{"href":3913,"rel":3914,"target":456},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-output-behavior-listener\u002Fblob\u002Fmain\u002FREADME.md",[454,455],[65,3916,3917],{},"README.md"," file. To simplify our keymaps and minimize repetitive code, I created some macros for a cleaner setup.",[58,3920,3922],{"className":3129,"code":3921,"language":3131,"meta":63,"style":63},"#define HAPTIC_OBG(node_name, device_ref, force_val) \\\n    node_name: node_name {                           \\\n        compatible = \"zmk,output-behavior-generic\";  \\\n        #binding-cells = \u003C0>;                        \\\n        device = \u003Cdevice_ref>;                       \\\n        force = \u003Cforce_val>;                         \\\n    };\n\n#define OUTPUT_SOURCE_LAYER_STATE_CHANGE 1\n#define HAPTIC_LAYER(node_name, bindings_list, layers_list)  \\\n    node_name: node_name {                                   \\\n        compatible = \"zmk,output-behavior-listener\";         \\\n        bindings = bindings_list;                            \\\n        layers = layers_list;                                \\\n        sources = \u003COUTPUT_SOURCE_LAYER_STATE_CHANGE>;        \\\n        all-state;                                           \\\n    };\n\n#define OUTPUT_SOURCE_KEYCODE_STATE_CHANGE 3\n#define HAPTIC_KEYCODE(node_name, keycode, bindings_list, layers_list) \\\n    node_name: node_name {                                             \\\n        compatible = \"zmk,output-behavior-listener\";                   \\\n        bindings = bindings_list;                                      \\\n        position = keycode;                                            \\\n        layers = layers_list;                                          \\\n        sources = \u003COUTPUT_SOURCE_KEYCODE_STATE_CHANGE>;                \\\n    };\n",[65,3923,3924,3952,3959,3973,3994,4011,4029,4033,4037,4047,4073,4080,4094,4106,4118,4137,4149,4153,4157,4167,4195,4202,4215,4226,4238,4249,4267],{"__ignoreMap":63},[68,3925,3926,3929,3932,3934,3937,3939,3942,3944,3947,3949],{"class":70,"line":71},[68,3927,3928],{"class":74},"#define",[68,3930,3931],{"class":109}," HAPTIC_OBG",[68,3933,737],{"class":78},[68,3935,3936],{"class":209},"node_name",[68,3938,335],{"class":78},[68,3940,3941],{"class":209},"device_ref",[68,3943,335],{"class":78},[68,3945,3946],{"class":209},"force_val",[68,3948,1486],{"class":78},[68,3950,3951],{"class":216},"\\\n",[68,3953,3954,3957],{"class":70,"line":82},[68,3955,3956],{"class":78},"    node_name: node_name {                           ",[68,3958,3951],{"class":216},[68,3960,3961,3963,3965,3968,3971],{"class":70,"line":96},[68,3962,3365],{"class":78},[68,3964,203],{"class":74},[68,3966,3967],{"class":119}," \"zmk,output-behavior-generic\"",[68,3969,3970],{"class":78},";  ",[68,3972,3951],{"class":216},[68,3974,3975,3977,3979,3981,3983,3985,3987,3989,3992],{"class":70,"line":103},[68,3976,3473],{"class":78},[68,3978,296],{"class":74},[68,3980,3478],{"class":78},[68,3982,203],{"class":74},[68,3984,3161],{"class":74},[68,3986,3170],{"class":216},[68,3988,3179],{"class":74},[68,3990,3991],{"class":78},";                        ",[68,3993,3951],{"class":216},[68,3995,3996,3998,4000,4002,4004,4006,4009],{"class":70,"line":116},[68,3997,3505],{"class":78},[68,3999,203],{"class":74},[68,4001,3161],{"class":74},[68,4003,3941],{"class":78},[68,4005,3179],{"class":74},[68,4007,4008],{"class":78},";                       ",[68,4010,3951],{"class":216},[68,4012,4013,4016,4018,4020,4022,4024,4027],{"class":70,"line":123},[68,4014,4015],{"class":78},"        force ",[68,4017,203],{"class":74},[68,4019,3161],{"class":74},[68,4021,3946],{"class":78},[68,4023,3179],{"class":74},[68,4025,4026],{"class":78},";                         ",[68,4028,3951],{"class":216},[68,4030,4031],{"class":70,"line":129},[68,4032,3221],{"class":78},[68,4034,4035],{"class":70,"line":134},[68,4036,100],{"emptyLinePlaceholder":99},[68,4038,4039,4041,4044],{"class":70,"line":140},[68,4040,3928],{"class":74},[68,4042,4043],{"class":109}," OUTPUT_SOURCE_LAYER_STATE_CHANGE",[68,4045,4046],{"class":216}," 1\n",[68,4048,4049,4051,4054,4056,4058,4060,4063,4065,4068,4071],{"class":70,"line":146},[68,4050,3928],{"class":74},[68,4052,4053],{"class":109}," HAPTIC_LAYER",[68,4055,737],{"class":78},[68,4057,3936],{"class":209},[68,4059,335],{"class":78},[68,4061,4062],{"class":209},"bindings_list",[68,4064,335],{"class":78},[68,4066,4067],{"class":209},"layers_list",[68,4069,4070],{"class":78},")  ",[68,4072,3951],{"class":216},[68,4074,4075,4078],{"class":70,"line":152},[68,4076,4077],{"class":78},"    node_name: node_name {                                   ",[68,4079,3951],{"class":216},[68,4081,4082,4084,4086,4089,4092],{"class":70,"line":158},[68,4083,3365],{"class":78},[68,4085,203],{"class":74},[68,4087,4088],{"class":119}," \"zmk,output-behavior-listener\"",[68,4090,4091],{"class":78},";         ",[68,4093,3951],{"class":216},[68,4095,4096,4099,4101,4104],{"class":70,"line":163},[68,4097,4098],{"class":78},"        bindings ",[68,4100,203],{"class":74},[68,4102,4103],{"class":78}," bindings_list;                            ",[68,4105,3951],{"class":216},[68,4107,4108,4111,4113,4116],{"class":70,"line":169},[68,4109,4110],{"class":78},"        layers ",[68,4112,203],{"class":74},[68,4114,4115],{"class":78}," layers_list;                                ",[68,4117,3951],{"class":216},[68,4119,4120,4123,4125,4127,4130,4132,4135],{"class":70,"line":175},[68,4121,4122],{"class":78},"        sources ",[68,4124,203],{"class":74},[68,4126,3161],{"class":74},[68,4128,4129],{"class":78},"OUTPUT_SOURCE_LAYER_STATE_CHANGE",[68,4131,3179],{"class":74},[68,4133,4134],{"class":78},";        ",[68,4136,3951],{"class":216},[68,4138,4139,4142,4144,4147],{"class":70,"line":180},[68,4140,4141],{"class":78},"        all",[68,4143,296],{"class":74},[68,4145,4146],{"class":78},"state;                                           ",[68,4148,3951],{"class":216},[68,4150,4151],{"class":70,"line":186},[68,4152,3221],{"class":78},[68,4154,4155],{"class":70,"line":192},[68,4156,100],{"emptyLinePlaceholder":99},[68,4158,4159,4161,4164],{"class":70,"line":197},[68,4160,3928],{"class":74},[68,4162,4163],{"class":109}," OUTPUT_SOURCE_KEYCODE_STATE_CHANGE",[68,4165,4166],{"class":216}," 3\n",[68,4168,4169,4171,4174,4176,4178,4180,4183,4185,4187,4189,4191,4193],{"class":70,"line":222},[68,4170,3928],{"class":74},[68,4172,4173],{"class":109}," HAPTIC_KEYCODE",[68,4175,737],{"class":78},[68,4177,3936],{"class":209},[68,4179,335],{"class":78},[68,4181,4182],{"class":209},"keycode",[68,4184,335],{"class":78},[68,4186,4062],{"class":209},[68,4188,335],{"class":78},[68,4190,4067],{"class":209},[68,4192,1486],{"class":78},[68,4194,3951],{"class":216},[68,4196,4197,4200],{"class":70,"line":241},[68,4198,4199],{"class":78},"    node_name: node_name {                                             ",[68,4201,3951],{"class":216},[68,4203,4204,4206,4208,4210,4213],{"class":70,"line":258},[68,4205,3365],{"class":78},[68,4207,203],{"class":74},[68,4209,4088],{"class":119},[68,4211,4212],{"class":78},";                   ",[68,4214,3951],{"class":216},[68,4216,4217,4219,4221,4224],{"class":70,"line":275},[68,4218,4098],{"class":78},[68,4220,203],{"class":74},[68,4222,4223],{"class":78}," bindings_list;                                      ",[68,4225,3951],{"class":216},[68,4227,4228,4231,4233,4236],{"class":70,"line":312},[68,4229,4230],{"class":78},"        position ",[68,4232,203],{"class":74},[68,4234,4235],{"class":78}," keycode;                                            ",[68,4237,3951],{"class":216},[68,4239,4240,4242,4244,4247],{"class":70,"line":348},[68,4241,4110],{"class":78},[68,4243,203],{"class":74},[68,4245,4246],{"class":78}," layers_list;                                          ",[68,4248,3951],{"class":216},[68,4250,4251,4253,4255,4257,4260,4262,4265],{"class":70,"line":364},[68,4252,4122],{"class":78},[68,4254,203],{"class":74},[68,4256,3161],{"class":74},[68,4258,4259],{"class":78},"OUTPUT_SOURCE_KEYCODE_STATE_CHANGE",[68,4261,3179],{"class":74},[68,4263,4264],{"class":78},";                ",[68,4266,3951],{"class":216},[68,4268,4269],{"class":70,"line":1259},[68,4270,3221],{"class":78},[11,4272,4273],{},"Here's an example keymap with the macros:",[58,4275,4277],{"className":3129,"code":4276,"language":3131,"meta":63,"style":63},"\u002F* Layer Definitions *\u002F\n#define DEFAULT 0\n#define LAYER1  1\n#define LAYER2  2\n\n\u002F {\n    \u002F\u002F setup output behaviors for both the left, and right haptic devices.\n    HAPTIC_OBG(hl_dc_strong_1, &haptic_l, 27)\n    HAPTIC_OBG(hr_dc_strong_1, &haptic_r, 27)\n    HAPTIC_OBG(hl_strong_click_1, &haptic_l, 1)\n    HAPTIC_OBG(hr_strong_click_1, &haptic_r, 1)\n\n    \u002F\u002F setup output listeners for keycodes and layers.\n    HAPTIC_KEYCODE(haptic_lshift, \u003C0xE1>, \u003C&hl_dc_strong_1>, \u003CDEFAULT>)\n    HAPTIC_KEYCODE(haptic_rshift, \u003C0xE5>, \u003C&hr_dc_strong_1>, \u003CDEFAULT>)\n    HAPTIC_LAYER(haptic_l1, \u003C&hl_strong_click_1>, \u003CLAYER1>)\n    HAPTIC_LAYER(haptic_l2, \u003C&hr_strong_click_1>, \u003CLAYER2>)\n};\n",[65,4278,4279,4284,4294,4304,4314,4318,4324,4329,4347,4363,4378,4393,4397,4402,4440,4473,4499,4524],{"__ignoreMap":63},[68,4280,4281],{"class":70,"line":71},[68,4282,4283],{"class":308},"\u002F* Layer Definitions *\u002F\n",[68,4285,4286,4288,4291],{"class":70,"line":82},[68,4287,3928],{"class":74},[68,4289,4290],{"class":109}," DEFAULT",[68,4292,4293],{"class":216}," 0\n",[68,4295,4296,4298,4301],{"class":70,"line":96},[68,4297,3928],{"class":74},[68,4299,4300],{"class":109}," LAYER1",[68,4302,4303],{"class":216},"  1\n",[68,4305,4306,4308,4311],{"class":70,"line":103},[68,4307,3928],{"class":74},[68,4309,4310],{"class":109}," LAYER2",[68,4312,4313],{"class":216},"  2\n",[68,4315,4316],{"class":70,"line":116},[68,4317,100],{"emptyLinePlaceholder":99},[68,4319,4320,4322],{"class":70,"line":123},[68,4321,323],{"class":74},[68,4323,713],{"class":78},[68,4325,4326],{"class":70,"line":129},[68,4327,4328],{"class":308},"    \u002F\u002F setup output behaviors for both the left, and right haptic devices.\n",[68,4330,4331,4334,4337,4339,4342,4345],{"class":70,"line":134},[68,4332,4333],{"class":109},"    HAPTIC_OBG",[68,4335,4336],{"class":78},"(hl_dc_strong_1, ",[68,4338,3138],{"class":74},[68,4340,4341],{"class":78},"haptic_l, ",[68,4343,4344],{"class":216},"27",[68,4346,219],{"class":78},[68,4348,4349,4351,4354,4356,4359,4361],{"class":70,"line":140},[68,4350,4333],{"class":109},[68,4352,4353],{"class":78},"(hr_dc_strong_1, ",[68,4355,3138],{"class":74},[68,4357,4358],{"class":78},"haptic_r, ",[68,4360,4344],{"class":216},[68,4362,219],{"class":78},[68,4364,4365,4367,4370,4372,4374,4376],{"class":70,"line":146},[68,4366,4333],{"class":109},[68,4368,4369],{"class":78},"(hl_strong_click_1, ",[68,4371,3138],{"class":74},[68,4373,4341],{"class":78},[68,4375,24],{"class":216},[68,4377,219],{"class":78},[68,4379,4380,4382,4385,4387,4389,4391],{"class":70,"line":152},[68,4381,4333],{"class":109},[68,4383,4384],{"class":78},"(hr_strong_click_1, ",[68,4386,3138],{"class":74},[68,4388,4358],{"class":78},[68,4390,24],{"class":216},[68,4392,219],{"class":78},[68,4394,4395],{"class":70,"line":158},[68,4396,100],{"emptyLinePlaceholder":99},[68,4398,4399],{"class":70,"line":163},[68,4400,4401],{"class":308},"    \u002F\u002F setup output listeners for keycodes and layers.\n",[68,4403,4404,4407,4410,4413,4416,4418,4420,4423,4426,4428,4430,4433,4436,4438],{"class":70,"line":169},[68,4405,4406],{"class":109},"    HAPTIC_KEYCODE",[68,4408,4409],{"class":78},"(haptic_lshift, ",[68,4411,4412],{"class":74},"\u003C0x",[68,4414,4415],{"class":216},"E1",[68,4417,3179],{"class":74},[68,4419,335],{"class":78},[68,4421,4422],{"class":74},"\u003C&",[68,4424,4425],{"class":78},"hl_dc_strong_1",[68,4427,3179],{"class":74},[68,4429,335],{"class":78},[68,4431,4432],{"class":74},"\u003C",[68,4434,4435],{"class":78},"DEFAULT",[68,4437,3179],{"class":74},[68,4439,219],{"class":78},[68,4441,4442,4444,4447,4449,4452,4454,4456,4458,4461,4463,4465,4467,4469,4471],{"class":70,"line":175},[68,4443,4406],{"class":109},[68,4445,4446],{"class":78},"(haptic_rshift, ",[68,4448,4412],{"class":74},[68,4450,4451],{"class":216},"E5",[68,4453,3179],{"class":74},[68,4455,335],{"class":78},[68,4457,4422],{"class":74},[68,4459,4460],{"class":78},"hr_dc_strong_1",[68,4462,3179],{"class":74},[68,4464,335],{"class":78},[68,4466,4432],{"class":74},[68,4468,4435],{"class":78},[68,4470,3179],{"class":74},[68,4472,219],{"class":78},[68,4474,4475,4478,4481,4483,4486,4488,4490,4492,4495,4497],{"class":70,"line":180},[68,4476,4477],{"class":109},"    HAPTIC_LAYER",[68,4479,4480],{"class":78},"(haptic_l1, ",[68,4482,4422],{"class":74},[68,4484,4485],{"class":78},"hl_strong_click_1",[68,4487,3179],{"class":74},[68,4489,335],{"class":78},[68,4491,4432],{"class":74},[68,4493,4494],{"class":78},"LAYER1",[68,4496,3179],{"class":74},[68,4498,219],{"class":78},[68,4500,4501,4503,4506,4508,4511,4513,4515,4517,4520,4522],{"class":70,"line":186},[68,4502,4477],{"class":109},[68,4504,4505],{"class":78},"(haptic_l2, ",[68,4507,4422],{"class":74},[68,4509,4510],{"class":78},"hr_strong_click_1",[68,4512,3179],{"class":74},[68,4514,335],{"class":78},[68,4516,4432],{"class":74},[68,4518,4519],{"class":78},"LAYER2",[68,4521,3179],{"class":74},[68,4523,219],{"class":78},[68,4525,4526],{"class":70,"line":192},[68,4527,3310],{"class":78},[11,4529,4530,4531,4534,4535,4539,4540,17],{},"Note that the ",[65,4532,4533],{},"force"," parameter in the output behavior definition corresponds to the waveform effect from the DRV2605L library. For a complete list of effects, refer to section 11.2 of the ",[450,4536,4538],{"href":4537,"target":456},"https:\u002F\u002Fwww.ti.com\u002Flit\u002Fds\u002Fsymlink\u002Fdrv2605.pdf#page=57","DRV2605L datasheet",". Additionally, the keycode values align with the usage IDs found in the ",[450,4541,4543],{"href":4542,"target":456},"https:\u002F\u002Fusb.org\u002Fsites\u002Fdefault\u002Ffiles\u002Fhut1_2.pdf#page=83","HID Usages document",[43,4545,4547],{"id":4546},"conclusion","Conclusion",[11,4549,4550],{},"That's the full breakdown. Adding haptic feedback to your custom keyboard is a rewarding project. By following this guide, you've learned how to select the right components, design and assemble the PCB, and configure the firmware to bring your keyboard to life with tactile feedback.",[11,4552,4553],{},"The possibilities for personalization are endless. Customize the vibration patterns to match your workflow, tune the intensity to your preference, or experiment with different actuators. Thanks for reading. Happy building, and may your keyboard be as expressive as your creativity allows!",[522,4555,4556],{},"html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s6RL2, html code.shiki .s6RL2{--shiki-default:#FDAEB7;--shiki-default-font-style:italic}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":63,"searchDepth":82,"depth":82,"links":4558},[4559,4560,4561,4564,4569,4573],{"id":2370,"depth":82,"text":2371},{"id":2436,"depth":82,"text":2379},{"id":2477,"depth":82,"text":2385,"children":4562},[4563],{"id":2537,"depth":96,"text":2393},{"id":2617,"depth":82,"text":2618,"children":4565},[4566,4567,4568],{"id":2624,"depth":96,"text":2407},{"id":2698,"depth":96,"text":2413},{"id":2750,"depth":96,"text":2751},{"id":2792,"depth":82,"text":2419,"children":4570},[4571,4572],{"id":3056,"depth":96,"text":2427},{"id":3905,"depth":96,"text":2433},{"id":4546,"depth":82,"text":4547},"2025-02-19","A complete guide to adding haptic feedback to a custom mechanical keyboard using DRV2605L: component selection, PCB layout, and ZMK firmware for linear resonant actuators.",{},"\u002Fwriting\u002Fadding-haptics-to-your-kb",{"title":2350,"description":4575},"writing\u002F20250219.adding-haptics-to-your-kb","zJbVm6KwUo8vMGwM3UJUKQ_ilxjuOr82WdPMh1ZmL7Y",{"id":4582,"title":4583,"body":4584,"date":4634,"description":4635,"extension":533,"meta":4636,"navigation":99,"path":4637,"seo":4638,"stem":4639,"__hash__":4640},"writing\u002Fwriting\u002F20240906.stack-eval.md","StackEval: Benchmarking LLMs in Coding Assistance",{"type":8,"value":4585,"toc":4632},[4586,4589,4592,4601,4609,4616,4625],[11,4587,4588],{},"Most coding benchmarks still emphasize problems that are easy to unit test. That usually means narrow code generation or completion tasks with clean success criteria. Those benchmarks are useful, but they miss a large share of how coding assistants are actually used.",[11,4590,4591],{},"In practice, a lot of coding assistance is natural-language Q&A: debugging an error, explaining unfamiliar behavior, comparing implementation options, or helping a developer get unstuck in an unfamiliar stack. That kind of support is harder to evaluate, precisely because it is less neatly unit testable, but it is also much closer to real developer workflows.",[27,4593,29,4594,29,4598],{},[31,4595],{"src":4596,"alt":4597},"\u002Fwriting\u002F20240906.stack-eval\u002Fstack-eval.webp","Team members reviewing coding-assistance outputs during the StackEval evaluation process.",[36,4599,4600],{},"Part of the annotation and evaluation process behind StackEval, where coding-assistance outputs were reviewed against a practical acceptability rubric.",[11,4602,4603,4604,4608],{},"That gap is what motivated our paper, ",[450,4605,4583],{"href":4606,"rel":4607,"target":456},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2412.05288",[454,455],". StackEval is built around real coding-assistance tasks drawn from practice, including debugging, implementation, optimization, and conceptual understanding across many programming languages. The goal is not just to measure whether a model can produce code, but whether it can provide help that is actually useful.",[11,4610,4611,4612,4615],{},"One important part of the benchmark is ",[550,4613,4614],{},"StackUnseen",", which evaluates models on newer Stack Overflow questions. This matters because coding changes quickly. Frameworks evolve, APIs get replaced, best practices shift, and new developer pain points appear all the time. A model that performs well on older benchmark data may still struggle when the question reflects a newer toolchain or a more current development pattern.",[11,4617,4618,4619,4624],{},"More broadly, this connects to the direction behind ",[450,4620,4623],{"href":4621,"rel":4622,"target":456},"https:\u002F\u002Fwww.prollm.ai\u002F",[454,455],"ProLLM",": benchmarks should be practical, reliable, and tied to real-world use cases. If a benchmark mostly rewards what is easy to score rather than what is valuable in practice, it becomes much less useful for model selection or product decisions. That is especially true for coding, where small differences in usefulness can have a large impact on developer productivity.",[11,4626,4627,4628,17],{},"StackEval is one step toward more practical evaluation for coding assistants. You can read the full paper here: ",[450,4629,4631],{"href":4606,"rel":4630,"target":456},[454,455],"arXiv:2412.05288",{"title":63,"searchDepth":82,"depth":82,"links":4633},[],"2024-09-06","Introducing StackEval, a benchmark for LLMs on code debugging, implementation, and conceptual Q&A.",{},"\u002Fwriting\u002Fstack-eval",{"title":4583,"description":4635},"writing\u002F20240906.stack-eval","vl3d27rzUQ1Lu7tQ7---od8D2bPNMzBf_r21sSuFLYg",1780404016419]