From 673ac42ea0f2fbc88add386c990de9f7525ee28f Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 23 Feb 2026 15:17:21 -0800 Subject: [PATCH 1/3] Add marketing materials --- marketing/differentiators-vs-claude-code.md | 13 ++ marketing/logo-transparent.png | Bin 0 -> 6243 bytes marketing/logo.png | Bin 0 -> 12032 bytes marketing/real-results.md | 24 ++ marketing/slides-intro.html | 246 ++++++++++++++++++++ marketing/why-pay-attention.md | 31 +++ 6 files changed, 314 insertions(+) create mode 100644 marketing/differentiators-vs-claude-code.md create mode 100644 marketing/logo-transparent.png create mode 100644 marketing/logo.png create mode 100644 marketing/real-results.md create mode 100644 marketing/slides-intro.html create mode 100644 marketing/why-pay-attention.md diff --git a/marketing/differentiators-vs-claude-code.md b/marketing/differentiators-vs-claude-code.md new file mode 100644 index 00000000..0def00c9 --- /dev/null +++ b/marketing/differentiators-vs-claude-code.md @@ -0,0 +1,13 @@ +# Codev vs Claude Code: Key Differentiators + +1. **Multi-model**: We use Gemini + Codex as reviewers alongside Claude. Each catches different classes of issues — Codex finds security edge cases, Claude catches runtime semantics, Gemini catches architecture problems. + +2. **Specs and plans are first-class citizens**: Every feature produces a specification and an implementation plan that are preserved as project artifacts. You always know WHY something was built and HOW it was designed. + +3. **Plans are enforced**: You can't start Phase 2 until the acceptance criteria for Phase 1 are met. The Porch state machine makes the process deterministic — no skipping steps. Human gates require architect approval before implementation begins, so the AI can't run off and build the wrong thing. Testing is enforced as acceptance criteria, not optional — this is why codev produces 2.9x more test coverage consistently. + +4. **Annotation over direct editing**: It's far more about annotating docs than directly editing code. Reviews, specs, and plans are documents that guide the work rather than the AI just hacking at files. + +5. **Parallel builders**: Push parallelization to its limit by having one "Architect" AI that can direct 5+ builders working simultaneously in isolated worktrees. + +6. **Whole lifecycle management**: From idea through specification, planning, implementation, review, PR, and deployment — codev manages the entire lifecycle, not just the coding step. diff --git a/marketing/logo-transparent.png b/marketing/logo-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..1a775e3dc1686b4bacbe67ec014610dc48030908 GIT binary patch literal 6243 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 NK-Wm8P@8_`KL84?r@#OJ literal 0 HcmV?d00001 diff --git a/marketing/logo.png b/marketing/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6baa6826f854e2ff8adcec137b60b96c52af8836 GIT binary patch literal 12032 zcmeHt`9GA=+yA)-gRz#$zD2g|WGQ5n>|`m~%D!Zc5E8c*OZHtNWKFgbvX3Q%?3JX* zR+4N}F){aZ_xXPRgy*;CGruse*SXF)*K)4weZ8-VGd0m=!g1pO08ILN+U5XIqyJI^ zIt=<}GqP<80GWP$ZLJF-dF!7(r7hGQ!j2tn*gyuCZc90G$@RTEe`780=>?Gw@@Z1H zeCT&}s8G58e*SBL|61UG!vf8AZuWq{_kQgcr9hU0O$r>cE&wwjMu@BgJ%}ZGQjhiZ zW9C&vAh2kC6Xc#WrBXLL48|U2V&?@=p=&rzd$3=23=~SQ;fP2cg7Ztp%*TZRI9=)( zsJ(M@ntMJj4ZyYg4{kv~F>!S5%>jkZ00Wf80etRAEM-HS9!xvvgRFW*at!(SHQVnH zoO$5>n+7CYZ@==Vd}sG0rLJ3m2~OsPFj<1zD?u+eyaPd72>{>IDFUyOUsaRWR_Ef7 zE~rnA2b1}ReD!mVi0&q}de!*tCMtqoFo1KNbT;F<%RX;u1W`EVfVl+#Z(}LvyWM8# zG=e&#>#C{8^7fc3iggQ(4iv_6ij>a`dB=Y6089^7U|fsaV1hkW>;tDex`>$kN7i|D zT^ODXa27 zJl~YwYIBUNNk4t)#8mJF9%*d{JciyVIddEb!(2{b0>&J57OGF?DwJ( z$;04scos-ztkB%u338v#t%Os4TgI(-9-Erz43a7bGRwlB#$#3V(`-dqV zY}pQ5o!r8k5o-Pyed z8kLE-KE)MIpPP@ASY#y6RqNXB@k{J$CyJ4-R({QxQ0n1yw{=moho+15nupz&K z9v&5CTTC>R)!g|Mpo)ag)$@O%IFUU zxXJhW3;Jgzbqt$QP=E7j?$ysTknR}~9i`bE56-Vk!(NxGj{e2W?xVX$<+mq$(y7Xs z4=GgsQgh~XPndL}D4kr%bcHAtZzT%hJIzJ{7Y|>{TT1h~u;$w*H^N!6j`zMlEb<<+ zKfZ?%oCh-RbbnFvDt0aNO^|aTMg{DuJjX*3i-A_g&g=ga(6X()5?!OB%^#OIx~#vv+31=i|fTlE^x;%z2%%ZmHjzEPKQ%Y_@T_%C(p2c;&Pt{|BMUa z7++Z)XS({yz)y$s6Vs!NHxI90w7H{F!tWMO8uv6$ew}W>yqc+&0qrw`x4XY!YlE7u zSKo@e-|dS)XStKCJ#roHH`ni&GknqZs_AOhd7pdh#Y?-hBwI`U;I ztm^u(TI`yc2NK;q9#6FqPK>*KcVhTrH-73y+is7(z?L{A_EY0Z$jE1}yRHu7X2W|k z;bG>(zWG8wtR{55>C}AZtV7ky*QB1y)qNA$&G50*5DjT!X1|7C75vSh3KwvWYI3#2 zC4|$$=Q7p#K4xaBs`B+Vb$6%^I7EMT`m~dg>Pt>)q})rZQHh~f@D60HHWHY??ifz~ zy%)wP(V}-M?x9P;vGxAcl_V}cZB60+#?-bqGuqdq@$0SygT@V-duskU*2U*&<;|1* zeHkNPb!*#pP=Qf)Bn-ZI5`a>74JaoGzWVi#|w>sHiSNlFw zlX9D!8IC<%_eM&K4F^1g4Y^XhI)w^Nm~veAfhjCbpt?PjE_KwlpKo5wENb<6mBL+D zFb?l-o7_KX^JiV`fa*|*m0j#GSdiLUF4k6T_&h6kK;?hT#+q*61}JRRKDrUmY;_@8 z|AZW<=rtIXI6Q2kZ_z+$_xgNihFw)+b?^*z1T6?z>uAw?>T~rx5H(i|$YZUyD2+Mc z72MK~5R5Y4ap*j!0^yEb8qlf$>=aI)bLVbOe&y*$TR~r(XH^Mj%kW9hr~vP!h_i)0 zsTK{HVBi%q&>fIhtKLc2-+upt0kVoe@__0G0MZv{*fw_s;57A`MX!Ki6aJXFB|bT( zn{s}_K*Gv@_dXvOSb8MP_U4!VAL_#mav=N_uKz}W>&v*X-u~>w0-5qM&NIviSS_(- z&KY0^OTi4;aVrNUJQ!fAlRHMBKK#+7haXAan`}SFbBQ0J(s#APlFJvbpX}{NWJM~ee~L0ICnHf)3L|?7?zG^3z1X$+4dtT;RIC=c1ft#JVUuxw zq%))9@Jk*=%n?9lX2 z7kfwaf`LPjGIfOctze+~I=7$pk?YS4(iURKHM#k^aeG%lTy)WBL=G)B?oZe0kCAi0 zwMVYW(&(xSt6PX4*UlE@-OeZb!pekt73XOPoVM~g3xKC#eV*c@r56_K|6hkI%1=;F z`EZqcVe3#&RrfuDNi4+N38~MFqHlh@m|sM)4`0OOyZlgcpjmN`_=_e;aQx+!T1m0_ zOieYgDvk@)%Kh+q5xHS`bN)i{d$`i_Vfr2#K=5yc2>p3E)a`cPYu|(${%29`g=%Wt z4K7ptxB5BGs8fZd8pHrdwT=s2iCf|4M+z<#9=56;f~Ro9EiV?N2}IubPE|VKX2!5> zB>Hz?5#gWE$enN{oLY6WIkm+H1n=6}A4Uy1Fmy<8H>n?Kzl`MGItt$A*fg7gO{rHq zmn>Nu8Dm2)g1U4J=$PL^LX@ciw?h8*q8g|xXisgv9j;HDp(g~R))u|G4aeQYkQu~@ z7oz#V^s7Y#zWsw95M+Wuh*lnVk7ABxpEun%S>S03i0;Q9rzWx++jHxZfy+LJqfX z8uZ}+qAfonw!EcDG;wQIk^m0k7CwH1%3!(-!0(szE_e}f!$Az@U^9z)Hc!7%3aO;s3(A__E|Xvv~4&3T7OO7PUCG}4`}Xf({;LXWXV;*E(!xqDI^Aq z>?IyjCeP7ys>2?5@QS`r?H3&@cc)}bBsNyyAd258Zdn3=kfWDKqd^Vex&%1>_j1?M zRr~E-O}=F0=1@+#;uXcau1;0P5YavBqLsmC+tVAFR0$A$+^EP2H-I3qU!KG6!I8|ySgX-yh-FoYK_ioR0i7nSuHuA(R3Se!UIC!gsm7i zyMr+fK6T;izbnOWP=cS^knPuZ&Rky^HNXQPrNsoU6$9~wk1gRJPJLcP?9?e6E+53( ze$d8<4nKAk2gf;s&)TTH9%>0{+D&ZR?Wf%}19kXhDFT{7!AUAB5jAW`VkGfR@tlUyIJ}TfQI0^=+|8mU)qmI z$*kt9MfX9LsA5iAy9HZQVDpHDS1=ClosB@R-6{1cQ25i&qD(jl#odIvbt|Wi5Jlo8 zdsu!;o}{npaml{;lxJi5cJ{Z%Kf9WTVj^>S6e(-VZg*&ZSKQdBMdOlS+*3KXX8#76 zm2zVGVYXUhpNJ6@X__@bU&_K(WVnEBgE?0r7U2GY>x|ch?ncA4uFBN=Hge$?=1uI= zF@gn+Dz;y2-!CHCy;Y6387bVEa2NelF_@>56moP;e)yF}A32ZwnaYH@Sd^FA8!r7u zyMRr;rVl2NA}b?n_|hEmBrug6+$9PXV$}fAg{gjiVNc4e(xl~RO2s|Ll7yxm^zA=* z*%geVu;RnMWf-?6?I-BoL2Y4P!^1@bd~MvecRS+}R*_W}v|>yFLa^Jj6D=}{nGr10 zsNZaUy&t@lp>fceA>>l4{(Zg_e?vwy7y$F9Ewg3QtQ9l6gwW5JjI;aqYvg? z1*Lv~<|C7&FC5|zJoggIJm&vbAEDKrW|bSTen)Ce_6Y_H0Xs$|Tl7wZ+f^Df<$iQ_ zGTAfIj4m#;@T|JSkl}H%9@)=hA)C65FIK5qvE8Kv&<*-1ruLmL@vPxy<9g5gz5aMq zrSp*iJ&1L@>f*`E+;xTK6FE`&KO_8n!?_#=os?D{)ZsziMQ?pY+fs|G)XZv4 zu!(VNcR73ii+qP=Dn-~(-XqFj|L`?I%WrWfB1D>0kJO!aUv7hqyVC3Ku;-$+6!*#W z2S`caD#?k66Pt=FjN=ft5IyYPi)r`(#dZs6r1r`FcvMkF+9s*HdzW8twj*(mJ&a*8 zyPLa}T$%+56j|iq-tKxG|1Fxg+Klv9P;~B6$i^(@=ZA0l7zcwha^vSqwl3|w#3)5F zne{}jGZu$+sxwb`h`Nm;b?x30Ni!-SF9CEUJM(O#zPaZU+D63ky@=t#XL3xjINhsL=DQ8(gg0SDFN0P417(U;$E&0*E~$?_;8URHDsEg z1Id~6(NLz9T1UB7$tbGbv-vFfRjxk@ro)5Yg)Pk(SNh2SO;3>JM2#h(NvNvZiN$$# zqrt;4Nxv{BoiD8YiZ1i`$2kSMC>|OR6|#T+Z1QEzr<`*8)7N05zgv|!!CL*m)x!wIyzy6G8_v88XEzQYWN_2@em=PnjH?+k0 z3KuZ>kR%&pu?0v}jfOy%-RWjOVB3KOa;Z&J zhhn~%!5G1;Qs3j~vftNvV^^~bBpM{p@^uH=OyhRga2Cd2&ubuF|DII*P4`t|j}(`QsE51sg6f&M5}gA}WDOyOL`RGxX zNb`eu5XAU0{eWMN;DVY+Lpu@=-fDL|)u7Pcrc*nwT!fv^A2=G78czSRY-;W>cD`io z*Qs5vE=|k4>7@vME+#_{Rf(8fEh%N9yjb~j?ex)keKoV8D&eVaW7Z@f-IXOk%;lxE zdAD5qf#ZyJ^s9goS*(^D+vo$h=iLYBC6}gOxz}TZ+ftL-D{5e9>lc9E?mnwK7fboE zL^sxAzQJg73AWxI(M+N`k`-p`u1-VzZo~!FHk}=RdAKmkIqmk3G&ugWOaHUQ!`k~m zzp4+vv`(NIIN)`oMX_KCsKfL}nb%ugPs6|*LZedr!qAB?s%YbsDvAefu31pUdRY4m zZlL?I(ODcgP4~-C=%4Y*v2-#c$Bnpf;I{$0=D@RAPuS6s^gG5fVodmtVh$fT zY=z?boL81a&Y_5FD24{0_6gJyuo7dz?dUlfbW*@B6p`1FxeuwFrCd&|1)4oYz*WW z7D5r*Cf}2vsoeAV+S!AijK;U0(}Nci5-|_T9;q^u^vGfg#py3^pZ`g~wO`M9@Eu*K zLl#rjg=2@Pad{^kxaBQ00(ifPeZSZxOaqD@=tS)fUr0uCWYLV~)GMM|v;_U#PfIjH zJOZzbER)&QnU5W$-$TtKSBja-Rq)HH;;#$Ovc=pO&}7rbSBDq2b+^aZvJ zFO3R6n)--~RbFynDbA{rdP+>MGaC;7RbkG=8a&XYtP2kezF=^1>BI}$z ztA2^f*StjD;$88SEhN#$)8>rUd!sJm`A1I#hk}w)Z%7g})ABJe$*7kgR928S?>0N!UcT>U#NXRXhpnh)qY_sShBPIUI zu2ZK)Ws#3;%Sx+yz1kbts98c=mS#PVfx5Ei1Fhft z*)sRClP0@Z5th(N%NK;yFAed^+;(k^|+2>v}ZX5we=(?976a z(bDhYj%LHvoO8bSUIG<10pvzYbhhW*)P&~nGP4W4Mb?D2rVo|1wjOe05Oyw!%o#Br zIIdGud_B7~!RG!!D#Cx>-k}~n#tz2EOQ?@u}fT(!Fa}#f}Ods;3 zmF8P(r`NdG2b^tT)Mb@mMn$n98G(&=yJyp@WPXf-Ulyy06>$ozw8xD9Y4mW4O*CWa z(cRihW0-jBDCezQZD(G#fUv}N9@%}nk79FAyh2{b3^WX znLh|ldQ+lW+pKDD)%JI6_Y?%j_kmXcHQgf(>C2K;E^`rmc0Orqo*YeQ$cR5LU5+sX zWM|)-#D7co=s&nI81g0$uY53vuYqQLSqob6D}Om@`lKqfWQw287hUtemrU^!59X~P zNio1E`BcnnVD+jCA;vRo7XFs{WV_wQ#IIImdG@CiT5S8>VzZjgFEx2qDBWZQ= zU~Z}cx^Ij(&AEA`?`jA5W97>cN^jnmU^Sf^6B%K68Jm$OUU=Ld6Md-Wy;pM3d*Sw; z%7~hT5=8CB3AtztXjyBiP?449-_csHGW|R0TEy|_ST#fvURp@H*O3iuuKW9hCX6-IVOjhs03cmxD7`_h|p7!w7e2};FESF`H| zdxm6$5}$aqq;ZdOZf3&yd?f-$l~m+Tuu13WSQWK4zRnRiQzo5^j6RIu74M$Kq zHzmlwQhNQv!#Fw=ZGt4jXWy7x2k5E~ec=w7P(P3qyP~igb$|!gQkH+4<-Lo&F0voU zu;vM8?;BwMQB+BFgQxGm5sNMqTg=Vf${n@cv7nV#&3FoZ_U8byC`ZF!IjL)NN0pa0 ztp65nA7z!=tBRA&U+CPAuX1-{y+qZ9!kkTQ2(((A7O0-&#w<6LHBFNk=)CejvhV6a z=*E{427(&Unm?xIOFORvfdXqK$7IuIYE!Rzzo4?k08W<5N`8Uepk|zfWF_cS6r@Zb zU@MiLoYvU)dWc=<58X$DB&zmwQZP0b<C_PE`;y4Ds}gDS`=m`>2p+NuRrlwB_C4lVPcWR$J6290{y)* zok<^$kClH`zJIb_{<|#a3Z%LZ2|C1A1or(yOlibLy)}*Rqye>`H>~dJvpzaviy3vv zH!wbQZ0G7k_7&wJSmx;z9}`WpH5`HRlC`gOG%`N0ai)*>&=`GLCzw9MfRL{3Iot0F zg_Gk56|x>0db3C+9pF(@M&fOElI}fpN@RUNYId(WBUfSDfcwmZhDse6nR4Z3XxEL~T6mXwtSKHlrNCp4wRo2Enc$e|Shbu=hv(Yi%f;{cNlT`YnPFe3*oE3>gC&O}6=BttWe}`J`ujTX>ek$w9Y=h=rVoB$U*k?|`R)9>M{|ReRPBHIsvi!B6MYyFy%+Zlk&h;Q z8KQ6A${r8;{-$yjaU%pI{!V?EPAS69L2yP1;^XdQ(6f=uynEc!3etSpM3sNx?_`(x z?C_?P!?nT%hIU3#C;!*muF6JgrqxQgw$LqSG4=ePE_cyP<(wf;( z*JKYCJ2>kY38y#sKBGuQ7mKO;d(I zm<2i!3#*;%r)&iUOdG!@X|CY-C?~z|EfOYW0VkSo?AM+d@ykeC4mr|#Jm%T@c?`9L z0-8v*d#YhYWa{>~<(X4`H#Kdagz*mK(-@ioqQ6-7cU9WRdhyU*F+O9P4b_x5I?KgJ zFD8Quy1xcmVJAf?wzybBT(hH4f;xH3Ui? z2#6{rW&B(xkPlgw0kDupVOhK7s~x^C{nua6fe!};T%N#^Yt-d;B8JSVzNuN|VLdj@A2R7aUXig|muwbbJye^slEP;m|-8x4>v zC`Lq1GXJOQ1g_8x6kS`&r1;@t8Y?dcn}6Y_y^T(}hL2Hwn(~dADflTDbVx2919JK} zBHyF+!>~_L?%pbAKm`ZB1ZiD;z5C4ZroGWy6Ln`Y0G7_QU_k2{uke)Yd(7A!REo$E zxlvaBWzOej`xV*@=rVHaKfmM{|0jDgq}b;zaQ2{eP)9G7K`Mw{B($UHCCVBPh^h-C z#dOtMTim>~-`zj3FdLzP$%}KLsXYkAm!JJ(7sn0$Wm(n?lBQ7#x1ddZV8c`l4a(>M zVO6rg>hw#Ae}GC zSd;((r?y|s=}M%{QBY|jB%+B=Nx}ey*O~k!h;AI&7b&fTI?R78f>1PQ#7hv>GjGAF z4U3>E4LFuA!nn8K7mV&#pxzU4HYypQi5|?P2k|faB-VNYTZm`-%1P5S4A`dU7v(*O z#JAO-CVwaU(6nW2KHr(K{JC%{8r0DtT2x%qf(p92C>_n)k=gkNZ4cv+t)QYsjo}_D2CH zg`hKGhZG93BTk>nGvHA+Om+$1-F`QVGtC2zA0U`i_BCOZgVo?J31RbAap-EII@ih% z>$-`HI~mNVWd$ryPp)8;^*$a8+cv-1zaV(lko=z*IhQcX3-$c8ljm`HQVmaOvSZ~E zFyK2U*Da^$Pl3@&dQUY2BN8$-nM*Lxf(0N*P(6af2FT|7O@)0=I&klSFOwh*4Zu&y z-pkv|UZfsdStv(5`r3{LUgOQCL`e->np)?*H((!WZJt0f?8QHtt}+$jfFMKt5h+J9 z&+Jonz}3lcXq`7Qk()rKJuL6o6>vJbXvIv29EmSd!u_Ds z|L&MCd9}vx;UZTB^w36Q%<%qQzJf&p@IfbGr>~PDTU7g_E-(;+aR6p1K?H?(Y11fC z7?g3aEkR6I`2T(5M!)&dhs(c0aaKwy2CW@owFggJ_WcD#AXBs{DCAo0%(?juwU+?} z{e1x(%X{!uheZ4R85P(xA}61hp;a}4z$VLUh10jFub~3p#4nrfyqepcU^@P9PzMm< z1FAXtP6i1i>EvgjA|AG&fpJ4JIR{o^U2= zW34myMoc-zh)fKs_;K`4nWX_!e`N6!KzL*?8sF?x#w=uqM>ma?u~y=}Ip4dh?5* zD@PL1pH%!m0&9Oxn&uIGZopEp6gm@6MPIAxpXnCOz`=tjtEb`A;bu_a8cxYy-s(g* z$!P)*J_9A3I@~<08GA(hi{Jt&1P{yEJVH zyHV$$a6RSYtlq&}`)mp%B*#G&espGZp)YPqz*cmm6eBf)&g3kMgn}yKA%{DA-1BN$ z%1}4Hbg%$`F41|*ZG@R?OfoahA><}J_AWduMG6-_-C=d^^v0&n+z`t%x zkPWp!IvP2;M)V64n&2zCn3 + + + + +codev — Demo Intro + + + + + +
+
+ codev + codev +
+ +

