From 9481be1cfd37b3ad464aec6ff37b272107cf60e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:00:46 +0000 Subject: [PATCH 1/4] Initial plan From 16de0024a1c638f8b6b72320f4b183e0437c3ccd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:16:51 +0000 Subject: [PATCH 2/4] Convert autoloop and sync-branches workflows from Python to JavaScript (actions/github-script) Replace python3 heredocs with node heredocs in: - workflows/autoloop.md: pre-step scheduling logic - workflows/sync-branches.md: branch sync logic Update test infrastructure: - tests/conftest.py: extract JS functions and call via Node.js subprocess - tests/test_scheduling.py: update docstrings and line references All 88 existing tests pass. Agent-Logs-Url: https://github.com/githubnext/autoloop/sessions/639174de-6b45-471f-878b-05ffaca3cdfa Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- .../conftest.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 5870 bytes ...st_scheduling.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 101713 bytes tests/conftest.py | 117 ++- tests/test_scheduling.py | 37 +- workflows/autoloop.md | 803 ++++++++++-------- workflows/sync-branches.md | 165 ++-- 6 files changed, 628 insertions(+), 494 deletions(-) create mode 100644 tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_scheduling.cpython-312-pytest-9.0.2.pyc diff --git a/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b231d5d9c55c403e0f1231c761234b928ee4c8a7 GIT binary patch literal 5870 zcmbtYO>7&-6`ox#|CT?ZMA?!o+1^ODCAtc2Id+^nb{xl+5!;I7T5ja1Y8GqmN+Lt< zGBZodVkwkp4b*B3NJ0-%&;Ze)C=xh_9CJ*7rcKd9snV^POO=c&E* zGAn(Lo_L0kH$g6dX1cLnEdyybhw*k(2!X@S%Mr(a`wxB8-b@AB1ATUFmJ zG`>UiL%I|C?O-ynA|lkasR2ma)gYuDTAS9cb*Q0RNDaRyjPFvLptf_Ck42POP+A0X~w{BO{R?*DT}pdiK%6! zX$@IW8Wi)}4SyM_FswO3MNs)+_Rj^`e zQpNx!w0>zulc0uZBAbvLQgWt|HjHdMqxMT$Rc$(W!-!R7pg&H+Xtb4CBdOYCQcv23 ziKYRQnp!3+oi#EUL%%p$n_1CSDWRm(v~xzfkyNB%P&1w)63Jc9Vk4mu0>)mQP7+De zNe*iSMj|GWl=O@;OC+2Fi>8z$tcI-stZq(8*H?~O9T&f&A9FayS$;E_(NryMDtSI0 zxBNAO^MMA7^PTKPrjyCcDiTkmNTRw&?2Miu7yKS|LAs30>dr*StnMF#*ls8p z+>PCxGTTve9Go5aV|$N7rk6>le5_Q@RsNTp$<8oJ+2_38N=tXCZt%?U1)luE3^{KI zr2q-GKB|jpstS2%q-V=)W>TpZR;uSr?;D8hXc)V-F2PC#o{fzgC3mv+dR~{*In|To z@?hVb%j{qh4fE_`b@jYZvw_X`E3Uw&cGo3X2~K$RTg?dtVLL=peKiU96q@rC+~?5G zkAgPdf;aUH8>gPL)kyW!>sU$kv#~JXsU9tOQu|q3JulRB;?SMv1*G~?z4caBDtOpf zbH0ZC*2mgak?o)Aj{6a7spo~7Z5-W`+eoMZHj9n?n|9@Fum=qw!p(p!Zy*l-7P&jG z-*ml!X1J{_vq7gm7Rq0`OfjZ*G&f<4M1g?84B!JhaTG^L9S|7~#)mh$b&6-tN+(ZB zy%$FZN6z<7=zZ~nr(+ZP1UYEee0DvrRCjT>0bTBop-OL&oVCt0g3kO?Y zQxeljT^lt4c>A5IQO66w>y&0*#>Ny@GQ&y+-Z3D1wZiTo&AljIjqS7qBdh6FXl&%l zrSn50WAf!QS6{URDsAytvphsoaAMl>oEy1%W@yL~U(U|j0H$E8S)Md-Mlof_v<#WD zg3v5q&n45~If&&+8-T>bYM040T{anOSu9Vbjq6HU)w?GShoUBidycBt)ydck%$hd_ZzT8d|9VU5y;qRMRh)Exw- zN2witq#yQ55q;|q-N}^wUF(Q*cdl}kmgw?;#{YZ5;OzmYqFgW5Br2is!r1)S;!oZgFP>lb zp-^;TXnts=wH(}29IP})ix(@A9S@PqE!`F>fu@BM^C#Xpc|Xv-7U(YZyl^k@<1d5F z6<^DJU&orSV`bkzeS6ovAiOT3XxnmZDYkO4+|paT^w@*M@Ev1?l>I&H$n6$a->&%l z3*LF}V&I;yt6E;?xzIK4vCHKbpSVG2J&b~pC&$6^2XsL;p7gQG4bldyw%9y)%=Njy z=REKF{Fr<2wD-@v`+UHgs6sVJAVeJ=b&HQyptjNFrfM#iiaC#2bGHSK0UFEa_y*@m zx5bu+5?ThOb1v0YaM^Crc8&$)xJa57c)RSO>6~B&*f6OeE7fz=tNIGUluPx)pZHPW zZk4>wxeM-XopSK4;|SqBbw{j*s&j|!x!v12X4=B61PAC&ZR3(tbiKDccdPG&n{BGQ zmK5F3Tb}L5WOI(9SR_Ammhw6UK(~&QHD+6ddQGiCaFr6;jsR_yROaKxcC6VD5M@pwEx0&R>)5>qvDX55CA zzKf$H!*Pm9`c!gqwvY8ZbxO+VHhTr-PD^?Yj5q<4Q8i*PmL*Wfljon+u6{=uSIlLF z6G|2o)?^qwn=>uJ1oJE}rJ7KK>9)-8WV}|RcxiK9O4sxJ328VM0t6%!Fx6ZpODsSw z&`W}DmKV@AtpK=*bv|sl0In?2StQDDt>BfMPG@tl*aK$5EP#u)xtK;^0FD4yB<8XF zHW=D?E+}}4Sbmzy_WVJOfGL_PXBCRh#10%Rk7GQc2hRace2@<5lPPnDp7;d2lB47Bvw|&jqzH;Ut-rZk^ca=Jy`#8DQdHU0RYn|u6 z*!lO}f8D*-`Fc4#R`QKi0?iAj=1<)Z>{$!!xfhTsP0_C%Y+RgN&Malhfqf-m-y^UN zZNegYdMU+zM6GYcB9$wk8g_66uE@>uW|fQsVsIbTh$@^FA^+;-S##O|K7y%7#1gnR zdT#8XjIo3`L{It+U?oJ#B-5}(G}!VgghPf@3u@w4Xj2~=@&p^DW4!&bFjXbSTO6_Y z3?X!fN|N(c+u-8^O_Qr3swP);!k0kk3jD}3kWkOoym)AJpzPmYe5K;`7xVU-wXSyG z3+yim`|%)1ZNFB#pym743RZ*9_dR7b8fjGAYPNN4tox9&4UBQPhw4flN7b;ds$m(2 zh{?c1%(`_WAUIBYL?8j{0?|@y?+0z=z(7eD_#wvZ-`<$bj4y75H&@_cPy;?5pPg*9 zp+g2S8Q_dkMVpk>KF=t0khd7HZ}oaP5Gx6>A7H?+71_#0nQ{U}kp}fPd?(XN=DMo9 zgvUUWX{rqkG>hmDH%o_K`Se`5_d*fby2J!4ym_BlQRjWr=dmJN?X_B{HnklK+81<2KtiB8Q{-)QFtkgKdg^{%=IyNGhHdkprE;%>`Gg7L$Xz)u$ebkHQ_- zDB2oR-8oy0soeQngHOGhksQ)n51<%hg*z1r<@UV_T&*Y&6r~yPKroPnizs%RG)X;R zQm0Is4VRphbPcXi)VMv$Fs6%+DTT67%!?`S!PG8HzdmlF2|ZSr-X>sr{9^)sotC?4 zLJz7<>j{NZsun|{Doc=^*{N4DHLT+|K`aeFLJw>afja;y`V^M^txw#RV|VPjJ+N?m z{`lg+J10s)_bLy`6EE;mo2_pU%H$TC&-A!H>v5my_r|&`uPnpun=D&C7B*o@t*j2& z3AEx)a0S6n9BVes0QbrwCF;!WI!y0U@N={yZxCmFfaQTlrI%!r$+TwqQ~)Om>o!tb z0(HokUSwKcxF)o{w@;R-s4Uw{ZHaJFXm@C~se>JfIABka8&iP86eKK<9j*|289#K6 zqrKp8e4XI+C7b~{3Z3MSkgU5nj(foSIN?bv;<#Jr*MYB)-~Mm@3N`&3?fE9svg}>* zE(e!_D_7nhdw1-8`Ca*gYvtX~ey3 z(VF@0=KiOuySitlF#}Q{ElX$)4^CBARrl1?Id$sPIj8<>dAZMq>#vq1BY)Lpv;8e4 z^vk6ow@=$`wpVSk?X*p{%Z{l1w1fS1_PRP9c3ZFew8zfxcu#wA#~GD+rPESx$?1|_ z-)SGa=Zcp0mYpuM+rnkCdlec!T@K8{unJ&ahWTab^S0BKatYuP*$22(E(NTT%K)q8 z8oB)Q_S4H8w#RL9#pi9Z|CO+ioA6!Zyoa1i@fzLZv*lIB5woB+lahPA}=uH2yPxM z#n)&Lt1&gu)vp9%J>h`b*%OurqmjPuzs?VFxPXKbiG`6(_waSmC((hh zqWY-GfD#^vMmq5|SiSH?bguro1MJQE8UyV;VKvOUA6+@v8}6h2zYvM_P#vHCtbp1- zsC0($P3Q~Z_rlE~d=o_ps{{BxfmnZ|?*%)1FQsNGsAs|*+NcR^Ap#q7?0{8(N(CgkEluoU4UR313o9A78Uv*S9T|5l5{rg2E_#$o9TN!L+1u39-y3dH z2K)NLN)!Fkf;tU^29?@GQ2&+i79grjl{V5l*b8>B_LVBMydMA6y#SYNe^*-m#a)+o zU1=Dr8Vi5p-0SCly*FLDW2SUZs&vo9_H^liONZaliaa!0H7ch|Yc3t0E%jge_-)j; zW&O_dd$8a6IlTV-e&-l9!mhLWoSuxlaSQ*V3N`O2@jYzarg z+2uH8tFgtZ^{2#^>sXg#vrTc@ZJ)Hg^5zBGMdv4N7wo~(_(^LDwP5DC+!&;)@hF2> z*+ds#{^Hgb)VjSJ8yXtx4m33EN8*LPU|q%rM(FN}hPu_Z_{tX>8XDM*ttb?Hp-)fV zAdYj#?u(b`Omfd1`<{43ETn)fHuQ!;;GKJdWl9;!DKt-l@jVxc4u+2?O23jRX&)NkXwaMSV$O13nr>eh5lY0jJapZ`I~j{H_J#}y&e2LTr^E)%0zLrpwhzw{8E)5Z{GWa7FOGfp*wl*WlCJ96Wvec=%~r3x z^wc~4C11RF`C@YA_T#@8(OYyOl{aP zvtdta!=Chpy;sWK+1PZgEWL5xm5Q9Y$=Zjf{kv~EoJ&@{vpG1v`M2w?)?GU_u`yk@ zcjB2;-GQ09BdNM0>AJ_$o1gg7Q#Z@;{F_T{WvfT`B}=y^U0eU>58bv^HD5oLT+w*T zW?!=E`_(INI`Q|8N*(pbo9hoRcYJ5L|L|7#cWUi`S+BTpYcc<9K{po5L^j7JTJl+K=@ zU8$i_LG%&egU26HJIt@8G~nKDDzpjUlI;iH+VO`|-d6ll?|4goZs@0ml1sLXd#Ak3 z$eAszRESSx^`j6j9>ZhIlMaLkO;A7;aglhT_n*#;3JDvs^W0XvUDaHz>21iaLHhOF zbCeS-Sy>^wWbYiMB)LR?x3AfL*+Hyvj*`A^F$&~Txoou!;*Jo9Al6Wq<(C2_@iEHSZ|3()xjkTqBJ`G{?t495mZ8r`b)^bbbm z4r-_#VsA+8?+Z4vBAJrT{@#IT7-FqL+&|-rDTCqmprbtw`Bl5KC&0N@fO9Qsffh0u z&p>EU4a;$f|J@VFlrgM>KO*BD>^s-jf1xkq$9e_)kx8tmq2Bn{fBUz8Yr8>$X4ZhB zL9-qIZ*KwkxXq41m9Tvp9Y`{wY@^}uOZHdocH1kZF58g(H=UT392rRopM~5PmfNn{ zkaRRQXY8sb#<8J)r(|z;xG#KhK-nMP%7~G5MTfD}Xxtm^#}Y{0-zZA9;T2RG^8b(R ze_papZg}k4!7=;T>EH0DHazwtl~~BX^Dn{U+v6Jtq7cMGgL?uyY4rjbbTAe~0JWPau1nEM+gua}HBP6X3|!^sUtlFN=J{YR71(I1pA z{o*Gte=@mZ>-eU0dBgab@y=v<>!qXbczq+ge)=<4LX+MVuN^|?Oe~+;@YvMa$7cPz zXG@oJu3_c|gwy}qM*-*uRyHK;$~p)n_BhlQ3EMgFUS%1$GWc>FWzK381yo!*4H2DU zvF)6Tc4Vv$?vDNDkPet8+Ncj;_{0cR8>EyHWMPGmc|7P;XfDo3(GXSy zSUO~!82>7LTaY7X!)5&EjC4_J$`iPC3jfs>fJ?S_eKu+NSGLZUR$N(mdH-nJWNF=O z)ymP`V|}SLJ7!iNNUc7QUVSiGbud|WaJF-Et z9PhRbI}OW%*6H|kC33t8FV_zx?B^cFm$koArssUg@yb${ZCFAXm(>rQaO-Sx zxJ0%O`>5?tughtZrU}-jRSU+7V%TT5_1Pi&x)Yv6Ny3+qWIOwIHYGdRATx*M|MwHZ-+c`ADn+nU&=&+(;xuYu*1rdoPTNRtqSyFL* zKn{0>p*ukX_|vaw^wt(H(O;o9URn!9R1BK6!9F>@q?Qy@X5l8|B2|b&^GMbVLt-7R zs%{f!C*~uwG0Ukz1~Umd+Z19Y+RzhfrY!5LS8$z!R#eSMdbelFb&UNU%5i#(q<1#fD*CU*8ptdc z>8zher4=*SnSOD$rd8J8N%<5Cy@>znYvA6OY&U(j>cC{xLu0L%j?Y$Y(6MDTlgol* z%B8ly_AZ+(T{X5gRoXCHwPLh-G&a^g-g<4-#Qx--C*R(dYj%^$dUGrRxOzclL9=mez zO4lzOOIAHOA_2^J4Y&JH$Ie<*3T}fzGZWkEFU>I>#w}B{PNIf zTiRbY8?5{7WmlJte=Z$7kgVMD{mNxGy(n~3vQ@4eT{m8l+})OZ`V-01FDCsjCZ!j@ zUs5(Jm3~pWETxvUj!09|_SvfCS6=?ouF)-D+D|XJX7u1!Jfr8oQi1o}xOwcE*Oz}| z_3Nw0Poy{QrFW>U8*dqJ9lLP7s%dPa{?<+O)=hbD?O$Q>0zWAAU#Y$l8*LwJ9g8JP zww@{~>-2yYb&S>>9EKOWWd7_P5!jt^4Ggt3-|>f3HLS$gV_Wb%mfd>QXS9*=JvK1Jl-?mFv!sLeje_%$U&viYACk0GV6FikR&?)J_=vkgS$tsHS9AQrqwchw7M^S6y z8IDq|2JO*2M?log*ZnN=VEm?wJov2DEc27WgIju-xUIZ|A5u9(Kqf${JjN-NFkw*w zy#)FQFo8awH7qfaeG6j_@tT6XffxEBHThEnXgQL#P+6oe^vEK8{Up*?372tNcl6w# z_3VNWSe1#uT6bvE??VMiOfUBHcn4LVfidqM#0-263prpDX8_)aOq`i&JP0g_%*KN$ z|G^oFKG*deo)j=eP4HCOK&PaGqvv?;B&#UObA&xf)OsVrQPi3^!%?c$s6CqJ2#C6i z9KvQr3yU1Ws#g*p@EKU+5Zjx3Z;(9i1|>(Nmfym=LmZeM^THI4;4kNw;UX$T8-MHd5(anyU2VNnUAJ8`_QOh zT6($of0;xSCjNhcuzM2!&C*Fr2Pge_Vd+FKHfm-N&<~Jw!u10OCtfC*;gb_DPc?Aq z1p0v->4b8Gl)#a>r4#4}s4$aGf;qL=lXxCv8qyC?QIbwx<|x(5dltX7D4pOpTx1X* z76y@V%b`egNckCJ2QLwLpUMFr6CPNj@wIaZ4eajM@G!Ab!4A|u@Ph8Ne+#gQO#p8M zCpJyh?E{uXX5GG&f8UHmpX+)KPYRf#CU`1spi|Pm(QckQ$tsHS9AQrqwcY@$pl6`g z#3qhXtvc<|JV!v(T@)4;dBKN<7kDm&!{?$yH%KhNGQ}l_hQu1>*@ao7P?PU_fl|tN z41d}9x;dB(zx^v_E(6}O7hI+%?XLwku@T^nMS>!Sw7A_Mn z4+OZ*6dS{^6YL!M8R}wH^kRL}u&Zl>hLIM4#(=4o?c0q9%AMZJ<-q;n9AcJIN}F@*H7L61Coh z3_{O9t+&7(2&G!B+M{`nfT&x5JNye+IobR#+Kk|rhB@S-&BzB=TPl~%IZDeGEt_ck z>er%Wljw;DZrLQ4&pApfaz?3Nw~@L>`@70T`#b%2=6~e!`W% zOJIL@gxlXeaaTfC#(ntUvE!d)Dxr+$dA1}}zKs4kPq}9~N=p zJ{18D&WGc*b4X*k1;38=)du_;Ke;w!v+I8{^;xs)KSV2JQerT>=_FQ81!Z7z5rV_e zpbdQ5rGD42p|%PY09PRwY@=BXlWnxi@QOT37E?J_7UvQ++E!N7`Fs4}TtADhsIuL!t1hJrub^}j=m8+(;D_&2n9PD7 zfmJXvAGun>_sjaJ=m3G22`B_q0`$`z31vI} z`Wt5?`b=vXJSAX?s_|6XK&PZeQngdPNmftb#PN#e+Y<{ zJDQQ&Mfr6C^m{8b<17D>z&8kt6JW!X?rK))-0x}yqk(YmKx`Ir<@C8`SKx>UbN6CX92 zfH@>03iZGU;hyPYHKPly8OTHkzA-pJQ2-IIqkay8uq^$b(fPM^=?I&I&7g!x+7DBu z=5cHTG$;LA@z>uxBhhDC%it*iQ&f$o(gr#uHIGJk?j)-x@NsDNwH5_(U20yWgpV2} z#JzlDY}dV0f+iu6-lv0Ndi4*+5C4XduvXn*e_lJz(%#&=S-H4r(Du5d;WM*$rA$0iUm?Ch#|3{mMU2d@=A8k4G6e zMe!(ozWsq`2Pnd&9H3~Xh>C(3o6+bH;(;atM30oir?H2&N#zx|<#pg*%rOexxEV1pscqfIPiH#zBL zj96>2JUufD_r!zAFdu%>MKND}(ndv1{@CP1opV7CMcOFx??-?`-BaIG$3hon(&-rf z^H$}|Ivt0lPN!6V9n%x77Oo;HnJ(;WIvw&Dhdu}4t|S&x#vzw*UED|R`}2@G4(x$4 z&EiMv`$L~s#sksBtF5@8?kBU9G69PJjXlZDdk6yGyeH+~Gb7PwTF>FhG)r=Pl2zfj8KMle`stJ(%fp7T^8iVI z&Di;rzhOqA&$O1oQ)!ms_#~C*s2QRNwb+sJLkZb5z8oOwe`tJp%HK32(PvuA;HflA zaeR`>bJPr{q^1HQvj^OBkq_(qSO1&Ghxt6@HMI2!Z%kvQbJPyN=6h(BWUkg z5*Bl=tX&Udt`UKEjpDHi9oOG1YrocFcFL~-1yYG=9;ty|Kf<(2rwsllNGJz zjo+W75f53TqAlZxktknNAucM`R5vKHG&ex~L&E-uz_$sI3=!XH+78^QM zXXi>5t2ZPAEveOLZTBnj&q&5+nr`!qw1G}ZyHUkl4mc{m37282 zi=sM8MYEg4{YhyzM4RNYV2a`{G>wOtY*RNL%9d@AVUlg?4B1A@Hp?~;e`e`B?MSWO znUo%-@3iwxHSK>CiIo4*8Oiue(`}xSHqa^QQB*OP1CGjXs-mclQqk-tQ3OpD5Uy}k z%smKKn!&|KZHbF8eI|30kHohJ(=BOJT0s3rnQ9Z}8e%P{8DwBBm&0|HCX`qHn81G_ zzLB>Zypn1C&-C|41t;oYnyq-FrGiFVKINDHzo>cI7|vZqed6-xl2Yv$Rw%V2pTi$N zI1hcMd73BEl;G$j73QcJq6DpVK9!<1mK|DnV5jVX{S@?3AZtXKgyxEV zFm_p4M3HR}G*=SUFlnxgIu;QoO}6WW%NbU|C9iSKl}Gl;rKWg?bUFsROm8XHAo@MT z=bkIxA;L=!yUDIslb=75-yURL-gakcB9N8e+ozSNZ5^hom2XVObgj(DNc8GAfk zS;@mFeBJ#ZJCkiGtJXHk){MkCnXf-s2QS!!HkOAyuiK%ysDrj3+!GB zFr&Ma4YG|fi_MOf#+U}9vSK{BH* zS;N5c1;#Q3ZCp@*iWSVCuSu2OzSy3nPy3jPXgbj72`P^PDv*U45y=fIN?3`WhpqC4nKms zlu-TV2#!vsdUWE94kkFVO|SWgj(T|V`ztzngy`snm}-Q|ULettt%>fmCb0QV#|6!Q ze|(U7&8iyPyjN%d&plNC8{Pd&01GjPTT$f>J!V21Z@!Dc%4*!9XIua<2|Kf8kIk0V z-gY~yd?Vf;O134N4e>@F#S5YWnvxm{jNq;uhA4|;tcLCE>=>L6!^WaB;Z(n3a#wKS z>*QQ;2XQiY5Y#c2F>t>y7AO`Q5vGliWv2G6mOSF!6mv{C^p_bNgm^OuErOrILmo zl<(Q@P^OTrlF0?ve{$Vtb&PE$KpefA7GKc22U z4)K2F>ZG)KWN_4R`7<-pYJ8@7k|zXAQ7%uV4RlIcJ&MSNRL%UElvXbi0eYG)OLvL~ zM+DsMfyoay@HTt_O3Ojz9|_P1R{jTp{}&){&}s2Fnb(76{}kU^V90T&2dyL-2dFZd zWdm;PW9iD;AIo@Kc(DP8^xmD=0MP=h+H;>ZPF^84V1N9+hz&lR5j&SvV0@F+9auy+ zgc|w=d(Yqm@`UndG}NFYyhk~KkH_9nlvpK>X~GUq!SR??5~q^cRC1Ry$py*UgULhP z>7_k6Di2*Is2^`(UI?}0EvYOo1Z42F7F%~gGfVyn@H5b}c?O5@L@2-x|97r3#mn)_ z%1jSoRG{um*w54Qh$I6c*IFK;qe}*;VMqq1Iu>$)p__mNA+!rt%qvTRFy6yTg(>6$ zNDxLztEY>SBtgt687vWfWVA*bgE`lnk^%NVWNEP{hu!l&bVr;Dm?LAMC)p|jXEu}B zSkQjQL&wBL`hxcOa@|&r=jhwgx;XU~el3N>DPso8xZzql7{{{@pHYrdW)x3HfW39ZK2#3oGN zU`SRoa$z(K^RyX>KGQtS6KP6tR6tZ97bPl-+d3+4^Xysw{b+bfswu#eeiq*liW_`I ze!c&dw(RPL!L97Z1`XEGanLW$uXH9nY_)@PE7ePd*DaVly_iD1RL8{AjJ1x{3PzX_ zo7%Q2CpK-0loQl|!>pXlEEy9m4zrT9wM$8CzAN;(45Mgs$4jQkY?H?W8Y^v6{%`yU z@uODjZo`Ouei&xp9cYYemUa41u;$jOexS_SSj%6_><#^(fBBZY(=4Kr1> zsjAxK*1gI7&!nqPj!5r7k@<=AlEx9wtbf^+o|JzBS&i%fNcuP6ugPqLWeB?wqzuE6 z9cGgej!#Loj3c!LE0qcQ5q^)r?-TeQfoTG>1gImFzb5c^1pc1DKNEPDz-PJq$pub}o1GtunWJ@2v`_d)qCa8yE4>qR)Kd{#bfi$!?RdVzHz$Ez`_Vd@RHtgE8tt{yoM0 zahue&1(n1_$_d?Ak#a(~lRYBt4{ev+ued)YZSkOf)^k%APJ)iX{*(@(i1bF{%t^9I zyWQ9;XO>x((6!C;j2NUj8&Sld5)9Xza3#<@vs`1Pl(R^pAJ&KpG)G_)u2+^L*nElg z!*v%vK5$)U+t%5Go!hYG6x>Fv@D)4j{pl)rmM zqR+IR!;=E0SY4h>oA4==4}6^GPqMlO{GzA09z54;-(u{F|zZmKi4oJBZm??`Y=6!a+m?= zJSayi-$(>Bq7e|zr+S_d5NyT~^Wpio{Qyb-;kW%M|MN2veWtYxo)R#{D)MC7gioP( z0LJ4~b&^#T_}hMtn&EZn`9tq`5cpfCnc>O7x8Qba zwmU(F*{}q7idE#9uth;59EZ(GGVmD3$u5PCrKFaC=e2?C>jh0z7U!1_>-h5U>U1-9_`e0ZO(V;d)dNKE$4>!=Xpn_cPlxOAKJI(Nu(Am>8xs085Y(+7qLOY0F zbeRlt*h`FFj;H-k0Z%^j5<%e4yp;04G$YYxTF>E00aL6lPo_=y6v_vV^ZZFx*TA3A z+7yidxh}o5IIVmHr^evzR0#fkL2lgU~pkA=>t{ZX84 z+A$D{^>hg7j2k;zk<1(w9yU)m$CA=1Mn1>V{wIOI9R^7HpL{!<@}HWK=rgTl@RWcl zR*@&uCVUFT1IKv&B&#a$x3L$bH7bzn(kVtdZ9yql4@349ibSB4Km~y%1akB%cFH4A zhyQ8-S2M4KEWVVDu_xHvQuoLb~}3_t75G?c>-gG+4%i+MJ3Gc1OB~juC5ZkHn&j$`x~b%8!5yy6hOAMgyd0Ll`J7aK@LJ z=h6lXIkXCcF<&#na^Ih3grD`L;`veSM4!Ad%5@^V$YVrZ+O%7P*eB0T`8OqN_ApP` z=aL7WLk^rW}-Yv2d8Hbo;qu1n7?PP-T# zAGOuok|)B^X#d8I8w1bxD^dBtfdlc<9?ELCKxxxNYe758GMX-|6(8IiO&th#Mndp> z?FlKNPDC?Mxi{Ke)R9;G5K)Sz^QhUCl%8OevMcR>82ALv-FzbHe|Q4NZay(1(PvuA z;3)x9tRhdQP52aw*X-i?ldP)1Cvcjk)~G-percL-1%Gcl+NUA%@>KgcdtxcZ8;dQC~{m@6QL)YRV7mU_8 zFsv?%DBg|Dt_O9-3OrcL;i^w=28a;WAct10kn zP;6*z3go)<*!!V@#SLF=%v&l4Kfs$xrHGl!C8vgljGJNiP7CYUTo45%aJv+oe$HHZ zxO^1fUSM*VtI}~Zjes!r5{>Lj`(d=c_uBISNq@_==f%mw(^`f&;FlvfSQzy+nh`J=nkJcM73+w2cfp}AdZ_#zi)J#_ZV;ev}?t<1ADa>Hm82DC6> zeKEac2MkzOA;5FZ$obLL2=&Z;V90?6X|qEN@D%0pJaVehP&l;Yl|-S*f_^TGZ2H5> zrb|L_oCopE=l;Y>s4z02*Jn22I_N9h?F=)vi-a8ZdyLjz;US>Fh@uSo?`WQCIY52CXS7QJyX)2F}Rx$O7Ybx zz`2I!HGw=dde=@4_!5TpkO^ z=cdCVzGnMn2c_V}iqrQJ?yuRv#3|Rwe8TJr_g++fCFrujOU#q-P`P393@a7~L-!0r z4P!zu2^bVfA?GrV`jn*@8PT%K>QGdZ8MS%hARe1@%`tIUJza=F@OFguO)m&|(%noapF#$Zf%LtC2iTa3m08W@vHe*9En>PL&W zHiBaoDd*GsL0+mk36#lIa&@9i^Vlq(_t+@cThjj`ls!=yd25yq`-aMb%i>R+L_kO! z!-NSV5bFsCNZ*NbgQ5t!1z&-H9F_+M;3LiMVJq%jSG4~^tr}pqTLHB*)Yk`vP(X|A z(in8a%a57%#MK)lt_B@SIr!`i@}G%&Sk(rl6bY8i&Lr-;Y7TPSCmgV!rBd$^_(uXM zfVOyL#B9CR(AsfLJ)b&ya&Ckes;VI1(l38`e)n*#$?11E{SZZTuFs z#9X~2z_P;U>_*YE$b{!;&Q?A1TFCKa5boZr`CtW)p+ny%F#%_d?}rz!o%)7qEZ9{2n?pn9C=6lj2-M}Cb?P4~m~QK+xe#j2QV8Mg6g~LGq<{O2M4xFbgQwCg z#c=^qXY56BUq{7lAgK0^f9aL|Y1m7^c`d}8*EWqIW;w!Y&<8P(laPuKoLtM`NJ^@G zMdc{M9P6kkgowyj_KVv(3cohA#j`19a&DuzW4OMR=bBtC$B(G2B(RzQIRYvh2y7%! zLtqPmtps)v$g#z$qC5fz@L#0>f7}-9d1ilgjKOCaJjftBOO72I*L=(4fPb50cRzaD z|@eG&1VS7p2 zojusX@~NFYkUgT|Jpt{Q&mLcl9o5*-%g%7zgCiUt+3BOAz8d}TL!Z)t(XPBifWkyT z9N0xtfGJ8ICVVa#0MR6a)}Xo-XMZZ{J}Qea5|9rvlKuh;jWb<5=%$u?2#|Q7v=VP5 znc(lc*!52blp9E4hG(yuqpG^?uZuVZ%_~T=FycX^go>e@?HzJ3})O2 zw%j_k2I|{J;=+w7KNef`nbtCRO28Cl^HkbE#qCLIP~6thDXEdpoT6q?08bI+1%k#5 zwmkBn*z)hQ@I*yre+spYLld-+ImMK^SPg*aC>FfwjFtrqZY=iWith4>l9206s~5|S z6CqEF5QZKW+}H@=Jzu5nT&KU_;oBOhK-tj}sif^C6W_8R^ z_809-vy5D7tOS32eF+)4tS!EU7+9V_oG%Z*PHg8eF&oCYvboUT^Zxk30b`eyiI)h| z!35QLI-_JA7h}k%Occ%8hn4U|T^{ACD(*1KzexvUa}Qaq;kgWID)u>Ia`Xt2JrCqF zxi!!!Z5X*Yihx4!nx@Y*PxFL;Daz%kw1G}Z8%B4CG6F&2*;Q+k(pm_SRhNfnq_y}= z^CV9Qn4(;sN*m~uw05*glo80Jw6=f{IkCvC_&~K8tN;7Tt+XQ+?vn3k^D61b%&Vji z2(Lm6qJXhJ_aVA{m!X+svF#gu{4ATfxD;RUK0!Q;8Cd7@kSgq6#Bg{`hpA~(kmWpS zDZAv)m^sPA50aDU%h3lW_blo>7j>STmA=pA=%UUOI{x8>(|N|v?%mkX5IFd3`-$Tx zPCUKm*r`*`9%*>`CFyD1; z_gB9{*A1@Nr>d3Q0Ts0?mPb&Q6~06i)rVK1Rn!yt^xNIbF;=`nsXjnNfR+};Etv<(imjR+{&dn zs{9hMNL`!$Tt18Zz)2j7t4a%vZ#&HHn4Rf&=C!Lg)? ze>|IMUkHcKVV@JRL&E)ou?7SJNANjyPhdxL?p8ueV{@Lw`eppa$}bWi)0&_?zH@%2 zHF9XkY*{ns7HT&iLpR^YQu86E2*e?oO317W#OW+J@k#B7boccuaDCY`_asoP?rY}R zpeuhisM`jgUhJ)IoLU3pmwMV;tv7r3vJCFAD|sW+qlcEZ&+n(tI}q)c6U~#L*Ago&rUd|KHCRhsIEx& zAjXZg9cdD;JokDV$JA?~G_aencyaMn+oM$q%Rzev$zA< zqf@ie(@KmJA&~(K7WNw`4CpJLOKx~F?Z<%!r{0dSh@citB$Pht1If96wufX#D z6g~(Ds-qMSbi)5QVA?IXH4koF@;`x9^B00YGFdklZG;`kv$gx=!_;`k9U zP+UHQ@zYU?;>%e}L&?-N_?QA3u2L zNTB_RW2XWaBC(#p5Nyu_-5{36_d^-ns2ee>i7c{+V;hOPR>WaZ__v7kULZi;de$s^ zOF?^-Jc*~6gXvGmSy8fQ!7MridoQ}%AxdJ>@GSVC_li@f==AM{JB7-@jF#!RU7Wss zGSrpw)6Dw)a-9}!D*9;Uo{xc3Z2Vg zve~71=Bn4^V8Kk*m|KN#xMXe>`SXyU$z#UafX5KAA~K`Qx9~XiRlEmnwsd0c=wCm2 zA?>e)FXQ-5033aXza|$*mSOg31fG(bX}%$6NO(Bvs3)iHO{k9VjNL& zvb-sgM3Dj{zc?FZO++m5VVI4Uwv$M9A*2Shxj=5@gr>~3oGB!n@o<6qtP<(#fXBE@ zc3JSl?Cgm*7n_H2Yy1uoj4rHgPfGhpSlgcV{k37DdK zJe4-kDHPtYoo7$7iUOZ-i@E~2F6}Eo9*Z;LS+dH@xw3TbPW`4;$P$B;J_wp}5n>>Q;~1e_ZJLY9=cq72&-faiK}J2LRjv{54_6{0fHpHWjf2<`%dHlF zDHafYrnL;75)fhGh|Z}5qQdE@xQ&o-G#zTUbrdn<*om{0jS}N9`XFQa(lCMFM9C$OI^ST&|MyIm#pO4g6P09^sDk?IuTha;JCT$XNKy@4xLSck?-! z)#}shlZaC3Xtfhz$T$v-iU0W2`6HZMHTFe1NUlgE*$w~Ty9x~6YXW=39-15R6@ajTH z0z_~@G#&?HO(C#g(O6S(PKR3(#r!b^A1tzz5UxD(!nxdtHF;4MFG^z3Q!1Bf?&b^K zlip!kpp>^MO&}pnBSrRD8JCJdpX0i%)Z>oQNVgG&Fr@bPv8XiA?H)qyc{_7Ue64Yo z8aqdTEjY4$WLOXwbuJ@!7%AXv7x38*I`ccumi88u%9X*{h5sB6RR098ix7q@>c`Jb z>_|2|mR@o2a@mM$q-%D?nxwR5Bz7e>l9-X!;4{sWJRx9;a(OCkpi|PCD_X872?T{_ z{i~DG=8?~gb|w9rXC(Se^E6MSDZx7;>Dh{ z9+O-o_P4cXJiVd5!B7;D8(Y9Zl~w|;6PS}zvT{O7=F%+-+(@&%(hFcA)AI8Q2SPY* zRgSZRg6CwOyikVP*#N*~*a_F$J*kH0(&k58{tg83$X-eB>GJAG*6@{ z!BGKGVRVL5Y(FW-;ai;q>YQh#(c~y4B=RhaKF%tOd)I#dAtu{gKsyH0HF~} z?Hno^zVI^&{(uO^kac^YE^pYMUU5Jtmau6xFFM_0*5FG%~6HyzfV1hU^ZCsR<^YEWVO4DEJV#fjADdz z!PKfl37^GQ6>Vr>ZyGZy?O%lk`GFX7%AW;l{sU1EDRJjPK@WVsJ+BS)Zkz1*HG9#y z7H4AIMaVN3n`IL^PjWcKhx7xeK1dMekoZQ%AW`p zhq3K0tQ$TY!`P!_4_;#!GpvaCqcAJ;PP0_|5PaXaYJYKu3BNI2`8!N<$z3U=5$F^oTzZeH{jvpHY2VvKRVJ5< zz2$|qcHs)?inewrqRC8yYolK$Y)(ZqD|few=pYlO#6}*1Mx28*HK0WLl}K!;gRIbX z&D+6(lhJ&d`2UDC2t$mC9dGYQH9VbO@r=$N?!%OH-wiR^aKh09{Vu;p;P(lT6^8OM z0Xio@p@oQYk-%pNQ2bWqrwIHs0SdyX{2YNC*Wez?Bk*VVuhLDjnsd4zy5)1Y*WQvG z?sc~$mwVH#G7YP_RpvyVPXlYI+`8KmYA|&GYs=b}?jAjbE5v{})^&I=oMj|OV!IRn zZ_^slJnNI@L3YY6xB@~U%CEDu5YD;Y@O%@I=IAn?Q=`@P-bHPP!6alG7Hsx!(tF`z zQU{iOh|%muyk$2G^mvGQQZKQ~%3>EgcobruA~d@VKFf4BZy^-~oG zMx5{XSB$zwLt{(FPL3~mbNBD={np+o|6?Prcd7$p)nl>oQ|aoK%OxY8EA|H$G+vPK zTMI$LWJba+V|dBlHw>-0Er&4a2&$4CoeT{dc3Fk50ZG#SM`?~urp!1s@T>*7Wf7*) zz7+|XWdx%nc?;%ThI2m%vn*D)tf7<9WeE>y@DqkkW`*9osU;$>oXa?8F-Iyx82TaG zRcP(MfZvf3rt%75|DFI5rt%*M69)*H2$NAv4pm+)H>gtid8)~%68l;6Ce5H`4oalS zAMuOSjulzbAk1}J#V~; z6#|rzOl41l*TXFNIV&yADalZgyHS;ZJ zGasfo$o|kp>GNNlolwA(0H#SVuUHi*>-Ot~DND?E&xm=@e}K(*9}tRr0;0zv#eH-s zPPVb5&fSAJ?~ollWpE5D-nE4BuB8du7;=15ThGW<3nqLBF`+2$5+%XAsy-mRtGXEP zsv~YBBoMRWl4SNA2_$I0g9SqXTbPK&wp@?J#&h(ur!rC>BAT&91#vpv!hlH}U#2Em zyJLh|i5(WJ0j?6IeN$q1L*ZC_L-zVtUjm2%t4PB5*Lm{69Jzl@?HeT%_NlFVCJw(< zlU{R_P8RfBk!Dw|PfF`?2+J}$7%!&0;VXJr_u&GC9NM_Cdvo|g=aU^B&8Y{ z`XMq>&5T5!X`bc@0aKLAQ)vU8l4?d_g00mQ$fQ)0Z;2q56(3NU#9~&5gO_+lCB-@p@(aIdBajxJhP)(HT zpa8lsx#=BW zraS^(>i7VDSP@TFj~#of%C?$ypKRtYlb{z35BlJ@h5&nEG<5u|fAfgXstn7qd^{z7!=f z`z$R|R;cBKUH$#&O9_4HzK6a<9aCRcEm&WQB|y=>6eXF$p>V%HkiLvn>0>+L(N(D! zt#-Mj$1z+6uZ}WC{pE>rwOEM8vP3zkznbcpdU@HrdwG>gS89R>mu$< z(OwrN(d%nXeNeF1?WcV?PtGZf$Pe+0fk5(6Y0=rFl;?eNNwTooVcquey{c zL6gc+0w)M;0m#@xcg&TVcZo_NROa#Gj#x>{aO5 z88?gn#ZM4q5B}fi!4_Ia#VPJjydOyP%C<1@{ZJRx9;a(OCkpi|Q7E3zmfkV$EEzIsiF@jr_J zpC`uKAjosk-C-@dzlh*zFam-30)%fDo3i2JT2x^7GBXeo^3AR}B@4!??@eGYDZ;A7 zu8r&y&YK7UW-xG{Y@k?&c5)4 zxCfvpONC|C-S9@;+QVRrcVX>OttlYL4wY;@x+01i#_78qVq4U_!hTri>#4iH7<7f}-Oi_hOml(E|9Q3XqA z{huh&q(R@jr9q!g4EkSP8XWd^Q%|}RUdCYN)0ZRz+GSd;nfr3#7>rm?741t=5`9^W z!90+@EQmbV}MVxVini^nj%%%ns9y2AOXcWFl{oSguyq@<|LH-%0 zsUmx_z*bl%+{vkJL>QlQvdAvhMw7+I@sj3sGp&?wIvpfY-t^ekY#ckEs%{)9!E7>H z<{xuj3;xjQ*zL=iR8Vp)rWLGye_~e44}KN-C+O~M;h1{^MeJ|5vgW0G7n4buP9c=C z#pHzxp(KRgqVz9Hg8qwzP%=b2J$6Y63!${2VzOL1T$+G|HbN*N;$YGF9nl)mljT&y z;G{+PnAmDszz|A8NnNzZMM?Cy5khI9d$M%c3r zY%*Q~`@;Ytvh>NOU`mWxvXFy}V`}yQcP&iw*h(~2o7p56sd=1&Z1%@u$gp?eM;}-S z*K<`kO`LNUPhw*BY+1$V(1dg1xog97F-I)L?~BY$F}gP*Mt5gcd_$x~07FueX`v4} zLh%ia+d674R5%Rb_pI0$*5(nrZ11+e;naA%B=5C@ybE(Pg%vS8*0cGT@*T7|hgp&y)5q39k<4{M#FXAK zGbQy}&9(E_*1g^N_OlP1F+mxydE~QWRau7zAT3S@7{Ts`cCG+NP{DLtN6m!_A0F@y zV@yzofe{fq!qFwr1>mnUOAla&X59&i3%Uck7@N|JYlfi(z#$zz7)AP+It>y-QAxp_ zC}lDi>KKv&WFFc*VAol)7|*$M1I+U_wb+KXSd$hd8&Pr|GM7*&%DT)|#$>KCO%q`9 zS~E#KR+@lvT?3&1)l`$E!#>%GzVzHfU!snwFBk486Tw)D_N6F^zAQEMfR!fTf%GM$ zuClf}Qtwa$+m4Mt)D-I-XzK212wiAmIStz4Pi@l6&_>9gW8h`6wP%r0WG%~-mrSo& zVJu#ix4_OarR-7nyoRO70%qY%U&O9r8c&V48IPc)EITM=ZqY2Oa1U|7n|KD|`BhYo zIA@n_NN#*0S$%AzWOl{IvDW1BEhDAcO4c{ADtYR~x!B^Cr1TILvwKqhhh`-DO!G8P z2$-T=o=O|&l=RT(9#KXhC_KB8?OhIzIxd^`E|C-)ksP7DOPm-$o13(EsiD}r%qxi! zh4(HwR|F?S7(x7ZYC2NHc_I#J=8Vvfy1$7Yq$^K93WK@3(vK1aW965lAC(r2D+;Y= zQA!piLCI1PN;Y(QCEA8fDVb;{7FIv%LSMS?Lf5SKCF+>C;=<{g#WqaQz7!?Vm-E*( z-^acLSHwY3-vw)mS2BiZ=6y`D_hpRcpk28uwrA4WDc?ie3#YBy%otr3(-KB6SdYS< zJsKT~vj~tDJg-PS3WlJ*i|&R5y+&ps=$^sMR*Xl8^#_HmkhJ`pA5+m zQG4dvAtI{9)-96@=c9}FG7czF9TU|q+%~I_P>NEmC<&@9w#{mAK;4d`l#X&+SPsak zHd`0p{acU!b-enw{`mmc$1`2h8H{DV)ynMOatz~0LQ(z*K%GN1-ei_4H0h|LI~-FH zB}~EJj$WA3MLIjWl>S~86Ng0zv!J`2MOT3)hKU5fs*#}C6wM&P<*P=wTzP698uit% zKU)V^*=qBCDw2HfR6yEK)ojOTKUG6%KQ*^x(f!mU-u_jac?D&b4;Xzo=cJFjl@MsX zM8~pa@1QQ6f=A5zMd?G71bq}!@EG)=D|kwof@fhR1lsij-v`|>z$SJ2^>=MnNa7s zj`NWa&E|bE_=U#8O8ofz=xlzi{Q}WjFQ40&wvTwiXX&@CO9GP2j%}_)`M^mB619_zMCl0%-y{K{~He9)a!j<@6w(rS3JiR(sqV zZ&fmbF_NL*-}UAH&m zPw@kHoo zoR**(^bKdEPEdO2Of;;JcKxc8yQsefTuA~TO3o||Xu4?%uE~60tRieWxq6fFp?^Xh zmDYaHPn!3P2SSPp#}tINmj|Qa4tR*usMK9lj6upL@VQF=Dv?(Ntz*O3zv8uuw7)hf z)u#NlGm`O{mTF&tHjy&Y1}aKSO0}<5h}$|kCDkUgn?aG3RGYsfUZfoKzj5XBi=wpr zL82UGhDJIY?zF$8d>=`D{IiUgDOVvd*Z5a5sFOYBH?hfS_2>M%q9{iAkv*HN|~_Pf7L3 z?1oS#CDktyfO%^2q4#sH4-Wy*{F6lhEaRRCU;`rnQV!_~qrOnDdGt40jsCgI|3=Qx z-;xK zw#-r`91_;KoS~ZR-{?gZX8-n&grxw$$yqLA-2;?MuWpiQm^dS1h2x|uW@Y>*R7i+3 z$`q0+iN26&bg5<|8{qlxW0Gu|t%i7^8sjv`U&M2;@!&@wz6J#0`}+B$zhOqA&$O1o zQ)!msxPYkhb!aZBH16xDxD8}VYGA{)-t1A)5R9*4V;J%SYsy4r9BH)gyY9ddtXM@L3FGZ=}+B7G|R88WLn z28O6F8@U9FbxNZWzJOCF!m#sZ;gXP*@oIT|5+>^bQ`98-X&}?~_jOT2{1<1kE1SR> z6QfWNN*#d)0*44ZL4bKMewwiF5cmTEcKVi81a=d6jsQD`;WA-tt;aedyL0YHdwcsy zxZSVZKskJD>h}R|I_-A*yAGe-_1;RG-TryoPnZ06TWiYJ`j@sfH+{CHE0Zg?rj`UR zRm?75n_SnLTE0DLtD3D^m0Vq)s%l8uDsNS~?3K4FD(%5rTRiq9x0d?tD{gI;?8|R2 z-)OIY@3eiJz2#jSz)jh{qSAG#?7bbw?e?wj+6cba<*2Z?+uyYje6Oya@&VrKvNzh- zy=wz_@2q{jz4=`mzHci10#*9P$3dHX~5&F|U(-unbBf$Zzw1-rAafA2YGnZ5Cr J4S@Z~|38ThDDMCO literal 0 HcmV?d00001 diff --git a/tests/conftest.py b/tests/conftest.py index 3e02cb4..c6033a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,57 +1,116 @@ """ Extract scheduling functions directly from the workflow pre-step heredoc. -Instead of duplicating the workflow's Python code in a separate module, we parse -workflows/autoloop.md, extract the Python heredoc, pull out function definitions -via the AST, and exec them into a namespace that tests can import from. +Instead of duplicating the workflow's JavaScript code in a separate module, we parse +workflows/autoloop.md, extract the JavaScript heredoc, write the function definitions +to a temp CommonJS module, and call them via Node.js subprocess. This ensures tests always run against the actual workflow code. """ -import ast +import json import os import re -import textwrap +import subprocess +import tempfile +from datetime import timedelta WORKFLOW_PATH = os.path.join(os.path.dirname(__file__), "..", "workflows", "autoloop.md") +# Path to the extracted JS module +_JS_MODULE_PATH = os.path.join(tempfile.gettempdir(), "autoloop_test_functions.cjs") + def _load_workflow_functions(): - """Parse workflows/autoloop.md and extract Python function defs from the pre-step.""" + """Parse workflows/autoloop.md and extract JS function defs from the pre-step.""" with open(WORKFLOW_PATH) as f: content = f.read() - # Extract the Python heredoc between PYEOF markers - m = re.search(r"python3 - << 'PYEOF'\n(.*?)\n\s*PYEOF", content, re.DOTALL) - assert m, "Could not find PYEOF heredoc in workflows/autoloop.md" - source = textwrap.dedent(m.group(1)) + # Extract the JavaScript heredoc between JSEOF markers + m = re.search(r"node - << 'JSEOF'\n(.*?)\n\s*JSEOF", content, re.DOTALL) + assert m, "Could not find JSEOF heredoc in workflows/autoloop.md" + source = m.group(1) + + # Extract function definitions: everything up to the main() async function. + # Functions are defined before 'async function main()' + lines = source.split("\n") + func_lines = [] + for line in lines: + if line.strip().startswith("async function main"): + break + func_lines.append(line) + + func_source = "\n".join(func_lines) + + # Write to a temp .cjs file with module.exports + with open(_JS_MODULE_PATH, "w") as f: + f.write(func_source) + f.write( + "\n\nmodule.exports = " + "{ parseMachineState, parseSchedule, getProgramName, readProgramState };\n" + ) + + return True + + +def _call_js(func_name, *args): + """Call a JS function from the extracted workflow module and return the result.""" + args_json = json.dumps(list(args)) + escaped_path = _JS_MODULE_PATH.replace("\\", "\\\\") + script = ( + "const m = require('" + escaped_path + "');\n" + "const result = m." + func_name + "(..." + args_json + ");\n" + "process.stdout.write(JSON.stringify(result === undefined ? null : result));\n" + ) + result = subprocess.run( + ["node", "-e", script], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + raise RuntimeError("Node.js error calling " + func_name + ": " + result.stderr) + if not result.stdout.strip(): + return None + return json.loads(result.stdout) + + +# Initialize at import time +_load_workflow_functions() + + +def _parse_schedule_wrapper(s): + """Python wrapper for JS parseSchedule. Converts milliseconds to timedelta.""" + ms = _call_js("parseSchedule", s) + if ms is None: + return None + return timedelta(milliseconds=ms) + + +def _parse_machine_state_wrapper(content): + """Python wrapper for JS parseMachineState.""" + return _call_js("parseMachineState", content) - # Parse AST and extract only top-level FunctionDef nodes - tree = ast.parse(source) - source_lines = source.splitlines(keepends=True) - func_sources = [] - for node in ast.iter_child_nodes(tree): - if isinstance(node, ast.FunctionDef): - func_sources.append("".join(source_lines[node.lineno - 1 : node.end_lineno])) - # Execute function defs with their required imports - ns = {} - preamble = "import os, re, json\nfrom datetime import datetime, timezone, timedelta\n\n" - exec(preamble + "\n".join(func_sources), ns) # noqa: S102 - return ns +def _get_program_name_wrapper(pf): + """Python wrapper for JS getProgramName.""" + return _call_js("getProgramName", pf) -# Load once at import time -_funcs = _load_workflow_functions() +_funcs = { + "parse_schedule": _parse_schedule_wrapper, + "parse_machine_state": _parse_machine_state_wrapper, + "get_program_name": _get_program_name_wrapper, + "read_program_state": lambda name: _call_js("readProgramState", name), +} def _extract_inline_pattern(name): - """Extract an inline code pattern from the workflow by name. + """Extract the JavaScript heredoc source from the workflow. - This is a helper for extracting small inline patterns (like the slugify regex) - that aren't wrapped in function defs in the workflow source. + This is a helper for inspecting the full inline source if needed. """ with open(WORKFLOW_PATH) as f: content = f.read() - m = re.search(r"python3 - << 'PYEOF'\n(.*?)\n\s*PYEOF", content, re.DOTALL) - return textwrap.dedent(m.group(1)) if m else "" + m = re.search(r"node - << 'JSEOF'\n(.*?)\n\s*JSEOF", content, re.DOTALL) + return m.group(1) if m else "" diff --git a/tests/test_scheduling.py b/tests/test_scheduling.py index b5870b2..a78d757 100644 --- a/tests/test_scheduling.py +++ b/tests/test_scheduling.py @@ -1,12 +1,13 @@ """Tests for the scheduling pre-step in workflows/autoloop.md. -Functions are extracted directly from the workflow heredoc at import time -(see conftest.py) — there is no separate copy of the scheduling code. +Functions are extracted directly from the workflow JavaScript heredoc at import +time (see conftest.py) and called via Node.js subprocess — there is no separate +copy of the scheduling code. For inline logic (slugify, frontmatter parsing, skip conditions, etc.) that -isn't wrapped in a function def in the workflow, we write thin test helpers +isn't wrapped in a named function in the workflow, we write thin test helpers that replicate the exact inline pattern. These are documented with the -workflow source lines they correspond to. +workflow source patterns they correspond to. """ import re @@ -27,14 +28,14 @@ # --------------------------------------------------------------------------- def slugify_issue_title(title): - """Replicates the inline slug logic at workflows/autoloop.md lines 236-237.""" + """Replicates the inline slug logic in the workflow's issue scanning section.""" slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-') slug = re.sub(r'-+', '-', slug) return slug def parse_frontmatter(content): - """Replicates the inline frontmatter parsing at workflows/autoloop.md lines 316-330.""" + """Replicates the inline frontmatter parsing in the workflow's program scanning loop.""" content_stripped = re.sub(r'^(\s*\s*\n)*', '', content, flags=re.DOTALL) schedule_delta = None target_metric = None @@ -53,7 +54,7 @@ def parse_frontmatter(content): def is_unconfigured(content): - """Replicates the inline unconfigured check at workflows/autoloop.md lines 306-312.""" + """Replicates the inline unconfigured check in the workflow's program scanning loop.""" if "" in content: return True if re.search(r'\bTODO\b|\bREPLACE', content): @@ -62,7 +63,7 @@ def is_unconfigured(content): def check_skip_conditions(state): - """Replicates the inline skip logic at workflows/autoloop.md lines 347-361. + """Replicates the inline skip logic in the workflow's program scanning loop. Returns (should_skip, reason). """ @@ -80,7 +81,7 @@ def check_skip_conditions(state): def check_if_due(schedule_delta, last_run, now): - """Replicates the inline due check at workflows/autoloop.md lines 363-368. + """Replicates the inline due check in the workflow's program scanning loop. Returns (is_due, next_due_iso). """ @@ -91,7 +92,7 @@ def check_if_due(schedule_delta, last_run, now): def select_program(due, forced_program=None, all_programs=None, unconfigured=None, issue_programs=None): - """Replicates the selection logic at workflows/autoloop.md lines 379-409. + """Replicates the selection logic in the workflow's program selection section. Returns (selected, selected_file, selected_issue, selected_target_metric, deferred, error). """ @@ -312,7 +313,7 @@ def test_absolute_path_directory(self): # --------------------------------------------------------------------------- -# slugify_issue_title (inline pattern, lines 236-237) +# slugify_issue_title (inline pattern, issue scanning section) # --------------------------------------------------------------------------- class TestSlugifyIssueTitle: @@ -345,7 +346,7 @@ def test_consecutive_hyphens_collapsed(self): assert slugify_issue_title("a b c") == "a-b-c" def test_collision_dedup(self): - """Replicates the slug collision dedup at workflows/autoloop.md lines 240-242.""" + """Replicates the slug collision dedup in the workflow's issue scanning section.""" # Simulate two issues that slugify to the same name issue_programs = {} titles = [("Improve Tests", 10), ("improve-tests", 20)] @@ -363,7 +364,7 @@ def test_collision_dedup(self): # --------------------------------------------------------------------------- -# parse_frontmatter (inline pattern, lines 316-330) +# parse_frontmatter (inline pattern, program scanning loop) # --------------------------------------------------------------------------- class TestParseFrontmatter: @@ -416,7 +417,7 @@ def test_extra_frontmatter_fields_ignored(self): # --------------------------------------------------------------------------- -# is_unconfigured (inline pattern, lines 306-312) +# is_unconfigured (inline pattern, program scanning loop) # --------------------------------------------------------------------------- class TestIsUnconfigured: @@ -453,7 +454,7 @@ def test_issue_template_detected(self): # --------------------------------------------------------------------------- -# check_skip_conditions (inline pattern, lines 347-361) +# check_skip_conditions (inline pattern, program scanning loop) # --------------------------------------------------------------------------- class TestCheckSkipConditions: @@ -512,7 +513,7 @@ def test_completed_takes_priority_over_paused(self): # --------------------------------------------------------------------------- -# check_if_due (inline pattern, lines 363-368) +# check_if_due (inline pattern, program scanning loop) # --------------------------------------------------------------------------- class TestCheckIfDue: @@ -556,7 +557,7 @@ def test_next_due_timestamp(self): # --------------------------------------------------------------------------- -# select_program (inline pattern, lines 379-409) +# select_program (inline pattern, program selection section) # --------------------------------------------------------------------------- class TestSelectProgram: @@ -646,7 +647,7 @@ def test_forced_program_gets_target_metric_from_due(self): def test_forced_program_not_in_due_select_returns_none(self): # select_program itself returns None for target_metric when program isn't in due. # The workflow's forced-program path has a fallback that parses target_metric - # directly from the program file (workflows/autoloop.md lines 399-410). + # directly from the program file (see forced-program fallback in the workflow). due = [] all_progs = {"a": "a.md"} selected, file, issue, target, deferred, err = select_program( diff --git a/workflows/autoloop.md b/workflows/autoloop.md index 1290679..2bbaabd 100644 --- a/workflows/autoloop.md +++ b/workflows/autoloop.md @@ -88,374 +88,449 @@ steps: GITHUB_REPOSITORY: ${{ github.repository }} AUTOLOOP_PROGRAM: ${{ github.event.inputs.program }} run: | - python3 - << 'PYEOF' - import os, json, re, glob, sys - import urllib.request, urllib.error - from datetime import datetime, timezone, timedelta - - programs_dir = ".autoloop/programs" - autoloop_dir = ".autoloop/programs" - template_file = os.path.join(autoloop_dir, "example.md") - - # Read program state from repo-memory (persistent git-backed storage) - github_token = os.environ.get("GITHUB_TOKEN", "") - repo = os.environ.get("GITHUB_REPOSITORY", "") - forced_program = os.environ.get("AUTOLOOP_PROGRAM", "").strip() - - # Repo-memory files are cloned to /tmp/gh-aw/repo-memory/{id}/ where {id} - # is derived from the branch-name configured in the tools section (memory/autoloop → autoloop) - repo_memory_dir = "/tmp/gh-aw/repo-memory/autoloop" - - def parse_machine_state(content): - """Parse the ⚙️ Machine State table from a state file. Returns a dict.""" - state = {} - m = re.search(r'## ⚙️ Machine State.*?\n(.*?)(?=\n## |\Z)', content, re.DOTALL) - if not m: - return state - section = m.group(0) - for row in re.finditer(r'\|\s*(.+?)\s*\|\s*(.+?)\s*\|', section): - raw_key = row.group(1).strip() - raw_val = row.group(2).strip() - if raw_key.lower() in ("field", "---", ":---", ":---:", "---:"): - continue - key = raw_key.lower().replace(" ", "_") - val = None if raw_val in ("—", "-", "") else raw_val - state[key] = val - # Coerce types - for int_field in ("iteration_count", "consecutive_errors"): - if int_field in state: - try: - state[int_field] = int(state[int_field]) - except (ValueError, TypeError): - state[int_field] = 0 - if "paused" in state: - state["paused"] = str(state.get("paused", "")).lower() == "true" - if "completed" in state: - state["completed"] = str(state.get("completed", "")).lower() == "true" - # recent_statuses: stored as comma-separated words (e.g. "accepted, rejected, error") - rs_raw = state.get("recent_statuses") or "" - if rs_raw: - state["recent_statuses"] = [s.strip().lower() for s in rs_raw.split(",") if s.strip()] - else: - state["recent_statuses"] = [] - return state - - def read_program_state(program_name): - """Read scheduling state from the repo-memory state file.""" - state_file = os.path.join(repo_memory_dir, f"{program_name}.md") - if not os.path.isfile(state_file): - print(f" {program_name}: no state file found (first run)") - return {} - with open(state_file, encoding="utf-8") as f: - content = f.read() - return parse_machine_state(content) - - # Bootstrap: create autoloop programs directory and template if missing - if not os.path.isdir(autoloop_dir): - os.makedirs(autoloop_dir, exist_ok=True) - bt = chr(96) # backtick — avoid literal backticks that break gh-aw compiler - template = "\n".join([ - "", - "", - "", - "", - "# Autoloop Program", - "", - "", - "", - "## Goal", - "", - "", - "", - "REPLACE THIS with your optimization goal.", - "", - "## Target", - "", - "", - "", - "Only modify these files:", - f"- {bt}REPLACE_WITH_FILE{bt} -- (describe what this file does)", - "", - "Do NOT modify:", - "- (list files that must not be touched)", - "", - "## Evaluation", - "", - "", - "", - f"{bt}{bt}{bt}bash", - "REPLACE_WITH_YOUR_EVALUATION_COMMAND", - f"{bt}{bt}{bt}", - "", - f"The metric is {bt}REPLACE_WITH_METRIC_NAME{bt}. **Lower/Higher is better.** (pick one)", - "", - ]) - with open(template_file, "w") as f: - f.write(template) - # Leave the template unstaged — the agent will create a draft PR with it - print(f"BOOTSTRAPPED: created {template_file} locally (agent will create a draft PR)") - - # Find all program files from all locations: - # 1. Directory-based programs: .autoloop/programs//program.md (preferred) - # 2. Bare markdown programs: .autoloop/programs/.md (simple) - # 3. Issue-based programs: GitHub issues with the 'autoloop-program' label - program_files = [] - issue_programs = {} # name -> {issue_number, file} - - # Scan .autoloop/programs/ for directory-based programs - if os.path.isdir(programs_dir): - for entry in sorted(os.listdir(programs_dir)): - prog_dir = os.path.join(programs_dir, entry) - if os.path.isdir(prog_dir): - # Look for program.md inside the directory - prog_file = os.path.join(prog_dir, "program.md") - if os.path.isfile(prog_file): - program_files.append(prog_file) - - # Scan .autoloop/programs/ for bare markdown programs - bare_programs = sorted(glob.glob(os.path.join(autoloop_dir, "*.md"))) - for pf in bare_programs: - program_files.append(pf) - - # Scan GitHub issues with the 'autoloop-program' label - issue_programs_dir = "/tmp/gh-aw/issue-programs" - os.makedirs(issue_programs_dir, exist_ok=True) - try: - api_url = f"https://api.github.com/repos/{repo}/issues?labels=autoloop-program&state=open&per_page=100" - req = urllib.request.Request(api_url, headers={ - "Authorization": f"token {github_token}", - "Accept": "application/vnd.github.v3+json", - }) - with urllib.request.urlopen(req, timeout=30) as resp: - issues = json.loads(resp.read().decode()) - for issue in issues: - if issue.get("pull_request"): - continue # skip PRs - body = issue.get("body") or "" - title = issue.get("title") or "" - number = issue["number"] - # Derive program name from issue title: slugify to lowercase with hyphens - slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-') - slug = re.sub(r'-+', '-', slug) # collapse consecutive hyphens - if not slug: - slug = f"issue-{number}" - # Avoid slug collisions: if another issue already claimed this slug, append issue number - if slug in issue_programs: - print(f" Warning: slug '{slug}' (issue #{number}) collides with issue #{issue_programs[slug]['issue_number']}, appending issue number") - slug = f"{slug}-{number}" - # Write issue body to a temp file so the scheduling loop can process it - issue_file = os.path.join(issue_programs_dir, f"{slug}.md") - with open(issue_file, "w") as f: - f.write(body) - program_files.append(issue_file) - issue_programs[slug] = {"issue_number": number, "file": issue_file, "title": title} - print(f" Found issue-based program: '{slug}' (issue #{number})") - except Exception as e: - print(f" Warning: could not fetch issue-based programs: {e}") - - if not program_files: - # Fallback to single-file locations - for path in [".autoloop/program.md", "program.md"]: - if os.path.isfile(path): - program_files = [path] - break - - if not program_files: - print("NO_PROGRAMS_FOUND") - os.makedirs("/tmp/gh-aw", exist_ok=True) - with open("/tmp/gh-aw/autoloop.json", "w") as f: - json.dump({"due": [], "skipped": [], "unconfigured": [], "no_programs": True}, f) - sys.exit(0) - - os.makedirs("/tmp/gh-aw", exist_ok=True) - now = datetime.now(timezone.utc) - due = [] - skipped = [] - unconfigured = [] - all_programs = {} # name -> file path (populated during scanning) - - # Schedule string to timedelta - def parse_schedule(s): - s = s.strip().lower() - m = re.match(r"every\s+(\d+)\s*h", s) - if m: - return timedelta(hours=int(m.group(1))) - m = re.match(r"every\s+(\d+)\s*m", s) - if m: - return timedelta(minutes=int(m.group(1))) - if s == "daily": - return timedelta(hours=24) - if s == "weekly": - return timedelta(days=7) - return None # No per-program schedule — always due - - def get_program_name(pf): - """Extract program name from file path. - Directory-based: .autoloop/programs//program.md -> - Bare markdown: .autoloop/programs/.md -> - Issue-based: /tmp/gh-aw/issue-programs/.md -> - """ - if pf.endswith("/program.md"): - # Directory-based program: name is the parent directory - return os.path.basename(os.path.dirname(pf)) - else: - # Bare markdown or issue-based program: name is the filename without .md - return os.path.splitext(os.path.basename(pf))[0] - - for pf in program_files: - name = get_program_name(pf) - all_programs[name] = pf - with open(pf) as f: - content = f.read() - - # Check sentinel (skip for issue-based programs which use AUTOLOOP:ISSUE-PROGRAM) - if "" in content: - unconfigured.append(name) - continue - - # Check for TODO/REPLACE placeholders - if re.search(r'\bTODO\b|\bREPLACE', content): - unconfigured.append(name) - continue - - # Parse optional YAML frontmatter for schedule and target-metric - # Strip leading HTML comments before checking (issue-based programs may have them) - content_stripped = re.sub(r'^(\s*\s*\n)*', '', content, flags=re.DOTALL) - schedule_delta = None - target_metric = None - fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content_stripped, re.DOTALL) - if fm_match: - for line in fm_match.group(1).split("\n"): - if line.strip().startswith("schedule:"): - schedule_str = line.split(":", 1)[1].strip() - schedule_delta = parse_schedule(schedule_str) - if line.strip().startswith("target-metric:"): - try: - target_metric = float(line.split(":", 1)[1].strip()) - except (ValueError, TypeError): - print(f" Warning: {name} has invalid target-metric value: {line.split(':', 1)[1].strip()}") - - # Read state from repo-memory - state = read_program_state(name) - if state: - print(f" {name}: last_run={state.get('last_run')}, iteration_count={state.get('iteration_count')}") - else: - print(f" {name}: no state found (first run)") - - last_run = None - lr = state.get("last_run") - if lr: - try: - last_run = datetime.fromisoformat(lr.replace("Z", "+00:00")) - except ValueError: - pass - - # Check if completed (target metric was reached) - if str(state.get("completed", "")).lower() == "true": - skipped.append({"name": name, "reason": f"completed: target metric reached"}) - continue - - # Check if paused (e.g., plateau or recurring errors) - if state.get("paused"): - skipped.append({"name": name, "reason": f"paused: {state.get('pause_reason', 'unknown')}"}) - continue - - # Auto-pause on plateau: 5+ consecutive rejections - recent = state.get("recent_statuses", [])[-5:] - if len(recent) >= 5 and all(s == "rejected" for s in recent): - skipped.append({"name": name, "reason": "plateau: 5 consecutive rejections"}) - continue - - # Check if due based on per-program schedule - if schedule_delta and last_run: - if now - last_run < schedule_delta: - skipped.append({"name": name, "reason": "not due yet", - "next_due": (last_run + schedule_delta).isoformat()}) - continue - - due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric}) - - # Pick the program to run - selected = None - selected_file = None - selected_issue = None - selected_target_metric = None - deferred = [] - - if forced_program: - # Manual dispatch requested a specific program — bypass scheduling - # (paused, not-due, and plateau programs can still be forced) - if forced_program not in all_programs: - print(f"ERROR: requested program '{forced_program}' not found.") - print(f" Available programs: {list(all_programs.keys())}") - sys.exit(1) - if forced_program in unconfigured: - print(f"ERROR: requested program '{forced_program}' is unconfigured (has placeholders).") - sys.exit(1) - selected = forced_program - selected_file = all_programs[forced_program] - deferred = [p["name"] for p in due if p["name"] != forced_program] - if selected in issue_programs: - selected_issue = issue_programs[selected]["issue_number"] - # Find target_metric: check the due list first, then parse from the program file - for p in due: - if p["name"] == forced_program: - selected_target_metric = p.get("target_metric") - break - if selected_target_metric is None: - # Program may have been skipped (completed/paused/plateau) — parse directly - try: - with open(selected_file) as _f: - _content = _f.read() - _content_stripped = re.sub(r'^(\s*\s*\n)*', '', _content, flags=re.DOTALL) - _fm = re.match(r"^---\s*\n(.*?)\n---\s*\n", _content_stripped, re.DOTALL) - if _fm: - for _line in _fm.group(1).split("\n"): - if _line.strip().startswith("target-metric:"): - selected_target_metric = float(_line.split(":", 1)[1].strip()) - break - except (OSError, ValueError, TypeError): - pass - print(f"FORCED: running program '{forced_program}' (manual dispatch)") - elif due: - # Normal scheduling: pick the single most-overdue program - due.sort(key=lambda p: p["last_run"] or "") # None/empty sorts first (never run) - selected = due[0]["name"] - selected_file = due[0]["file"] - selected_target_metric = due[0].get("target_metric") - deferred = [p["name"] for p in due[1:]] - # Check if the selected program is issue-based - if selected in issue_programs: - selected_issue = issue_programs[selected]["issue_number"] - - result = { - "selected": selected, - "selected_file": selected_file, - "selected_issue": selected_issue, - "selected_target_metric": selected_target_metric, - "issue_programs": {name: info["issue_number"] for name, info in issue_programs.items()}, - "deferred": deferred, - "skipped": skipped, - "unconfigured": unconfigured, - "no_programs": False, + node - << 'JSEOF' + const fs = require('fs'); + const path = require('path'); + + const programsDir = '.autoloop/programs'; + const autoloopDir = '.autoloop/programs'; + const templateFile = path.join(autoloopDir, 'example.md'); + + // Read program state from repo-memory (persistent git-backed storage) + const githubToken = process.env.GITHUB_TOKEN || ''; + const repo = process.env.GITHUB_REPOSITORY || ''; + const forcedProgram = (process.env.AUTOLOOP_PROGRAM || '').trim(); + + // Repo-memory files are cloned to /tmp/gh-aw/repo-memory/{id}/ where {id} + // is derived from the branch-name configured in the tools section (memory/autoloop -> autoloop) + const repoMemoryDir = '/tmp/gh-aw/repo-memory/autoloop'; + + function parseMachineState(content) { + const state = {}; + const sectionMatch = content.match(/## ⚙️ Machine State[^\n]*\n([\s\S]*?)(?=\n## |$)/); + if (!sectionMatch) return state; + const section = sectionMatch[0]; + const rowRegex = /\|\s*(.+?)\s*\|\s*(.+?)\s*\|/g; + let row; + while ((row = rowRegex.exec(section)) !== null) { + const rawKey = row[1].trim(); + const rawVal = row[2].trim(); + if (['field', '---', ':---', ':---:', '---:'].includes(rawKey.toLowerCase())) continue; + const key = rawKey.toLowerCase().replace(/ /g, '_'); + const val = ['\u2014', '-', ''].includes(rawVal) ? null : rawVal; + state[key] = val; + } + // Coerce types + for (const intField of ['iteration_count', 'consecutive_errors']) { + if (intField in state) { + const n = parseInt(state[intField], 10); + state[intField] = isNaN(n) ? 0 : n; + } + } + if ('paused' in state) { + state.paused = String(state.paused || '').toLowerCase() === 'true'; + } + if ('completed' in state) { + state.completed = String(state.completed || '').toLowerCase() === 'true'; + } + // recent_statuses: stored as comma-separated words (e.g. "accepted, rejected, error") + const rsRaw = state.recent_statuses || ''; + if (rsRaw) { + state.recent_statuses = rsRaw.split(',').map(s => s.trim().toLowerCase()).filter(s => s); + } else { + state.recent_statuses = []; + } + return state; } - os.makedirs("/tmp/gh-aw", exist_ok=True) - with open("/tmp/gh-aw/autoloop.json", "w") as f: - json.dump(result, f, indent=2) + function readProgramState(programName) { + const stateFile = path.join(repoMemoryDir, programName + '.md'); + try { + if (!fs.statSync(stateFile).isFile()) { + console.log(' ' + programName + ': no state file found (first run)'); + return {}; + } + } catch (e) { + console.log(' ' + programName + ': no state file found (first run)'); + return {}; + } + const content = fs.readFileSync(stateFile, 'utf-8'); + return parseMachineState(content); + } + + // Schedule string to milliseconds + function parseSchedule(s) { + s = s.trim().toLowerCase(); + let m = s.match(/^every\s+(\d+)\s*h/); + if (m) return parseInt(m[1], 10) * 3600 * 1000; + m = s.match(/^every\s+(\d+)\s*m/); + if (m) return parseInt(m[1], 10) * 60 * 1000; + if (s === 'daily') return 24 * 3600 * 1000; + if (s === 'weekly') return 7 * 24 * 3600 * 1000; + return null; + } - print("=== Autoloop Program Check ===") - print(f"Selected program: {selected or '(none)'} ({selected_file or 'n/a'})") - print(f"Deferred (next run): {deferred or '(none)'}") - print(f"Programs skipped: {[s['name'] for s in skipped] or '(none)'}") - print(f"Programs unconfigured: {unconfigured or '(none)'}") + function getProgramName(pf) { + // Extract program name from file path. + // Directory-based: .autoloop/programs//program.md -> + // Bare markdown: .autoloop/programs/.md -> + // Issue-based: /tmp/gh-aw/issue-programs/.md -> + if (pf.endsWith('/program.md')) { + return path.basename(path.dirname(pf)); + } else { + return path.parse(pf).name; + } + } + + // Main execution + async function main() { + // Bootstrap: create autoloop programs directory and template if missing + if (!fs.existsSync(autoloopDir)) { + fs.mkdirSync(autoloopDir, { recursive: true }); + const bt = String.fromCharCode(96); // backtick -- avoid literal backticks that break gh-aw compiler + const template = [ + '', + '', + '', + '', + '# Autoloop Program', + '', + '', + '', + '## Goal', + '', + "", + '', + 'REPLACE THIS with your optimization goal.', + '', + '## Target', + '', + '', + '', + 'Only modify these files:', + '- ' + bt + 'REPLACE_WITH_FILE' + bt + ' -- (describe what this file does)', + '', + 'Do NOT modify:', + '- (list files that must not be touched)', + '', + '## Evaluation', + '', + '', + '', + bt + bt + bt + 'bash', + 'REPLACE_WITH_YOUR_EVALUATION_COMMAND', + bt + bt + bt, + '', + 'The metric is ' + bt + 'REPLACE_WITH_METRIC_NAME' + bt + '. **Lower/Higher is better.** (pick one)', + '', + ].join('\n'); + fs.writeFileSync(templateFile, template); + console.log('BOOTSTRAPPED: created ' + templateFile + ' locally (agent will create a draft PR)'); + } + + // Find all program files from all locations: + // 1. Directory-based programs: .autoloop/programs//program.md (preferred) + // 2. Bare markdown programs: .autoloop/programs/.md (simple) + // 3. Issue-based programs: GitHub issues with the 'autoloop-program' label + let programFiles = []; + const issuePrograms = {}; + + // Scan .autoloop/programs/ for directory-based programs + if (fs.existsSync(programsDir)) { + try { + if (fs.statSync(programsDir).isDirectory()) { + const entries = fs.readdirSync(programsDir).sort(); + for (const entry of entries) { + const progDir = path.join(programsDir, entry); + try { + if (fs.statSync(progDir).isDirectory()) { + const progFile = path.join(progDir, 'program.md'); + try { + if (fs.statSync(progFile).isFile()) { + programFiles.push(progFile); + } + } catch (e) { /* file doesn't exist */ } + } + } catch (e) { /* stat failed */ } + } + } + } catch (e) { /* stat failed */ } + } + + // Scan .autoloop/programs/ for bare markdown programs + if (fs.existsSync(autoloopDir)) { + try { + if (fs.statSync(autoloopDir).isDirectory()) { + const barePrograms = fs.readdirSync(autoloopDir) + .filter(f => f.endsWith('.md')) + .sort() + .map(f => path.join(autoloopDir, f)); + for (const pf of barePrograms) { + programFiles.push(pf); + } + } + } catch (e) { /* stat failed */ } + } + + // Scan GitHub issues with the 'autoloop-program' label + const issueProgramsDir = '/tmp/gh-aw/issue-programs'; + fs.mkdirSync(issueProgramsDir, { recursive: true }); + try { + const apiUrl = 'https://api.github.com/repos/' + repo + '/issues?labels=autoloop-program&state=open&per_page=100'; + const response = await fetch(apiUrl, { + headers: { + 'Authorization': 'token ' + githubToken, + 'Accept': 'application/vnd.github.v3+json', + }, + }); + const issues = await response.json(); + for (const issue of issues) { + if (issue.pull_request) continue; // skip PRs + const body = issue.body || ''; + const title = issue.title || ''; + const number = issue.number; + // Derive program name from issue title: slugify to lowercase with hyphens + let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + slug = slug.replace(/-+/g, '-'); // collapse consecutive hyphens + if (!slug) slug = 'issue-' + number; + // Avoid slug collisions: if another issue already claimed this slug, append issue number + if (slug in issuePrograms) { + console.log(" Warning: slug '" + slug + "' (issue #" + number + ") collides with issue #" + issuePrograms[slug].issue_number + ", appending issue number"); + slug = slug + '-' + number; + } + // Write issue body to a temp file so the scheduling loop can process it + const issueFile = path.join(issueProgramsDir, slug + '.md'); + fs.writeFileSync(issueFile, body); + programFiles.push(issueFile); + issuePrograms[slug] = { issue_number: number, file: issueFile, title: title }; + console.log(" Found issue-based program: '" + slug + "' (issue #" + number + ")"); + } + } catch (e) { + console.log(' Warning: could not fetch issue-based programs: ' + e.message); + } + + if (programFiles.length === 0) { + // Fallback to single-file locations + for (const p of ['.autoloop/program.md', 'program.md']) { + try { + if (fs.statSync(p).isFile()) { + programFiles = [p]; + break; + } + } catch (e) { /* file doesn't exist */ } + } + } + + if (programFiles.length === 0) { + console.log('NO_PROGRAMS_FOUND'); + fs.mkdirSync('/tmp/gh-aw', { recursive: true }); + fs.writeFileSync('/tmp/gh-aw/autoloop.json', JSON.stringify( + { due: [], skipped: [], unconfigured: [], no_programs: true } + )); + process.exit(0); + } + + fs.mkdirSync('/tmp/gh-aw', { recursive: true }); + const now = new Date(); + const due = []; + const skipped = []; + const unconfigured = []; + const allPrograms = {}; + + for (const pf of programFiles) { + const name = getProgramName(pf); + allPrograms[name] = pf; + const content = fs.readFileSync(pf, 'utf-8'); + + // Check sentinel (skip for issue-based programs which use AUTOLOOP:ISSUE-PROGRAM) + if (content.includes('')) { + unconfigured.push(name); + continue; + } + + // Check for TODO/REPLACE placeholders + if (/\bTODO\b|\bREPLACE/.test(content)) { + unconfigured.push(name); + continue; + } + + // Parse optional YAML frontmatter for schedule and target-metric + // Strip leading HTML comments before checking (issue-based programs may have them) + const contentStripped = content.replace(/^(\s*\s*\n)*/, ''); + let scheduleDelta = null; + let targetMetric = null; + const fmMatch = contentStripped.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); + if (fmMatch) { + for (const line of fmMatch[1].split('\n')) { + if (line.trim().startsWith('schedule:')) { + const scheduleStr = line.substring(line.indexOf(':') + 1).trim(); + scheduleDelta = parseSchedule(scheduleStr); + } + if (line.trim().startsWith('target-metric:')) { + const val = parseFloat(line.substring(line.indexOf(':') + 1).trim()); + if (!isNaN(val)) { + targetMetric = val; + } else { + console.log(' Warning: ' + name + ' has invalid target-metric value: ' + line.substring(line.indexOf(':') + 1).trim()); + } + } + } + } + + // Read state from repo-memory + const state = readProgramState(name); + if (state && Object.keys(state).length > 0) { + console.log(' ' + name + ': last_run=' + (state.last_run || null) + ', iteration_count=' + (state.iteration_count != null ? state.iteration_count : null)); + } else { + console.log(' ' + name + ': no state found (first run)'); + } + + let lastRun = null; + const lr = state.last_run || null; + if (lr) { + try { + const d = new Date(lr.endsWith('Z') ? lr : lr.replace('Z', '+00:00')); + if (!isNaN(d.getTime())) lastRun = d; + } catch (e) { + // ignore invalid date + } + } + + // Check if completed (target metric was reached) + if (String(state.completed || '').toLowerCase() === 'true') { + skipped.push({ name: name, reason: 'completed: target metric reached' }); + continue; + } + + // Check if paused (e.g., plateau or recurring errors) + if (state.paused) { + skipped.push({ name: name, reason: 'paused: ' + (state.pause_reason || 'unknown') }); + continue; + } + + // Auto-pause on plateau: 5+ consecutive rejections + const recent = (state.recent_statuses || []).slice(-5); + if (recent.length >= 5 && recent.every(s => s === 'rejected')) { + skipped.push({ name: name, reason: 'plateau: 5 consecutive rejections' }); + continue; + } + + // Check if due based on per-program schedule + if (scheduleDelta && lastRun) { + if (now.getTime() - lastRun.getTime() < scheduleDelta) { + skipped.push({ + name: name, + reason: 'not due yet', + next_due: new Date(lastRun.getTime() + scheduleDelta).toISOString(), + }); + continue; + } + } + + due.push({ name: name, last_run: lr, file: pf, target_metric: targetMetric }); + } + + // Pick the program to run + let selected = null; + let selectedFile = null; + let selectedIssue = null; + let selectedTargetMetric = null; + let deferred = []; + + if (forcedProgram) { + // Manual dispatch requested a specific program -- bypass scheduling + // (paused, not-due, and plateau programs can still be forced) + if (!(forcedProgram in allPrograms)) { + console.log("ERROR: requested program '" + forcedProgram + "' not found."); + console.log(' Available programs: ' + JSON.stringify(Object.keys(allPrograms))); + process.exit(1); + } + if (unconfigured.includes(forcedProgram)) { + console.log("ERROR: requested program '" + forcedProgram + "' is unconfigured (has placeholders)."); + process.exit(1); + } + selected = forcedProgram; + selectedFile = allPrograms[forcedProgram]; + deferred = due.filter(p => p.name !== forcedProgram).map(p => p.name); + if (selected in issuePrograms) { + selectedIssue = issuePrograms[selected].issue_number; + } + // Find target_metric: check the due list first, then parse from the program file + for (const p of due) { + if (p.name === forcedProgram) { + selectedTargetMetric = p.target_metric || null; + break; + } + } + if (selectedTargetMetric === null) { + // Program may have been skipped (completed/paused/plateau) -- parse directly + try { + const _content = fs.readFileSync(selectedFile, 'utf-8'); + const _contentStripped = _content.replace(/^(\s*\s*\n)*/, ''); + const _fm = _contentStripped.match(/^---\s*\n([\s\S]*?)\n---\s*\n/); + if (_fm) { + for (const _line of _fm[1].split('\n')) { + if (_line.trim().startsWith('target-metric:')) { + const val = parseFloat(_line.substring(_line.indexOf(':') + 1).trim()); + if (!isNaN(val)) { + selectedTargetMetric = val; + break; + } + } + } + } + } catch (e) { /* ignore */ } + } + console.log("FORCED: running program '" + forcedProgram + "' (manual dispatch)"); + } else if (due.length > 0) { + // Normal scheduling: pick the single most-overdue program + due.sort((a, b) => (a.last_run || '').localeCompare(b.last_run || '')); // null/empty sorts first (never run) + selected = due[0].name; + selectedFile = due[0].file; + selectedTargetMetric = due[0].target_metric || null; + deferred = due.slice(1).map(p => p.name); + // Check if the selected program is issue-based + if (selected in issuePrograms) { + selectedIssue = issuePrograms[selected].issue_number; + } + } + + const issueProgramsMap = {}; + for (const [name, info] of Object.entries(issuePrograms)) { + issueProgramsMap[name] = info.issue_number; + } + + const result = { + selected: selected, + selected_file: selectedFile, + selected_issue: selectedIssue, + selected_target_metric: selectedTargetMetric, + issue_programs: issueProgramsMap, + deferred: deferred, + skipped: skipped, + unconfigured: unconfigured, + no_programs: false, + }; + + fs.mkdirSync('/tmp/gh-aw', { recursive: true }); + fs.writeFileSync('/tmp/gh-aw/autoloop.json', JSON.stringify(result, null, 2)); + + console.log('=== Autoloop Program Check ==='); + console.log('Selected program: ' + (selected || '(none)') + ' (' + (selectedFile || 'n/a') + ')'); + console.log('Deferred (next run): ' + (deferred.length > 0 ? JSON.stringify(deferred) : '(none)')); + console.log('Programs skipped: ' + (skipped.length > 0 ? JSON.stringify(skipped.map(s => s.name)) : '(none)')); + console.log('Programs unconfigured: ' + (unconfigured.length > 0 ? JSON.stringify(unconfigured) : '(none)')); + + if (!selected && unconfigured.length === 0) { + console.log('\nNo programs due this run. Exiting early.'); + process.exit(1); // Non-zero exit skips the agent step + } + } - if not selected and not unconfigured: - print("\nNo programs due this run. Exiting early.") - sys.exit(1) # Non-zero exit skips the agent step - PYEOF + main().catch(err => { console.error(err.message || err); process.exit(1); }); + JSEOF source: githubnext/autoloop engine: copilot diff --git a/workflows/sync-branches.md b/workflows/sync-branches.md index a45df2d..9dce6a5 100644 --- a/workflows/sync-branches.md +++ b/workflows/sync-branches.md @@ -24,89 +24,88 @@ steps: GITHUB_REPOSITORY: ${{ github.repository }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | - python3 - << 'PYEOF' - import os, subprocess, sys - - token = os.environ.get("GITHUB_TOKEN", "") - repo = os.environ.get("GITHUB_REPOSITORY", "") - default_branch = os.environ.get("DEFAULT_BRANCH", "main") - - # List all remote branches matching the autoloop/* pattern - result = subprocess.run( - ["git", "branch", "-r", "--list", "origin/autoloop/*"], - capture_output=True, text=True - ) - if result.returncode != 0: - print(f"Failed to list remote branches: {result.stderr}") - sys.exit(0) - - branches = [b.strip().replace("origin/", "") for b in result.stdout.strip().split("\n") if b.strip()] - - if not branches: - print("No autoloop/* branches found. Nothing to sync.") - sys.exit(0) - - print(f"Found {len(branches)} autoloop branch(es) to sync: {branches}") - - failed = [] - for branch in branches: - print(f"\n--- Syncing {branch} with {default_branch} ---") - - # Fetch both branches - subprocess.run(["git", "fetch", "origin", branch], capture_output=True) - subprocess.run(["git", "fetch", "origin", default_branch], capture_output=True) - - # Check out the program branch - checkout = subprocess.run( - ["git", "checkout", branch], - capture_output=True, text=True - ) - if checkout.returncode != 0: - # Try creating a local tracking branch - checkout = subprocess.run( - ["git", "checkout", "-b", branch, f"origin/{branch}"], - capture_output=True, text=True - ) - if checkout.returncode != 0: - print(f" Failed to checkout {branch}: {checkout.stderr}") - failed.append(branch) - continue - - # Merge the default branch into the program branch - merge = subprocess.run( - ["git", "merge", f"origin/{default_branch}", "--no-edit", - "-m", f"Merge {default_branch} into {branch}"], - capture_output=True, text=True - ) - if merge.returncode != 0: - print(f" Merge conflict or failure for {branch}: {merge.stderr}") - # Abort the merge to leave a clean state - subprocess.run(["git", "merge", "--abort"], capture_output=True) - failed.append(branch) - continue - - # Push the updated branch - push = subprocess.run( - ["git", "push", "origin", branch], - capture_output=True, text=True - ) - if push.returncode != 0: - print(f" Failed to push {branch}: {push.stderr}") - failed.append(branch) - continue - - print(f" Successfully synced {branch}") - - # Return to default branch - subprocess.run(["git", "checkout", default_branch], capture_output=True) - - if failed: - print(f"\n⚠️ Failed to sync {len(failed)} branch(es): {failed}") - print("These branches may need manual conflict resolution.") - # Don't fail the workflow — log the issue but continue - else: - print(f"\n✅ All {len(branches)} branch(es) synced successfully.") - PYEOF + node - << 'JSEOF' + const { execSync, spawnSync } = require('child_process'); + + const defaultBranch = process.env.DEFAULT_BRANCH || 'main'; + + function git(...args) { + const result = spawnSync('git', args, { encoding: 'utf-8' }); + return { returncode: result.status, stdout: result.stdout || '', stderr: result.stderr || '' }; + } + + // List all remote branches matching the autoloop/* pattern + const listResult = git('branch', '-r', '--list', 'origin/autoloop/*'); + if (listResult.returncode !== 0) { + console.log('Failed to list remote branches: ' + listResult.stderr); + process.exit(0); + } + + const branches = listResult.stdout.trim().split('\n') + .map(b => b.trim()) + .filter(b => b) + .map(b => b.replace('origin/', '')); + + if (branches.length === 0) { + console.log('No autoloop/* branches found. Nothing to sync.'); + process.exit(0); + } + + console.log('Found ' + branches.length + ' autoloop branch(es) to sync: ' + JSON.stringify(branches)); + + const failed = []; + for (const branch of branches) { + console.log('\n--- Syncing ' + branch + ' with ' + defaultBranch + ' ---'); + + // Fetch both branches + git('fetch', 'origin', branch); + git('fetch', 'origin', defaultBranch); + + // Check out the program branch + let checkout = git('checkout', branch); + if (checkout.returncode !== 0) { + // Try creating a local tracking branch + checkout = git('checkout', '-b', branch, 'origin/' + branch); + } + if (checkout.returncode !== 0) { + console.log(' Failed to checkout ' + branch + ': ' + checkout.stderr); + failed.push(branch); + continue; + } + + // Merge the default branch into the program branch + const merge = git('merge', 'origin/' + defaultBranch, '--no-edit', + '-m', 'Merge ' + defaultBranch + ' into ' + branch); + if (merge.returncode !== 0) { + console.log(' Merge conflict or failure for ' + branch + ': ' + merge.stderr); + // Abort the merge to leave a clean state + git('merge', '--abort'); + failed.push(branch); + continue; + } + + // Push the updated branch + const push = git('push', 'origin', branch); + if (push.returncode !== 0) { + console.log(' Failed to push ' + branch + ': ' + push.stderr); + failed.push(branch); + continue; + } + + console.log(' Successfully synced ' + branch); + } + + // Return to default branch + git('checkout', defaultBranch); + + if (failed.length > 0) { + console.log('\n\u26a0\ufe0f Failed to sync ' + failed.length + ' branch(es): ' + JSON.stringify(failed)); + console.log('These branches may need manual conflict resolution.'); + // Don't fail the workflow -- log the issue but continue + } else { + console.log('\n\u2705 All ' + branches.length + ' branch(es) synced successfully.'); + } + JSEOF --- Sync all autoloop/* branches with the default branch. From e5e395b64193ba5b58ae5c3010fbaf8ad85c51fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:17:12 +0000 Subject: [PATCH 3/4] Add .gitignore and remove accidentally committed __pycache__ Agent-Logs-Url: https://github.com/githubnext/autoloop/sessions/639174de-6b45-471f-878b-05ffaca3cdfa Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- .gitignore | 2 ++ .../conftest.cpython-312-pytest-9.0.2.pyc | Bin 5870 -> 0 bytes ...est_scheduling.cpython-312-pytest-9.0.2.pyc | Bin 101713 -> 0 bytes 3 files changed, 2 insertions(+) create mode 100644 .gitignore delete mode 100644 tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_scheduling.cpython-312-pytest-9.0.2.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c56ff1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.pytest_cache/ diff --git a/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index b231d5d9c55c403e0f1231c761234b928ee4c8a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5870 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 J4S@Z~|38ThDDMCO From 1cba2b0327e705cf7de9de90d49d89c7e9341149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:19:36 +0000 Subject: [PATCH 4/4] Address code review feedback: improve path escaping, date parsing, and add inline comments for unicode escapes Agent-Logs-Url: https://github.com/githubnext/autoloop/sessions/639174de-6b45-471f-878b-05ffaca3cdfa Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- tests/conftest.py | 4 ++-- workflows/autoloop.md | 4 ++-- workflows/sync-branches.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c6033a4..f59db42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,9 +56,9 @@ def _load_workflow_functions(): def _call_js(func_name, *args): """Call a JS function from the extracted workflow module and return the result.""" args_json = json.dumps(list(args)) - escaped_path = _JS_MODULE_PATH.replace("\\", "\\\\") + escaped_path = json.dumps(_JS_MODULE_PATH) script = ( - "const m = require('" + escaped_path + "');\n" + "const m = require(" + escaped_path + ");\n" "const result = m." + func_name + "(..." + args_json + ");\n" "process.stdout.write(JSON.stringify(result === undefined ? null : result));\n" ) diff --git a/workflows/autoloop.md b/workflows/autoloop.md index 2bbaabd..d3f6a95 100644 --- a/workflows/autoloop.md +++ b/workflows/autoloop.md @@ -117,7 +117,7 @@ steps: const rawVal = row[2].trim(); if (['field', '---', ':---', ':---:', '---:'].includes(rawKey.toLowerCase())) continue; const key = rawKey.toLowerCase().replace(/ /g, '_'); - const val = ['\u2014', '-', ''].includes(rawVal) ? null : rawVal; + const val = ['\u2014', '-', ''].includes(rawVal) ? null : rawVal; // \u2014 = em dash state[key] = val; } // Coerce types @@ -391,7 +391,7 @@ steps: const lr = state.last_run || null; if (lr) { try { - const d = new Date(lr.endsWith('Z') ? lr : lr.replace('Z', '+00:00')); + const d = new Date(lr); if (!isNaN(d.getTime())) lastRun = d; } catch (e) { // ignore invalid date diff --git a/workflows/sync-branches.md b/workflows/sync-branches.md index 9dce6a5..5111d05 100644 --- a/workflows/sync-branches.md +++ b/workflows/sync-branches.md @@ -99,11 +99,11 @@ steps: git('checkout', defaultBranch); if (failed.length > 0) { - console.log('\n\u26a0\ufe0f Failed to sync ' + failed.length + ' branch(es): ' + JSON.stringify(failed)); + console.log('\n\u26a0\ufe0f Failed to sync ' + failed.length + " branch(es): " + JSON.stringify(failed)); // \u26a0\ufe0f = warning sign console.log('These branches may need manual conflict resolution.'); // Don't fail the workflow -- log the issue but continue } else { - console.log('\n\u2705 All ' + branches.length + ' branch(es) synced successfully.'); + console.log('\n\u2705 All ' + branches.length + " branch(es) synced successfully."); // \u2705 = checkmark } JSEOF ---