Productive Human-AI Co-development
on Large Codebases: Demo

+ + +
+ + +
+

Vibe coding is not production coding

+ +
    +
  • We've all seen the demos — "build me an app" in minutes
  • +
  • Production is different — tests, review, deployment, docs, maintenance
  • +
  • Real codebases are 10K-100K+ lines. Context windows run out.
  • +
  • Codev is a Human-AI development OS built for this reality
  • +
+
+ + +
+

We used Codev to build Codev

+ +

~80K lines of TypeScript. 289 source files. 14 days.

+ +
    +
  • 106 PRs merged in 14 days — 85% fully autonomous
  • +
  • 20 bugs caught before merge — including 1 security-critical
  • +
  • 26 features shipped — from session management to workspace orchestration
  • +
  • Throughput of 3-4 elite engineers (vs LinearB 2026 benchmarks, 8.1M PRs)
  • +
  • With everything you'd expect: testing, PRs, architecture overview, design docs
  • +
+
+ + +
+
+ codev + codev +
+ +

Let me show you

+
+ + + +
1 / 4
+ + + + + diff --git a/marketing/why-pay-attention.md b/marketing/why-pay-attention.md new file mode 100644 index 00000000..9b750bd1 --- /dev/null +++ b/marketing/why-pay-attention.md @@ -0,0 +1,31 @@ +# Why You Should Pay Attention + +## When Starting a New Project + +We ran the same feature spec through Claude Code and Codev four times. Codev produces far fewer bugs, higher quality code, and manages the whole lifecycle — tests, deployment artifacts, and PRs. + +| Dimension | Claude Code | Codev | Delta | +|-----------|:----------:|:-----:|:-----:| +| **Bugs** | 6.7 | 7.3 | +0.7 | +| **Code Quality** | 7.0 | 7.7 | +0.7 | +| **Maintainability** | 7.3 | 7.3 | 0.0 | +| **Tests** | 5.0 | 6.7 | +1.7 | +| **Extensibility** | 5.7 | 6.3 | +0.7 | +| **NL Interface** | 6.3 | 6.7 | +0.3 | +| **Deployment** | 2.7 | 6.7 | +4.0 | +| **Overall** | **5.8** | **7.0** | **+1.2** | + +Scored by three independent AI reviewers (Claude, Codex, Gemini). Full methodology in the R4 comparison report. + +**No free lunch**: Codev took ~56 minutes vs ~15 minutes for Claude Code, and cost 3-5x more ($14-19 vs $4-7) due to multi-model review overhead. The question is whether the quality delta is worth it for your project. + +## When Maintaining Large Codebases + +We used Codev to build Codev — an ~80K-line TypeScript codebase across 289 source files. + +- **106 PRs merged in 14 days** — 85% completed fully autonomously, no human intervention +- **20 bugs caught before merge** by multi-model review, including 1 security-critical +- **Extensive architectural documentation**, including an accessible `arch.md` that stays current as the codebase evolves +- **Allowed us to ship 26 features in two weeks** — from custom session management to full workspace orchestration +- **Equivalent throughput of 3-4 elite engineers** at $1.59 per PR ($168.64 total for the sprint) +- But with everything you'd expect of production grade: testing, PRs, clear docs, and multi-model code review on every change From b53d8e9b78947222f89cc3b0be7960f85f3077df Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 23 Feb 2026 22:12:51 -0800 Subject: [PATCH 2/3] [Bugfix #548] Fix: Switch analytics to median, fix workspace prefix matching, remove activeBuilders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace computeAvgHours with computeMedianHours for time-to-merge and bug close time (outliers no longer skew the result) - Rename avgTimeToMergeHours → medianTimeToMergeHours and avgTimeToCloseBugsHours → medianTimeToCloseBugsHours - Fix consultation workspace scoping: use prefix match (LIKE) instead of exact match so builder worktree paths (.builders/*) count toward the parent workspace's consultations - Remove activeBuilders from analytics response and dashboard --- .../src/components/AnalyticsView.tsx | 5 +- packages/codev/dashboard/src/lib/api.ts | 5 +- .../agent-farm/__tests__/analytics.test.ts | 135 +++++++++++++----- .../agent-farm/__tests__/tower-routes.test.ts | 14 +- .../codev/src/agent-farm/servers/analytics.ts | 45 +++--- .../src/agent-farm/servers/tower-routes.ts | 9 +- .../consult/__tests__/metrics.test.ts | 25 ++++ .../codev/src/commands/consult/metrics.ts | 9 +- 8 files changed, 164 insertions(+), 83 deletions(-) diff --git a/packages/codev/dashboard/src/components/AnalyticsView.tsx b/packages/codev/dashboard/src/components/AnalyticsView.tsx index 986b85d3..38b342b7 100644 --- a/packages/codev/dashboard/src/components/AnalyticsView.tsx +++ b/packages/codev/dashboard/src/components/AnalyticsView.tsx @@ -129,9 +129,8 @@ function ActivitySection({ activity, errors }: { activity: AnalyticsResponse['ac - - - + + ); diff --git a/packages/codev/dashboard/src/lib/api.ts b/packages/codev/dashboard/src/lib/api.ts index b729c72c..14f3bf0d 100644 --- a/packages/codev/dashboard/src/lib/api.ts +++ b/packages/codev/dashboard/src/lib/api.ts @@ -152,10 +152,9 @@ export interface AnalyticsResponse { timeRange: '24h' | '7d' | '30d' | 'all'; activity: { prsMerged: number; - avgTimeToMergeHours: number | null; + medianTimeToMergeHours: number | null; issuesClosed: number; - avgTimeToCloseBugsHours: number | null; - activeBuilders: number; + medianTimeToCloseBugsHours: number | null; projectsByProtocol: Record; }; consultation: { diff --git a/packages/codev/src/agent-farm/__tests__/analytics.test.ts b/packages/codev/src/agent-farm/__tests__/analytics.test.ts index e2ba2c16..d63cca34 100644 --- a/packages/codev/src/agent-farm/__tests__/analytics.test.ts +++ b/packages/codev/src/agent-farm/__tests__/analytics.test.ts @@ -232,14 +232,14 @@ describe('computeAnalytics', () => { ]), }); - const result = await computeAnalytics('/tmp/workspace', '7', 3); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.timeRange).toBe('7d'); expect(result.activity.prsMerged).toBe(2); - expect(result.activity.avgTimeToMergeHours).toBeCloseTo(30); // (36+24)/2 + expect(result.activity.medianTimeToMergeHours).toBeCloseTo(30); // median of [24, 36] = 30 expect(result.activity.issuesClosed).toBe(2); - expect(result.activity.avgTimeToCloseBugsHours).toBeCloseTo(84); // 3.5 days for bug only - expect(result.activity.activeBuilders).toBe(3); + expect(result.activity.medianTimeToCloseBugsHours).toBeCloseTo(84); // 3.5 days for bug only (single item) + expect(result.activity).not.toHaveProperty('activeBuilders'); // Protocol breakdown now includes count + avgWallClockHours (no "on it" → falls back to PR times) expect(result.activity.projectsByProtocol.spir).toEqual({ count: 1, avgWallClockHours: expect.closeTo(36) }); expect(result.activity.projectsByProtocol.aspir).toEqual({ count: 1, avgWallClockHours: expect.closeTo(24) }); @@ -262,7 +262,7 @@ describe('computeAnalytics', () => { it('does not have github or builders top-level keys', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result).not.toHaveProperty('github'); expect(result).not.toHaveProperty('builders'); expect(result).toHaveProperty('activity'); @@ -270,31 +270,31 @@ describe('computeAnalytics', () => { it('does not have costByProject in consultation', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.consultation).not.toHaveProperty('costByProject'); }); it('returns 24h label for range "1"', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - const result = await computeAnalytics('/tmp/workspace', '1', 0); + const result = await computeAnalytics('/tmp/workspace', '1'); expect(result.timeRange).toBe('24h'); }); it('returns 30d label for range "30"', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - const result = await computeAnalytics('/tmp/workspace', '30', 0); + const result = await computeAnalytics('/tmp/workspace', '30'); expect(result.timeRange).toBe('30d'); }); it('returns all label for range "all"', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - const result = await computeAnalytics('/tmp/workspace', 'all', 0); + const result = await computeAnalytics('/tmp/workspace', 'all'); expect(result.timeRange).toBe('all'); }); it('passes null since date for "all" range', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - await computeAnalytics('/tmp/workspace', 'all', 0); + await computeAnalytics('/tmp/workspace', 'all'); const prCall = execFileMock.mock.calls.find( (c: unknown[]) => (c[1] as string[]).includes('merged'), @@ -305,7 +305,7 @@ describe('computeAnalytics', () => { it('passes a date string for "7" range', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - await computeAnalytics('/tmp/workspace', '7', 0); + await computeAnalytics('/tmp/workspace', '7'); const prCall = execFileMock.mock.calls.find( (c: unknown[]) => (c[1] as string[]).includes('merged'), @@ -322,14 +322,14 @@ describe('computeAnalytics', () => { it('returns GitHub defaults and error when all GitHub calls fail', async () => { execFileMock.mockRejectedValue(new Error('gh not found')); - const result = await computeAnalytics('/tmp/workspace', '7', 2); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.errors?.github).toBeDefined(); expect(result.activity.prsMerged).toBe(0); - expect(result.activity.avgTimeToMergeHours).toBeNull(); + expect(result.activity.medianTimeToMergeHours).toBeNull(); expect(result.activity.issuesClosed).toBe(0); - expect(result.activity.avgTimeToCloseBugsHours).toBeNull(); - expect(result.activity.activeBuilders).toBe(2); + expect(result.activity.medianTimeToCloseBugsHours).toBeNull(); + expect(result.activity).not.toHaveProperty('activeBuilders'); expect(result.activity.projectsByProtocol).toEqual({}); // Consultation still works expect(result.consultation.totalCount).toBe(5); @@ -342,7 +342,7 @@ describe('computeAnalytics', () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); mockSummary.mockImplementation(() => { throw new Error('DB file not found'); }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.errors?.consultation).toBe('DB file not found'); expect(result.consultation.totalCount).toBe(0); @@ -365,10 +365,10 @@ describe('computeAnalytics', () => { successCount: 0, byModel: [], byType: [], byProtocol: [], }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); - expect(result.activity.avgTimeToMergeHours).toBeNull(); - expect(result.activity.avgTimeToCloseBugsHours).toBeNull(); + expect(result.activity.medianTimeToMergeHours).toBeNull(); + expect(result.activity.medianTimeToCloseBugsHours).toBeNull(); expect(result.consultation.avgLatencySeconds).toBeNull(); expect(result.consultation.successRate).toBeNull(); }); @@ -384,8 +384,8 @@ describe('computeAnalytics', () => { ]), }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); - expect(result.activity.avgTimeToCloseBugsHours).toBeCloseTo(24); + const result = await computeAnalytics('/tmp/workspace', '7'); + expect(result.activity.medianTimeToCloseBugsHours).toBeCloseTo(24); }); // --- costByModel derivation --- @@ -400,7 +400,7 @@ describe('computeAnalytics', () => { ], }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.consultation.costByModel).toEqual({ codex: 3.50 }); }); @@ -417,7 +417,7 @@ describe('computeAnalytics', () => { closedIssues: '[]', }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.activity.projectsByProtocol.spir?.count).toBe(2); expect(result.activity.projectsByProtocol.spir?.avgWallClockHours).toBeCloseTo(24); expect(result.activity.projectsByProtocol.air?.count).toBe(1); @@ -435,7 +435,7 @@ describe('computeAnalytics', () => { 42: '2026-02-10T06:00:00Z', }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); // Wall clock should be mergedAt - onIt = 30 hours (not 24 from PR createdAt) expect(result.activity.projectsByProtocol.bugfix?.avgWallClockHours).toBeCloseTo(30); }); @@ -448,7 +448,7 @@ describe('computeAnalytics', () => { closedIssues: '[]', }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); // No "on it" → uses PR createdAt → mergedAt = 24 hours expect(result.activity.projectsByProtocol.bugfix?.avgWallClockHours).toBeCloseTo(24); }); @@ -463,7 +463,7 @@ describe('computeAnalytics', () => { closedIssues: '[]', }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(Object.keys(result.activity.projectsByProtocol)).toEqual(['bugfix']); expect(result.activity.projectsByProtocol.bugfix?.count).toBe(1); }); @@ -476,14 +476,14 @@ describe('computeAnalytics', () => { closedIssues: '[]', }); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.activity.projectsByProtocol).toEqual({}); }); it('returns empty projectsByProtocol when GitHub fails', async () => { execFileMock.mockRejectedValue(new Error('gh not found')); - const result = await computeAnalytics('/tmp/workspace', '7', 0); + const result = await computeAnalytics('/tmp/workspace', '7'); expect(result.activity.projectsByProtocol).toEqual({}); }); @@ -492,8 +492,8 @@ describe('computeAnalytics', () => { it('returns cached result on second call within TTL', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - const result1 = await computeAnalytics('/tmp/workspace', '7', 3); - const result2 = await computeAnalytics('/tmp/workspace', '7', 3); + const result1 = await computeAnalytics('/tmp/workspace', '7'); + const result2 = await computeAnalytics('/tmp/workspace', '7'); expect(result1).toBe(result2); expect(mockSummary).toHaveBeenCalledTimes(1); @@ -502,8 +502,8 @@ describe('computeAnalytics', () => { it('bypasses cache when refresh=true', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - await computeAnalytics('/tmp/workspace', '7', 3); - await computeAnalytics('/tmp/workspace', '7', 3, true); + await computeAnalytics('/tmp/workspace', '7'); + await computeAnalytics('/tmp/workspace', '7', true); expect(mockSummary).toHaveBeenCalledTimes(2); }); @@ -511,8 +511,8 @@ describe('computeAnalytics', () => { it('does not share cache between different ranges', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - await computeAnalytics('/tmp/workspace', '7', 3); - await computeAnalytics('/tmp/workspace', '30', 3); + await computeAnalytics('/tmp/workspace', '7'); + await computeAnalytics('/tmp/workspace', '30'); expect(mockSummary).toHaveBeenCalledTimes(2); }); @@ -522,7 +522,7 @@ describe('computeAnalytics', () => { it('passes workspace filter to MetricsDB.summary()', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - await computeAnalytics('/tmp/my-workspace', '7', 0); + await computeAnalytics('/tmp/my-workspace', '7'); expect(mockSummary).toHaveBeenCalledWith( expect.objectContaining({ workspace: '/tmp/my-workspace' }), @@ -532,7 +532,7 @@ describe('computeAnalytics', () => { it('passes workspace filter for all time ranges', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - await computeAnalytics('/tmp/workspace-a', 'all', 0); + await computeAnalytics('/tmp/workspace-a', 'all'); expect(mockSummary).toHaveBeenCalledWith( expect.objectContaining({ workspace: '/tmp/workspace-a' }), @@ -542,8 +542,8 @@ describe('computeAnalytics', () => { it('different workspaces get different cache entries', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); - await computeAnalytics('/tmp/workspace-a', '7', 0); - await computeAnalytics('/tmp/workspace-b', '7', 0); + await computeAnalytics('/tmp/workspace-a', '7'); + await computeAnalytics('/tmp/workspace-b', '7'); expect(mockSummary).toHaveBeenCalledTimes(2); expect(mockSummary).toHaveBeenCalledWith( @@ -554,6 +554,65 @@ describe('computeAnalytics', () => { ); }); + // --- Regression: median instead of average (#548) --- + + it('uses median (not average) for time-to-merge with outliers', async () => { + // 3 PRs: 2h, 3h, 100h — average=35h, median=3h + mockGhOutput({ + mergedPRs: JSON.stringify([ + { number: 1, title: 'PR', createdAt: '2026-02-10T00:00:00Z', mergedAt: '2026-02-10T02:00:00Z', body: '', headRefName: 'main' }, + { number: 2, title: 'PR', createdAt: '2026-02-10T00:00:00Z', mergedAt: '2026-02-10T03:00:00Z', body: '', headRefName: 'main' }, + { number: 3, title: 'PR', createdAt: '2026-02-10T00:00:00Z', mergedAt: '2026-02-14T04:00:00Z', body: '', headRefName: 'main' }, + ]), + closedIssues: '[]', + }); + + const result = await computeAnalytics('/tmp/workspace', '7'); + // Median of [2, 3, 100] = 3 (middle value) + expect(result.activity.medianTimeToMergeHours).toBeCloseTo(3); + }); + + it('uses median (not average) for bug close time with outliers', async () => { + // 3 bugs: 1h, 2h, 200h — average=67.67h, median=2h + mockGhOutput({ + mergedPRs: '[]', + closedIssues: JSON.stringify([ + { number: 1, title: 'Bug 1', createdAt: '2026-02-10T00:00:00Z', closedAt: '2026-02-10T01:00:00Z', labels: [{ name: 'bug' }] }, + { number: 2, title: 'Bug 2', createdAt: '2026-02-10T00:00:00Z', closedAt: '2026-02-10T02:00:00Z', labels: [{ name: 'bug' }] }, + { number: 3, title: 'Bug 3', createdAt: '2026-02-10T00:00:00Z', closedAt: '2026-02-18T08:00:00Z', labels: [{ name: 'bug' }] }, + ]), + }); + + const result = await computeAnalytics('/tmp/workspace', '7'); + // Median of [1, 2, 200] = 2 (middle value) + expect(result.activity.medianTimeToCloseBugsHours).toBeCloseTo(2); + }); + + it('computes median correctly for even number of items', async () => { + // 4 PRs: 1h, 2h, 10h, 20h — median = (2+10)/2 = 6 + mockGhOutput({ + mergedPRs: JSON.stringify([ + { number: 1, title: 'PR', createdAt: '2026-02-10T00:00:00Z', mergedAt: '2026-02-10T01:00:00Z', body: '', headRefName: 'main' }, + { number: 2, title: 'PR', createdAt: '2026-02-10T00:00:00Z', mergedAt: '2026-02-10T02:00:00Z', body: '', headRefName: 'main' }, + { number: 3, title: 'PR', createdAt: '2026-02-10T00:00:00Z', mergedAt: '2026-02-10T10:00:00Z', body: '', headRefName: 'main' }, + { number: 4, title: 'PR', createdAt: '2026-02-10T00:00:00Z', mergedAt: '2026-02-10T20:00:00Z', body: '', headRefName: 'main' }, + ]), + closedIssues: '[]', + }); + + const result = await computeAnalytics('/tmp/workspace', '7'); + // Median of [1, 2, 10, 20] = (2+10)/2 = 6 + expect(result.activity.medianTimeToMergeHours).toBeCloseTo(6); + }); + + // --- Regression: activeBuilders removed (#548) --- + + it('does not include activeBuilders in response', async () => { + mockGhOutput({ mergedPRs: '[]', closedIssues: '[]' }); + const result = await computeAnalytics('/tmp/workspace', '7'); + expect(result.activity).not.toHaveProperty('activeBuilders'); + }); + }); // --------------------------------------------------------------------------- diff --git a/packages/codev/src/agent-farm/__tests__/tower-routes.test.ts b/packages/codev/src/agent-farm/__tests__/tower-routes.test.ts index ce1df3e3..e70734cb 100644 --- a/packages/codev/src/agent-farm/__tests__/tower-routes.test.ts +++ b/packages/codev/src/agent-farm/__tests__/tower-routes.test.ts @@ -1017,7 +1017,7 @@ describe('tower-routes', () => { describe('GET /api/analytics', () => { const fakeStats = { timeRange: '7d', - activity: { prsMerged: 5, avgTimeToMergeHours: 2.5, issuesClosed: 4, avgTimeToCloseBugsHours: 1.2, activeBuilders: 1, projectsByProtocol: { spir: { count: 2, avgWallClockHours: 36 }, bugfix: { count: 1, avgWallClockHours: 2.5 } } }, + activity: { prsMerged: 5, medianTimeToMergeHours: 2.5, issuesClosed: 4, medianTimeToCloseBugsHours: 1.2, projectsByProtocol: { spir: { count: 2, avgWallClockHours: 36 }, bugfix: { count: 1, avgWallClockHours: 2.5 } } }, consultation: { totalCount: 10, totalCostUsd: 0.5, costByModel: {}, avgLatencySeconds: 12, successRate: 90, byModel: [], byReviewType: {}, byProtocol: {} }, }; @@ -1034,7 +1034,7 @@ describe('tower-routes', () => { expect(statusCode()).toBe(200); const parsed = JSON.parse(body()); expect(parsed.activity.prsMerged).toBe(5); - expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '7', 0, false); + expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '7', false); }); it('returns 400 for invalid range', async () => { @@ -1052,7 +1052,7 @@ describe('tower-routes', () => { const { res } = makeRes(); await handleRequest(req, res, makeCtx()); - expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '7', 0, false); + expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '7', false); }); it('passes refresh=true when refresh=1 query param is set', async () => { @@ -1060,7 +1060,7 @@ describe('tower-routes', () => { const { res } = makeRes(); await handleRequest(req, res, makeCtx()); - expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '30', 0, true); + expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '30', true); }); it('returns default empty response when no workspace is available', async () => { @@ -1074,7 +1074,7 @@ describe('tower-routes', () => { const parsed = JSON.parse(body()); expect(parsed.timeRange).toBe('30d'); expect(parsed.activity.prsMerged).toBe(0); - expect(parsed.activity.activeBuilders).toBe(0); + expect(parsed.activity).not.toHaveProperty('activeBuilders'); expect(mockComputeAnalytics).not.toHaveBeenCalled(); }); @@ -1083,7 +1083,7 @@ describe('tower-routes', () => { const { res } = makeRes(); await handleRequest(req, res, makeCtx()); - expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', 'all', 0, false); + expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', 'all', false); }); it('accepts range=1 (24h)', async () => { @@ -1091,7 +1091,7 @@ describe('tower-routes', () => { const { res } = makeRes(); await handleRequest(req, res, makeCtx()); - expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '1', 0, false); + expect(mockComputeAnalytics).toHaveBeenCalledWith('/tmp/workspace', '1', false); }); }); }); diff --git a/packages/codev/src/agent-farm/servers/analytics.ts b/packages/codev/src/agent-farm/servers/analytics.ts index fae10314..cff8abc8 100644 --- a/packages/codev/src/agent-farm/servers/analytics.ts +++ b/packages/codev/src/agent-farm/servers/analytics.ts @@ -32,10 +32,9 @@ export interface AnalyticsResponse { timeRange: '24h' | '7d' | '30d' | 'all'; activity: { prsMerged: number; - avgTimeToMergeHours: number | null; + medianTimeToMergeHours: number | null; issuesClosed: number; - avgTimeToCloseBugsHours: number | null; - activeBuilders: number; + medianTimeToCloseBugsHours: number | null; projectsByProtocol: Record; }; consultation: { @@ -108,19 +107,22 @@ function rangeToSinceDate(range: RangeParam): string | null { // GitHub metrics computation // ============================================================================= -function computeAvgHours(items: Array<{ start: string; end: string }>): number | null { +function computeMedianHours(items: Array<{ start: string; end: string }>): number | null { if (items.length === 0) return null; - const totalMs = items.reduce((sum, item) => { - return sum + (new Date(item.end).getTime() - new Date(item.start).getTime()); - }, 0); - return totalMs / items.length / (1000 * 60 * 60); + const hours = items + .map(item => (new Date(item.end).getTime() - new Date(item.start).getTime()) / (1000 * 60 * 60)) + .sort((a, b) => a - b); + const mid = Math.floor(hours.length / 2); + return hours.length % 2 === 0 + ? (hours[mid - 1] + hours[mid]) / 2 + : hours[mid]; } interface GitHubMetrics { prsMerged: number; - avgTimeToMergeHours: number | null; + medianTimeToMergeHours: number | null; issuesClosed: number; - avgTimeToCloseBugsHours: number | null; + medianTimeToCloseBugsHours: number | null; mergedPRList: MergedPR[]; } @@ -142,8 +144,8 @@ async function computeGitHubMetrics( const prs = mergedPRs ?? []; const prsMerged = prs.length; - // Average time to merge - const avgTimeToMergeHours = computeAvgHours( + // Median time to merge + const medianTimeToMergeHours = computeMedianHours( prs.filter(pr => pr.mergedAt).map(pr => ({ start: pr.createdAt, end: pr.mergedAt })), ); @@ -151,19 +153,19 @@ async function computeGitHubMetrics( const closed = closedIssues ?? []; const issuesClosed = closed.length; - // Average time to close bugs + // Median time to close bugs const closedBugs = closed.filter(i => i.labels.some(l => l.name === 'bug') && i.closedAt, ); - const avgTimeToCloseBugsHours = computeAvgHours( + const medianTimeToCloseBugsHours = computeMedianHours( closedBugs.map(i => ({ start: i.createdAt, end: i.closedAt })), ); return { prsMerged, - avgTimeToMergeHours, + medianTimeToMergeHours, issuesClosed, - avgTimeToCloseBugsHours, + medianTimeToCloseBugsHours, mergedPRList: prs, }; } @@ -337,13 +339,11 @@ async function computeProjectsByProtocol( * * @param workspaceRoot - Path to the workspace root (used as cwd for gh CLI) * @param range - Time range: '1', '7', '30', or 'all' - * @param activeBuilders - Current active builder count (from tower context) * @param refresh - If true, bypass the cache */ export async function computeAnalytics( workspaceRoot: string, range: RangeParam, - activeBuilders: number, refresh = false, ): Promise { const cacheKey = `${workspaceRoot}:${range}`; @@ -369,9 +369,9 @@ export async function computeAnalytics( errors.github = msg; githubMetrics = { prsMerged: 0, - avgTimeToMergeHours: null, + medianTimeToMergeHours: null, issuesClosed: 0, - avgTimeToCloseBugsHours: null, + medianTimeToCloseBugsHours: null, mergedPRList: [], }; } @@ -405,10 +405,9 @@ export async function computeAnalytics( timeRange: rangeToLabel(range), activity: { prsMerged: githubMetrics.prsMerged, - avgTimeToMergeHours: githubMetrics.avgTimeToMergeHours, + medianTimeToMergeHours: githubMetrics.medianTimeToMergeHours, issuesClosed: githubMetrics.issuesClosed, - avgTimeToCloseBugsHours: githubMetrics.avgTimeToCloseBugsHours, - activeBuilders, + medianTimeToCloseBugsHours: githubMetrics.medianTimeToCloseBugsHours, projectsByProtocol, }, consultation: consultMetrics, diff --git a/packages/codev/src/agent-farm/servers/tower-routes.ts b/packages/codev/src/agent-farm/servers/tower-routes.ts index 95806014..1780eadc 100644 --- a/packages/codev/src/agent-farm/servers/tower-routes.ts +++ b/packages/codev/src/agent-farm/servers/tower-routes.ts @@ -719,18 +719,13 @@ async function handleAnalytics(res: http.ServerResponse, url: URL, workspaceOver if (!workspaceRoot) { res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ timeRange: rangeLabel, activity: { prsMerged: 0, avgTimeToMergeHours: null, issuesClosed: 0, avgTimeToCloseBugsHours: null, activeBuilders: 0, projectsByProtocol: {} }, consultation: { totalCount: 0, totalCostUsd: null, costByModel: {}, avgLatencySeconds: null, successRate: null, byModel: [], byReviewType: {}, byProtocol: {} } })); + res.end(JSON.stringify({ timeRange: rangeLabel, activity: { prsMerged: 0, medianTimeToMergeHours: null, issuesClosed: 0, medianTimeToCloseBugsHours: null, projectsByProtocol: {} }, consultation: { totalCount: 0, totalCostUsd: null, costByModel: {}, avgLatencySeconds: null, successRate: null, byModel: [], byReviewType: {}, byProtocol: {} } })); return; } const range = rangeParam as '1' | '7' | '30' | 'all'; const refresh = url.searchParams.get('refresh') === '1'; - // Get active builder count from workspace terminals - const wsTerminals = getWorkspaceTerminals(); - const entry = wsTerminals.get(normalizeWorkspacePath(workspaceRoot)); - const activeBuilders = entry?.builders.size ?? 0; - - const data = await computeAnalytics(workspaceRoot, range, activeBuilders, refresh); + const data = await computeAnalytics(workspaceRoot, range, refresh); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); } diff --git a/packages/codev/src/commands/consult/__tests__/metrics.test.ts b/packages/codev/src/commands/consult/__tests__/metrics.test.ts index 01f3468a..b98caf9e 100644 --- a/packages/codev/src/commands/consult/__tests__/metrics.test.ts +++ b/packages/codev/src/commands/consult/__tests__/metrics.test.ts @@ -390,6 +390,31 @@ describe('Workspace filtering (#545)', () => { expect(proj).toBeDefined(); expect(proj!.totalCost).toBeCloseTo(10.00); }); + + // --- Regression: prefix match for builder worktree paths (#548) --- + + it('includes builder worktree paths that are children of the workspace', () => { + db.record(sampleRecord({ workspacePath: '/projects/codev/.builders/bugfix-535-fix', costUsd: 7.00 })); + + const summary = db.summary({ workspace: '/projects/codev' }); + // Should include the worktree record via prefix match + expect(summary.totalCount).toBe(3); // 2 existing + 1 builder worktree + }); + + it('does not include unrelated workspace paths via prefix match', () => { + db.record(sampleRecord({ workspacePath: '/projects/codev-other', costUsd: 7.00 })); + + const summary = db.summary({ workspace: '/projects/codev' }); + // Should NOT include /projects/codev-other (different workspace, not a subpath) + expect(summary.totalCount).toBe(2); // only the 2 original /projects/codev records + }); + + it('handles workspace paths with trailing slash', () => { + db.record(sampleRecord({ workspacePath: '/projects/codev/.builders/spir-42', costUsd: 4.00 })); + + const summary = db.summary({ workspace: '/projects/codev/' }); + expect(summary.totalCount).toBe(3); // 2 existing + 1 builder + }); }); // Test 8: CLI flag acceptance (--protocol, --project-id) diff --git a/packages/codev/src/commands/consult/metrics.ts b/packages/codev/src/commands/consult/metrics.ts index 9cd98d8d..c7835ea0 100644 --- a/packages/codev/src/commands/consult/metrics.ts +++ b/packages/codev/src/commands/consult/metrics.ts @@ -150,8 +150,13 @@ function buildWhereClause(filters: StatsFilters): { where: string; params: Recor params.filterProject = filters.project; } if (filters.workspace) { - conditions.push('workspace_path = @filterWorkspace'); - params.filterWorkspace = filters.workspace; + // Prefix match: builder worktree paths like /repo/.builders/bugfix-42 + // should match when filtering by /repo. Also handles repo renames + // (e.g. codev-public → codev) by matching on the workspace root's parent. + const ws = filters.workspace.endsWith('/') ? filters.workspace.slice(0, -1) : filters.workspace; + conditions.push("(workspace_path = @filterWorkspace OR workspace_path LIKE @filterWorkspacePrefix)"); + params.filterWorkspace = ws; + params.filterWorkspacePrefix = ws + '/%'; } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; From 076c1a8bd4a2909d4a0ed99455cfd93176fc219c Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 23 Feb 2026 22:18:20 -0800 Subject: [PATCH 3/3] [Bugfix #548] Address CMAP feedback: fix dashboard tests, stale comments - Update dashboard test fixtures to use new property names (medianTimeToMergeHours, medianTimeToCloseBugsHours, no activeBuilders) - Fix stale doc comment in analytics.ts (removed builder count reference) - Fix stale test description string referencing avgTimeToCloseBugsHours - Fix inaccurate comment in metrics.ts about repo renames --- packages/codev/dashboard/__tests__/analytics.test.tsx | 10 ++++------ .../codev/src/agent-farm/__tests__/analytics.test.ts | 2 +- packages/codev/src/agent-farm/servers/analytics.ts | 3 +-- packages/codev/src/commands/consult/metrics.ts | 3 +-- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/codev/dashboard/__tests__/analytics.test.tsx b/packages/codev/dashboard/__tests__/analytics.test.tsx index b8c436d9..ce1c2657 100644 --- a/packages/codev/dashboard/__tests__/analytics.test.tsx +++ b/packages/codev/dashboard/__tests__/analytics.test.tsx @@ -27,10 +27,9 @@ function makeStats(overrides: Partial = {}): AnalyticsRespons timeRange: '7d', activity: { prsMerged: 12, - avgTimeToMergeHours: 3.5, + medianTimeToMergeHours: 3.5, issuesClosed: 6, - avgTimeToCloseBugsHours: 1.2, - activeBuilders: 2, + medianTimeToCloseBugsHours: 1.2, projectsByProtocol: { spir: { count: 3, avgWallClockHours: 48.2 }, bugfix: { count: 2, avgWallClockHours: 1.5 }, @@ -240,10 +239,9 @@ describe('AnalyticsView', () => { const stats = makeStats({ activity: { prsMerged: 3, - avgTimeToMergeHours: null, + medianTimeToMergeHours: null, issuesClosed: 0, - avgTimeToCloseBugsHours: null, - activeBuilders: 0, + medianTimeToCloseBugsHours: null, projectsByProtocol: {}, }, }); diff --git a/packages/codev/src/agent-farm/__tests__/analytics.test.ts b/packages/codev/src/agent-farm/__tests__/analytics.test.ts index d63cca34..1d6b31ba 100644 --- a/packages/codev/src/agent-farm/__tests__/analytics.test.ts +++ b/packages/codev/src/agent-farm/__tests__/analytics.test.ts @@ -375,7 +375,7 @@ describe('computeAnalytics', () => { // --- Bug-only avg time to close --- - it('only counts bug-labeled issues for avgTimeToCloseBugsHours', async () => { + it('only counts bug-labeled issues for medianTimeToCloseBugsHours', async () => { mockGhOutput({ mergedPRs: '[]', closedIssues: JSON.stringify([ diff --git a/packages/codev/src/agent-farm/servers/analytics.ts b/packages/codev/src/agent-farm/servers/analytics.ts index cff8abc8..52ba3a29 100644 --- a/packages/codev/src/agent-farm/servers/analytics.ts +++ b/packages/codev/src/agent-farm/servers/analytics.ts @@ -1,10 +1,9 @@ /** * Analytics aggregation service for the dashboard Analytics tab. * - * Aggregates data from three sources: + * Aggregates data from two sources: * - GitHub CLI (merged PRs, closed issues, protocol breakdown from branch names) * - Consultation metrics DB (~/.codev/metrics.db) - * - Active builder count (passed in from tower context) * * Each data source fails independently — partial results are returned * with error messages in the `errors` field. diff --git a/packages/codev/src/commands/consult/metrics.ts b/packages/codev/src/commands/consult/metrics.ts index c7835ea0..abb8af67 100644 --- a/packages/codev/src/commands/consult/metrics.ts +++ b/packages/codev/src/commands/consult/metrics.ts @@ -151,8 +151,7 @@ function buildWhereClause(filters: StatsFilters): { where: string; params: Recor } if (filters.workspace) { // Prefix match: builder worktree paths like /repo/.builders/bugfix-42 - // should match when filtering by /repo. Also handles repo renames - // (e.g. codev-public → codev) by matching on the workspace root's parent. + // should match when filtering by /repo. const ws = filters.workspace.endsWith('/') ? filters.workspace.slice(0, -1) : filters.workspace; conditions.push("(workspace_path = @filterWorkspace OR workspace_path LIKE @filterWorkspacePrefix)"); params.filterWorkspace = ws;