From 8dea63420af1c15648f3a87642030648c72e1c15 Mon Sep 17 00:00:00 2001 From: nhathuy7611 Date: Sun, 25 Oct 2026 23:54:43 +0700 Subject: [PATCH 1/2] docs: define AI Office zone architecture --- docs/ai-office-architecture.md | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/ai-office-architecture.md diff --git a/docs/ai-office-architecture.md b/docs/ai-office-architecture.md new file mode 100644 index 000000000..b09c19557 --- /dev/null +++ b/docs/ai-office-architecture.md @@ -0,0 +1,40 @@ +# AI Office architecture + +The first slice adds an organizational layer around the existing building. A floor still owns one project checkout and its workers, queue and boards. No worker or floor record is rewritten. + +``` +OfficeZone (training | main) + └── Department + └── Floor (project checkout) + └── Worker (running session) +``` + +## Boundaries + +- Zones and departments are persistent, versioned metadata owned by the office, not by a floor. Their defaults are installed once: Training Campus with its seven logical labs, and Main Office with its eleven departments. +- A floor has one department assignment. Existing floors without an assignment resolve to Main Office / Software Engineering. Assignment does not move a checkout or change its workers. +- Client navigation selects a zone and department and uses the existing `floor.go` path for travel. The selected zone is a browser view preference, not an execution boundary. +- The new HTTP API uses the existing signed session. Reads are available to signed-in users; only office admins can change department or floor assignments. The server derives the actor from the session. +- Provider, model, role, AgentProfile, worker and task remain distinct concepts. AgentProfiles, evaluation, promotion and routing belong to later slices. + +## Persistence and migration + +`zones.json` stores a schema version, zone/department definitions and floor assignments in the office data directory. The server validates it on load and writes a replacement file before renaming it into place. An absent file is initialized with defaults. Existing floors are resolved to Main Office / Software Engineering without editing `floors.json`; new and removed floors are reconciled by ID. Reloading does not duplicate defaults or assignments. + +Department and floor-assignment edits append an actor-derived audit record in the same atomic metadata write. The audit records are not exposed in the navigation response. + +Malformed or unsupported metadata must fail closed with a logged error. It must not overwrite the saved file or silently move a floor. Keep all original floor, worker, queue, account and worktree files untouched. + +## Security boundary + +Training Campus is an organizational view in this slice. Existing git worktrees isolate checkouts and branches, but all worker processes still run as the same OS user. Training agents must not be treated as sandboxed. The Sandbox Lab needs an execution boundary, credential separation, resource limits and enforced tool policy before untrusted agents run there. Promotion remains unavailable until evaluation, security review and human approval are implemented. + +## Delivery sequence + +1. Zones, departments, metadata store and legacy floor resolution. +2. Authenticated API and 3D/`/lite` navigation over the current floor travel path. +3. Persistent AgentProfiles and project stages. +4. Evaluation, promotion, approval and audit records. +5. Department orchestration and assisted routing. + +Each slice keeps the existing protocol and floor behavior valid and is verified with typecheck, tests, build and a browser screenshot for UI changes. From a8e8c6a94a331f06538a165eca73fe636295ffb3 Mon Sep 17 00:00:00 2001 From: nhathuy7611 Date: Sun, 25 Oct 2026 23:54:52 +0700 Subject: [PATCH 2/2] feat: add AI Office zones and department navigation --- README.md | 2 + docs/ai-office-3d-modal.png | Bin 0 -> 44341 bytes docs/ai-office-lite.png | Bin 0 -> 56213 bytes docs/code-layout.md | 4 + docs/features.md | 2 + src/client/features/hud/index.ts | 2 + src/client/lite.html | 1 + src/client/lite.ts | 2 + src/client/ui/structure.css | 14 +++ src/client/ui/structure.ts | 98 +++++++++++++++++ src/server/http/router.ts | 2 +- src/server/http/routes/index.ts | 6 + src/server/http/routes/structure.ts | 30 +++++ src/server/office-structure.ts | 165 ++++++++++++++++++++++++++++ src/server/office/context.ts | 2 + src/server/office/core.ts | 4 +- src/shared/protocol.ts | 2 + src/shared/protocol/departments.ts | 10 ++ src/shared/protocol/zones.ts | 17 +++ tests/client-registry.test.ts | 2 +- tests/office-structure.test.ts | 59 ++++++++++ 21 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 docs/ai-office-3d-modal.png create mode 100644 docs/ai-office-lite.png create mode 100644 src/client/ui/structure.css create mode 100644 src/client/ui/structure.ts create mode 100644 src/server/http/routes/structure.ts create mode 100644 src/server/office-structure.ts create mode 100644 src/shared/protocol/departments.ts create mode 100644 src/shared/protocol/zones.ts create mode 100644 tests/office-structure.test.ts diff --git a/README.md b/README.md index fac9441a5..09d7b784f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/i ## What it is +- **AI Office zones.** Open **Training Campus & Main Office** from the ☰ menu (or **Zones** in `/lite`) to browse departments and their project floors. Existing projects start in Main Office / Software Engineering. Admins can add or edit departments and assign projects. Training Campus is currently an organizational view, not an execution sandbox; see [AI Office architecture](docs/ai-office-architecture.md). + - **A floor per project.** Ride the elevator, pick one of your GitHub repos, and the office clones it (showing how far along it is) and opens a floor for it. Every worker, board and queue on that floor works in that checkout. - **Workers at desks.** Walk up to an empty desk, press **E**, and pick Claude Code, Codex, OpenCode, Grok, Muse, DeepSeek Harness, Pi or Cursor, each with its model and reasoning effort. The agent's live terminal shows on its laptop, and anyone can open it and type. - **Talk instead of typing.** Hold **Ctrl+Space** (or the **🎤**) in a worker's terminal or a prompt box and say what you want: it's typed in for you to send. Your browser does the listening, so there's nothing to install. diff --git a/docs/ai-office-3d-modal.png b/docs/ai-office-3d-modal.png new file mode 100644 index 0000000000000000000000000000000000000000..932880e32a6f73072c9c94af93ab47063e561ec9 GIT binary patch literal 44341 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 x3QS0kv@`x600960f1$eN00006NklWX literal 0 HcmV?d00001 diff --git a/docs/ai-office-lite.png b/docs/ai-office-lite.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b46944f2e063fbcac4439d44229642eca992dd GIT binary patch literal 56213 zcmbrmcQ{<%+y6UANC?tI5PU?98a;@Th-lFbMvZ9EMjwnG5sBU>dJje)y^a*UMH^+* zFnWp3F#NX9@B2LGIp@00ALn=c!?mwn)?Rz9d)@EX`(D0isw+~G)02ZhAWCJWSK1)Z zb>L5ue{K*1zxMG4B_Pmkkn*b+x?X9UGdJJrj?EnJ3g3H4%tG;xYM_#(DDT~*&mT}- z93QxpUho*aYR~6S(5|UD@^CaMq*ma82GW$g-1zj^hotNA$M*&n#2+iJDG^(mvBV!| zN-SA_RepK*2XFWBi1(j#sg+qD>*emJ=<`A8y6-v1vpPTnK%h|%>#GKVyoQ>-aB-8t zg!O+oAHy}c3Z#&E)aZ@k_ph%@jQpor&i@R(HyUl7ILSq3+}vm>-Luzx$PM%w`)JH7qIYXO&^XQl!m5^zvCi zn>Gm#8JkW!r8{P#80DK_YZJAOJOP&+)^3bxFWcW$T3T6gva@%#GfTR2XPzu|Zt@+tNhtZ#Kpw8Or8NuXqV6J_ z!91(wrDfU&8Rd00BSfGcE1_h1v4f+1`{`zgPEn_dV67t-Gd306{_gZ(&TOsmy#b=Q zGon5_u7d}2QE1Z=XEAfOv77NMtHls_GsXcTQsm-jSz?nqV#aaE&z<@5gxUXjLNUEZ zy}6Q&Z0a`&cndf;iiFJF*0m^f69z>maw_+BO;zpuy*Oc=l<+181%`DqzosB3$H9&q z@Ik?$R%-Da{xd%$kDK{RY&GA4kN#nS0Ud_mH{$QT3*|{>gY=*~(gsmqLNf+T4p%7J zv>4OciJVDl@8~ZvPN76)&f=&iV?$x-noO^Inm5E01PoBForCBYeaWSBN<>lH zFi%)T);E4EzUMTXeWGK6uP$3+!KRZvn!w9{Sjq2{t_&4-`0|A7nN7(W6UMwJFLps% zs^d!L~1vGP&T&V`9ylNAuUw6s${apWkZaex{>$E7k4|3xKj7 z?X76deOiveykIFkn~}+xTdSTPY(1Yl3vW#dB_}7>FQ1J`OOy1S=uh0qbHwVwS=dEe za>P31Ut0`<6R)}lm?Ji}o6MT&p=>76+d3@}IpfvTM5BlUsikEZhSv1sUnCX+>Sz#1 z_sjV{%jM@TS@u2ZFjLn2d8by0$*JDO1?{)~u3mwX!pPm2p$WJD>ot+4xt}7LL^uCH z%6qPzh;yH6Y!}^Q=R98i#ayB8h#Rk0&Zkb?@|_nQ<`P)wq$G1q;EXx&`G-YW;BxCy zdT(Hki4{)_;*USqd3>4GCCh`~2F4Z7V4Ik*Ay{a>a3Z_1067KBjcCxVQL64@9{q9z zc0dQIyh6oSvO(n-D=KUHY^_dJS)wj3v`1v^w}QB2Vp{3+OrzS+Ydm48MRramCzBYI zl?2>-FFM>b=~gq8Kur$fCn0)Rf7kGs!9%_i^|(+sB9ek0yr-Eg=2&+14hv@LPdsZm zk17QBAgx&kYj=SBaGMQJVK!rDn zp9a!T)w*T2{uzo%J$t%G2Xa>EmK7Cc) z?|-*BprW!wlHXu1MGkDe#=!lA^P5Y7Hg9i;1g0qUb_tQnqW>OtS!dZ3S661g!T zGgUaDt!cpJNKD+@GHnhY{WB>3?V+cX#tET<3X>mEn9mGVsj_E;t*}~1a)b1#Xx@TE zD`hV}o7UU>-~-muf~OZllgvJH61}a`=sdQ9bzJdCd^w-W&8IZ5KA9Ple0)lnC9dIz z8)sCyWV-7&aw)05xVdpo-E-&7@tuB%w*gW8yBlpFBp(n>H`AU&R+%tCdjg;~+a+ek z(*nf@s4BO(xHR|tkI&@m1;#$20blI zPMW_)6|FB#p`O&xqVY7Dcz><%q~L9STW6aUisR#QQx_$o`j-!hDYK@c*iBC4PZE?_ zX+T*S%Fp3;{hw9~GZ(2i*wU4IQ3kw;B4Vli{i%4*_1|JSp5w9d_)n&O4YU$50q_I& z->b)f;U4VwM>%q2}mDA^iZyby}|iUb23y?v<=5h))Z8vSCl4~pzdgkPM|EZ_Rut%Zb~Vhb)0x<4@H+YlkrNj3{l!D{TBy)S!g zPnAKWWjBbQ&X?T{NUQquel0!lyAC{$capw%$7F0jyW2dd$7&$YYCYsZKk;>6#_-2my(FX3#%{By&R3DRHB=P*?Yj3mQcQJftLfb!Fdx`31 z$D4SGtk0X*f0jzpshGBi{t>#1+|ruXREM^j{ao;a2M0S(N-ATPo56ias4uygFUgucl$$3wT! z9B>%MkJC8xT|Nj=nnUH?zcnBB4~Rq*f$|s&`xf8v5owror)e^Bq@0WQc{%NyJTp)s zS>BO--W+tM@wxO_tUmz`b}{{B|47u_tON#R#M-r3o#m&Yk-n| zF=CQ;2ip=~f=KyQH^Z2J`uoU}fs2QQddg@gJlZxFwn|hx^|?H?jNfcu55zy-T`fW^ z^A7cDpQN8*C=wCAJS>%`RT^{25*yb|$dhJ{D0Z|L_k+@pqP1chg1QHdtdfjzh2G|{ ztfH)Z{TDyoB-Hws-)^uOB#}}MWK2X!9mX)EpHAEkymjDl?kn*pDC!6O-WcQflGPx+m{GScAg+pDT-A3S1vIKDTI=7Q{qfW^k4 z;Mli*09~zaU*25hEiBFtsjV!j_MziX;oA6+P+Gj~Ao|CCY$~C9qS;*oGF=nB4GP37 z{F^{&4Eiq#jg)gp#hR$4%BVtCel_+fXQ#gj8-PF{-rlE>$w`SxiHnO1#6;G?wYQ2v zcj9dnxj^dHdLX;i^@_6ma2F$ewxn^k-A?iq6!Lw%-LuvoER#u+cwFNm7;3S2Fl3PW z(&c4BtUo)SMhXF|OilTA$_7-Zh;jIgp^y)yD#UxE5o}Fyj8lw12+1X!|1gl04`$Zg zr-3|sa^o+rZeUOf1%r&7Ye^ZRqs$<`aY{OauBL_8_fur0I>JgPI&y}|N2hRRVhv{D zFJ&9y*pFF$^tP$R=DfMCMAs)fx!)OA-=cMAtC7vPF>>auL+a2_vX)K6m?HXoMQcO8 zE-E&o*id-qXL)loAw}GAd2w-ZYs*onkU};^jKX2;2imXr6FF$!T}#&NL~6V6PB6GM zj&WT$2zOgFm1@csTECX_a?8W7=?RD_ocyV?#8f1mo5z}~wS)0;d$jep1h7sy6-CX3 zc(uvb%5C=!Ip?YlO~b^Pcw#!kquz zORuz3^5r1pYGi&>uadC}kg4PW@z2oT{5_-FZKuV};eKJR;d~G3E_6(wrr$|4`qx}W zf?XM&;=2hhiU)8=OcT6Cq>HcRBPqx$rk;qxWt{Pwfx}(e z@cXlsz$E=A*gNe!53a5$qLse5{Q%PX|H?v;-%LTfxkb4yC>uC3XL!ZbYQ zpMdyZ08!CQ?pLX_QBqSYT2Rigz05#tC9~IoGfY;v_rF+xB|l@lv3yXAPXcbZzLZ_6 zuEkVq9L9*me0#@Es}aVn+{7;E^+C`Xk2L(eg@s+jAuOKs@d#nAJ$jDeT~HeAi%Zy! zPtfy~6xPM9hN^HpdLmZ9VFR`ryzA+xQ#^sPaPkOcM&mIBuPG-7#ChuI=?q{#+_=q< zi!*W4+WFH`o)u+V@k4j`>*bJv;~k%^sG3XTztNNTplFFaM~a!f*)KougS=$A%doWy zm++oN9=#GCZf=yH(zgFDF3e|};P|Fb26tasxm&-m(g#VRLF5eNu$m0SKexF&;Y)Eu z!!!=V9&+7j28i~fTb5G#8ud>-^uSZPIZI(`m1tI*k}UwZS=yAlZu zD=$a&N}XxCrMFLXhL+=x*6!w~kEF&ot6qWEvf`B3x7Tq{6aEOnqz@GdA>c&E36!dK znJK0cwzC{P>rJ?s`(kPN;o>VZc|ZsEwnTKcZTN^lmslzIQpn1T@#2$-RNdsD=b7D> zjYm`JCX$|Bhfe#ggfd=|Oes(Q$$(ZLLZqjU$#|(e8K>W$_I73=JlKUNUfx0jTmxt* zrWFuL4Qs`5>AitxrhEDx4`pm&A7{qgNk}ysM050B%ZAQAJ*%6^Q*Rnteq)hpn^rvi z4asjTS+A!~JaD`xT4!5)S74g5r}w6`{&eAjcVhX}id9-wpKN&hmBt@;)8y&VB~lBefKfp6~j~t*XPf0Vl4|XK`q-4rRB&hT7D*stkv3_sG?!$ej@e{4mPXMj=$SB#?S*&0T<`#o)IX6FhIo195YjN%z1CFl1wN>Fr4anEr7ha1kjdmVnmYEUcQ{-8r1z)h!Z_5%!redYEGfP{%PhIyRNII z`gntq%Dz-ziMzlPX6W&`ir{sEuve{%JCiR zJyiz00M!xRu64MhtEK9_+Eq!x8fq2q6Cgc%uiys~pjPvrS$>0z^XlsrUO-HIRWH9Y zyEE;=gX5Mi&C6LX-%G7#(y`DoRT&J>W;88bICRP$HpUZz&@>QB5kb&aXL8kP^g3IFTRtcMnrhVCll?u%VXsCVww3YnNfJS_Zn+CK|^eA`9z3>@eB+^SEkq zsZ^!)vxx2k2-JaJ8qtTf@zJbP-Tn=`Sc%GJ&z;J%F5Xf3I(@8~gCs_Gv^pBboIdohXo?SY9Hhh$ow-g7+%MIEHcYPfElvd(3-l|!gq|a7sfz~VM%Yx&X+9@&mGt;=F8kcvZ-F;L z6*Hs1Ygb6kT9L<;z$i6eXc&~aw4TLMaL#&Q??ulAT=)*n&A~6r%Uj&GeTP2o)``8J zZ+1%Mh+pgYyq#DVONxH<%-VC;{ANArsy|5-6vjd z{?-~^%S&5*vro2^Jj(2A#5$lFJHDB~R~(BhooW{|d-D}v=;zA!l60D^@HQbF8V|SzHJf4cZVQ2h()wN5@|8k-{LMMa=u4HCX)Tu_@ zvxObQ-Uroxc8N4I(pH=~vN-5u7C1YU_PjW%w3Q@qZ=@Mi`5*sI_ngMho`nZoc$b?r z)Wk8GMx{bmy-kj(z_w5N^D6h8!2zux2Q4C?oB6_opwCc}r3cX8-8zc?h5_fRU(%5V ztu!-2zRtZdVm-R9R24g?=SEfc!0m{(8pcZpaT!RGRpdYiIP-hj_P{;Ih7+b_s<(T^fxOJogbYJWa&BncUc(Qn@92(yvNbhn zcF}JsL|I=s_ah{L+IGBS{5VI$7J%L8Owq;drp}OBCmW&Uo|$YUJ1U?kM30AZ_P2OR zW%=M#V=|^sGINY#fQmhm!=7gE< z?6e`QGe@y8B_0@gmHqw1!1{@jnfJ0o{9yOUe(GdtS4Sq&_Y>s}Eg(@{2eMlDLJj)D z1(2J_Bj4khMw`n6@fPEm0JR9yCmXz);;-GyCza-_)*#rNBd|YdWK%=h3WeNNm%3um zEvZWye>uugcY|KdxvdQPB$sa)tbh|g^i+P z32@nB3Rpb>2<(WBkn2AAbSVdKxyJv^hInN*FLWo6s9tVdoy9ud^R+c>RHJg1^3BR1 zF1a5G5cK{OQW{z#^1*Zp=*aMN+vC^??F6&0`qQT=*6ke2ydaQ`J^jS1o;=iTx(1Jx z0TTk=1kU{(8Dr8-6)9hsf=m?Ql4{6y`Z|3-5FQ9b&p2HM%w@@wq^32yk&YUJv zp$_3w<%qej`?EHgt!}4?BR+2KIFm`HmqdY8np%K;A$JN~B${-*6P+os5z?F10ue)` zca`->UjrH2{_EEyI4#Ar)_MY*Vb*PD5s>rRqK_GRmOJKGT&amb`uG2JK{Q)`7aK*> z4F$AMZFSH;nN?4fv0wd7l>7P?74;6&qiNLMiy`ej>Hnq{TRG)1>AfuXLC+|N9s=eC z4gBB&t6vrkP^wxU_8!S^lj}mM{V2EVAoX9j-vcM;Dsfi_Bugup5TT5E(4e%w(Uui} z*V$PDDHx!anNH^8Y4&AKRK_wmZ6Y#V<{pAsrJ-<5BDu(?*XV)k5&xLiKw>Uc-I69E zL&}J%CV6Y-zAOTi%2|>aFkV3XYLD&FfBE-m{p_VU^*sK=qimLhLHdGxh7|@xavpuj zO-ce+cU#Z?m0JMl(e_S=`k$<-x2_CK_D9A`(-I-#|9TAHFB4cF=AlnY*iAN8Hl zfON?K$_Ttd@W%39J+E3n`f<)UY(!i0ZCzyc`mX0)&ckQ9fUd`x`aqYSd7R~=O5XpAuLM! zUtjA`yx56>zP+W}`o7)+1|dQ7i&NLY^rK@jsUJ_7!)o|B70b`Jy~-;ISYxl8e8&U) z_n=*d#r<=7@t$rl@cclscR>HBVcyw>i#dX^Fg}OZFET&J9q^pGO|Mq^86}9$@&oU+ zx>~siui>__Q7dEQr#A5tkr0#VtyC@}=bw2l0k>KE2lsqz+j{b8&ej*Mn|1j5I`@NK z>1vmmcB#s5W@GtzE<1YMoc=2UlU6o25Rc$qh$w<8lQt&MG0)73k5#H{aCb)tmUiXr zKEDlg9lT^Q6TCjytAF9pl9{Xc!l?I$|JGE(txzcT8`#|rHB|4CTFWjvF6L;srNUqE zz(MEQK!2ZZkH6fZLaL38)dp$g2P|Y`ptjR-n4^9-I~bGx5EYflRYd5x{AQ&sbhMbZ$^ORRhFXP@v*t5Xo5Vp{lRzq};o7lhWKQaOA4i2>T&cOj*n#+kU1! zxH#XS^y*Z6D9dj-QAc9*Lj*Ql3;U7OPD(3Al`-jQBpLjPCldJ(g|e zu5Z^Q}VUFj2sMoZ96B^Sd9tON$+>=MTndl1nN~|(+Ba2)&4Gp5b=@VRlX|M4{Dud$>duMYL%EKRq1{pE znTPc~k~j4(!$Z01w#4<6(uTfTn2HeSN?O81MD$ z(i8_FU($CDhz&k?OsbeEhTCU=JsZvNkqB#WwNfSYdDSg#1?bT?@^pu)W! z8A;Cnm`L={J!QJqF*^Bpklg~xc)l9ZEvovyIhCfUot6&p;~xS#^8HWPkN;u;9&d&R zc9JfgpX&FZ3C|ab4+GGK!q7j9OYC)zokE+R>|qM+{>+p#(xbaTpm|qKBGPI7)Xy$s zvW#_(=t?FX9sJvcfV}(G(~OetZ2uCnqPKQ|H~gqf*l*K_S;- zR1%Mw7cSFgM9BKhC78zZ-6izXNtE3V!)%UuMJ58FoLy*;-uEO-na|jdO?Jpu%{mgQ)`|QwG}x^c`X%BG52|3I$${sIq>gOn1MB{ zUhYq99^yjMOh9$v7@))~gvOiRtV>%l;tF$#b;`3PJ>m_kGq-jL4hP`8xt{I~*7CS! zJvce5s^& z=@QLIZ{pSG$f~^^Nc0Swa%gn;<99a6!_&>$rBXrCmkdtHxhPNJUx?VjG4}GWg0~yv z{(ZoMs|SQnHRuZfUV_2*NIIvO-6RQaA93s%w{GTcBY5vI2`F&t2k&oC8Xx~fh3O6I zowE6k2=Okj-baZoseD|Qn2-N`8bILZ9c=caoE#k=r(sy!?> zViynsA~zq9J`Cs?8<)_2z3A!x?N%b({Wt+%GsI)21_7uOrz?A(&Iv9|91k;JFvslhZ}@4^aZ$T-{I znBz6GnD$%Q(&t{2pW*yoIjMiz%pQvlZ8zd&$8HY`DBRiQTCBc<6A3qv2D1)fp zYq{!Q_VwyTrWn8b*?-B`09HLt)g5Qx=iQh>mAzD5+LkzZ8Fpmro2@U^$}@>h2;ah7 zg}`*~(`l&ICn{pE)eQ$%Z*_?6d=4Ah$oByT zMuGl1X~!}=e{(55dIf(fidk&Q#|~cvQ_GHNER>ha?Bf}B);aTotA_^7e?of(dWIB- zsObiEB}ZC}XcwP!T^LorhdxL8fP-b(N9k`Sg?O_Q)qAJL1#-(;slC`%Aety6^|8z} zJgSQ`gbuQ>dk(S{ZOtJUMpR(hU(1Kpi}bQ-F#^qA4$b$I^CSJ0qDy9vgUPoIP^eh+ zx|E8^4B;iomtW$uMet}%%4TrzCo6k4&J|1J3H0`-MlaqvCQZHwEex9t2sC|HJ}U@G zvF^GR@+Q?)GIvS#reZ`4ozMK|^FAf1+#N9|q>+lXsb#9asv*Cbh|OWF3RV2xPyNhS z1wg*>`C{$Ig5iO8A*s3OQ9y^wJv{-G>Hw44-K;xsN97;q1ZLBub(aRCZVpLIm{U@D zQK8tzM4wn|PcAMahxhK?sPzzF;OS-vTX7Cv>^e1gpT4g1z>oFDc|_VA4}BP1TJ89> zG43Q51wD>oU2BN-H_`ZD)v*>A@+neRvp6o(8^X>~1!!!Zlt7g~#p(qwg{n-1O-vt5 z=3mNYdK~?bXc?w7Su5qgx$+u6_R^>=V623RUHyj8zKADy9TZqn{dP0eSg3Q$eZDo0=9 zevBViF!)?IJ^y;&-v+UHd4WRhU)Ppw4LGWgs!dOz*V_zXV-)e4B2+m_Ehp#wplk4- z?4b!SZ>fzxpKkJLna+F6O>Jdq3BDhAsMnZ`>Mlk&SobBe^LU=0q$g;DMb|1ZjhWiu zBvI|}1O44&(JEt}lPmlAAB%T-E^u6p$&7@U?NoPVNxkkDk(^vxxOxhiltt8Dc~Yi_ z`F-~@CUzIC9`p)@fZabK)8YjQ)3ttn3(aP%JyWsde~)((N3CnZQWANjA%2H?$~W~P zy|u0-bVKPiYxjM<4hl>K49&5j zY(hwuK^)u2paG8opOJyEja^6buqaX?OO%TtR`mVfhl~*HoG!hKM|(Z}U6@(duz6iV zWL8=7!NKsWudOca{Pt{6*^ZG8S?u~qlVbP-+6%Et=OJysc2<+E1cR^IFF<9L@}YMR zUxrfYtA2X)>}k&4QGt5?^78Uzsqgk+`G)`c6BJuvri@?hbiX4-7feG@1H(R}33fhS za*-_V35?WTwE zO*J88-|k2-f!L^Ugp29fNrxtQ0P?e*4j3(bL3|#L%738v*onBEu@~m!>#H7IVl1o* znJm7JKK9cs&iX~m{iUSF@bjv{DlBf(<1xlMDv*@is0Ir_A3JksNYT_;ml;;KsR z_?OlE#=UBpd_amdO&X9xiev{0C+^Z)E%YVJ_egq!_m};!p#*0xJ-x&S9;zCkvM=G* zgWU0^DfuHhd2bAYXUs%8!J5BX7Qt&DP1)H<1CUIOHH*bT&*WX1Q+f`!o{Pq5X#YFz)}01k3V=wD%XrKa=E)zVJVZ`=M&YT@sIGwe|C zqu5vB_=c(p*a)d|Ru~TIMS{Y?%!z3wMqkZpm*2$UJA#U+);Vt83*o=THGEH+PL#A!I4*GCesq6!wW*?!zo zA&zGN`vE+`#eARJICY#7x4i5JP&egxs6MS3wX&I=l8JXM!hRTG)#!Sa&k5)rm?BzI zb%JT^@;d*-!?Uf?nsU;XzI$C}{#SSeSg7o{C4!C$hKaggm@}%rvr&dTJNn^Wo6iYC z+}Mj(|*kHN5-Yg8C8N$ig2tzhL-`i`$2&rYEwm zUVA)pY6^{O+@;2_r=B=IZA9$##zwSadiL81PqegqUV}?bt9JACm#4~3fq}_qER%Bu zp%Sm8z6oX|P=G6a1_^nqYB8n&G$2Amj31geek%hO;B9l7#FhG(=jij|vz54Ke&)YW zfLnL?Y7F#%AaGNb=&@mqq-22vLWHDXi*Sl$a0DWyEl&>?^Z4c-Sm%9HcGgilc z8qA_){g+E7(ERSwh>6o|qkBmqnB?qPPRQq8C z>!?gCkq)H1PsIM82y(bLoMQc1bb_x&#+xr+ChOo^ubWnRmipuBN^%@~y|3C;yWIEt z_MFz1-%lfvTSxnQ?_iwv-N%;dT5s$Y)>%(sL;5D`|J|S0nbHNlaVgBa55l)pzL-X> zQIuJUrQ-UL&1$DR%ayM0n0mm8|6ARCk|S{@dIBThG@`ldrM*-6A<0WK&2PZou|y!b zQlPxbIT8@9GN;%l|9Gb&hvPh}I*=DvqO7+Q0HFNB7WncMQA*#n^8KyXgFkS6 z)>R4Y#Wa0h;>E?JS8}WV4S@gZ<9yG|F6KNtrX>x&J9c4NoKz@3(Ct`{jh$Sz8}YOH z{r7j*L1Thf)7W@>KHpJZwLASD|%76l7P*xZ)DD5HuSqA+4A92n9cN9v`1#nJP z8B;FVf;XvU{HdGVu({caZD~uV4wQeP-jSJyeXE(vU%B&xQ0BwKLooF1xW;4J1MDbQT(&qGfqP|)6>S+s^ z>=VKg5-h!y=tT~8Py0!zLrh1$!&c_}2#JpDkXi!pwtV<`zbC@hdHH;2n>vuwz5Rgo zfW@{gZ0#ye`FNU#Fm$GmL1jZZc25>KGvZKrkK8Sm;E-H0bu5#*zQEaQV>JG3$AO`X zqce)>2^UvaPhU)W7yTEUH#li_j5JCRI?_3`iwDn$83-0klN7Px2I$Ahp)|-U#z@J1Up?C#qns;0^y%i;4 z+s%F(Nfpo;i{l;P41Gk!(@Eu~n3%7bIfrKq%nUq(?ME$Qc!v_+<4HP!5B}j;&(*he zi2xftym>{7+#AFkdOIlZ8oJ+z-E)%5x-lHB(j1G^pYv6vbrL5ywanDNziAO?!)C1f z$X`3V$NU9`$VF5`W@omlzn6&^K29u{+Q)Jsm{&KGv%8aAuIe zlv+T?vvv2(u`$J4M;s=JnW)6=hNaUmN#h|^7ZJgs_fH0uJ``5-@^%gOb^UItVI=13 zMa8$ZW$eW`Tr4#0D5Z?`;#-oAUS~#9_WHqGSM^GbHduZS%i(kDfZak_rtcoS2bHmW z=UrMqN)?A{VyZb3wXt2?Pgak1@3~j0r#C%=^5-isTCgoCeG!$F9X!$k26EQ7qk=dU z4}EYXoB0>zMiBP0gQ&GNlI;a$vvES}F)?xHtc0njfuRb2A}7126l}fZ zo_|`+4~f+z^Tw9v5bvJGNR9yFn9pSl8@^l`J!PD*l}?p7Pv7kncZ=zhrU)vjBOTXr zUR7yh&l3B(?H)X;cVQF8{=#!^R=T5!HT8__n%?L_m}%Gxc_%@`^?n6;X0s-=&cx9^ zo`@vjkkEouP_=-_nqg&(GL_`uhK3D;1KXmpvM9Dp$$Y@~@>96}FtAq%87rBwRLpww zrc7(=-I%$%>ZxYs3 z{r#a-X-R-U1y--%MQ2oE@w6=0GuSi2pSB_gQ*w)6C=^w<9)fS}q!N*N8SheOW|*=? zVRg3Ypw@ggu6=FxB=oR0K8FYZ>_EJ2vtp}I_VCnMy3~%34~=XbJ7V#(E@669+&QOb zUk9(Q+M>&V8@scc4=24EiTkr43b2B9>4S+y@N+BO+QjEO51$AN3q?%uLbQ%-#ZQw= zxOu+(p^~m-wQ_EtV~vRnUN4G1y#3zKiWMw~jY>57r-l&xf%m?F2F_!%0u^PcME18r6O#hg~Xu zOJ9k+LFLXUl?r{)cM%~;1DA6Q<=_J(F(|MR*eC_kk9##$sbkZv1UnqA^o&AuAvrG` z$N67@<2R3rCN(ysS4H8=3*6AM;*_f(h>C)3k2OV?D4#x?eCW1OUl%8=EE$EpIX}g@F&g#GjE|-|cNGsEx=Rh~Fu`euX&trlS!6Rp zZ^F_VWuq1|wo~T0QJX8 z`z?AaSL-FBwyL<(Ih%7jOqx6p@9LX97dh3Zb{EC`Y~QbyHrIlm&;$8KlSp^~UmgM?IE(M|-`W04qHFphaGwFT)#69Jme`h@koirg^>)V_?HyAWt-~^3M~) znZD&Mc1CPST)|2eU^#2Uk+*#Nxs%~vYnZya_8FY66N~_YcAG7*ci}2i`2XdE{&&Uz zBnZx&5)$6A40VbMJCTv9N|Ld0iHVMt_jy2poKI8LfHIq@T3b3yjr^-QCV8^s<9+)) zP@5xw)9b@Y$f9{qH?ieOr|Q-){zt+l zpXR2Oi9pZQG&SXb`rHP>a_g74kF)}(u0T-{v%m$sb3nSh_3!BN?KHEW zxw;Nw%QnZW4hvt5bAjYcSjOh_GyBnp7#2D@h|}QZaZs=#P|tD$SY>;8&mRx)6T)_L z8-X*^g;ye1JV^bwLjr*AzXRNH+@MzIJ9Me);qN*}=UxtSkS47%DaImurOyQDZJH~$ zWZuhl0NLNkI8MDeu1*A6FO!wouRlNc+8w|3k#znp;0N$mK<(oed#r<6%^6d9KyouQ zOtXG_c`m4mr79;H&@;v>g@U8KPq^*Z1pz#}(og(7pZVf(}9&hv+N19NkZ?*gU`9bh&+!w$ty;>w(cN`S2% zx}5=N1HtWqwE+!uR37#4y)_Lm{c$7z8~=_DcA!7`xdDyGC(4JOJ|t4}1&SSdWKRC- zJZYi#45eaxYWHjY@89k3-}zdan@4H%bambLELGn0wE}wZ^=l{?lk%5Bpw$D|H5n1Q zUFvIO!lf=5n*;RBm2l(}-)UY{sp?+Z>i>ElRLEUaL%&vMRL2ig{pXpXO?$quS=8Cn z^43*agzii?Y(z@zY|SwXY~<_rr(9QSwtvHpCB1&VYp|rm;W}ubeOwK&d$s|J1Guu_ zgs)zk`G}Qut0(9FM{82*>S)-~>M+^bmsTFXcqYBpFOv?n24TQQq`fLuX#3e0;8dBh z0pbf&C7hf4#48J_+Rae!xRe(Gl^${aI1rRplPyyTNR&|Z?hUQKcCb?c-SI@zX{p~ zR$U;RDTk?+bF9dWm`w`7!Nz^yC{X>?G#Wgb{3DR4kp4!6a?9)RV|VV=wp>jjj_<_t zMVOL^^W#pl6Zn}3bhKV)<@f%|fM4rsss~SiX2uiI!@gBP9Uo(Hho2pez$Y1J;lw3(LZLKA3zfAcB5nMm7-FoeO(7N z0qTbq8)VTJ{d=Od_M0->u5SAoUVEooQvMj`JNBvqW^5Gv=epMdd9F4oQ}1_w81z)w zaU3m-?S_9yZtqD6Qmu9v%gK6r8Bi=9Q03q_1*yvjuv*B;*M1u-@C^?=hME9{x?NY4 zw|MHGx%bRo&b5yCQkr4&wOV&mqQczQ5f3fv-X42j2W0@8BtX=Q8yg!|jjL6~^#&R> z?_#aui!9a7e#6vKM7X;I>X&AX!H0?NjaI4&TPY9tQp$}h4aPF`FC{hBB~Z?t^|9rx zdl9A$4wH53LV4dZPS;ji$OCPVe>gL$Cjl_;;t6!J6Jd^w9JGE7m|M>y&;e}=@}2kY zssFlkeFmG$CUNBKSi9(wBX(6ssyY%2TILnq%!)I2!eL@y<+`4$&&uR|IR+H0v6%_{r>_sic#XH z?`%@1GtNfh3FSL{8?*u!FLRa)E-$!+g;gy7A|T1c2ZM}`o>@M@%SR+ak-+RJ}N#Uq6C5t z81h1uqj#YSX5Ki8gS;kY(SdJCYTKz|BelC^%>Kw+84zM5(+eMMUwiM`Dtht>u-yy@ zUC$FegFjijbsMabn$EiFu+wHb%x{fpntbYrWFYusZgoe-P0ASlO}?*X1pyk&dTZxo zx2dIRT-6FprhAIv;!CW-aBtv_qAz_a_i~jeVKXU%?!y=(HFN;VF&G$A$7?h452zSb zbo2Kulr;e7)<;n{BBRh6aA>T4dd~VQ&^t`t&FA?^+G4K2-*C^{F5cgx>AA>;r*MKO zMuqv)tyV!-f{YZ69ecLJghb>*^q)l4;PU3Jr zibER0+WmL5NVkEfVHeNPK_AI(16y^jF)0;&oP$8=>5f`G1iWY6_xXQ$#zsGW%tcwR z#xUe_${KD|vlAU|D@Jy3s&lL?Evcx8W860%8g>sHQuWS!F-;of2ET{Ibn$8QLmM4} zs}ZFM9&hs;r6EUiE$&%0Y~HQ>B+wrxzl3E*mS~#+X1}*K)aXHR9drn~3RuV>-M|+2 z>-7sEbBo zL056P{Lx!6)f)YFUE*h*27M-LG2C01H7Dpj_PvpSfc%2gu&^oI ziatipY<#@F&xr;o7D9EW)TF)(kl->m7}xd4XkNnkMxK|fKt(?pTyE8PE_s7d@vbCp z?CG~|@fJ~I)oSufCJl(3g*%kdds4-r$rEKIpA$YGD&3am#R0~#Pzg!k3Sj-W>3I7{ zq>=E_%-c}~iE(@%-cJX-EdrsG+C69F-n(v8No+pKN z9k6qkn`cJs!2ZOxduUvmOax#p)EUYeOXT?TTf&=ln77p=!j z%kD1i!(oNMRs~ZbsJ znsn(9s-hxD5ilUqRCd4u;+mibABf5K0JTKf!-_*Iw(KvBp08d|Q0c!GJI` z^UOK#=emCPbuVoYWp&5mf90(!=&G*3_8i%6;b!IB8$ad6iFXAp+(JtFdNUchyQMu8 zLhk=k;?l<(3|^cxLAV{5Ce$x?e0G*olSD4amD^}C*cX?lU!)2POh8{%xSQ$GuPimQ zJ^ZfD{fmm1?JdRYGg;q^-Oq4}4P67&m5zgH)x&bI4S6Tw7jHSjwjWd2FpqZiG`Qs6hC|uXP<3!8iHj4m1D#Oncuu8Aomw$+TZ#v zKZB*lTLJ>rD2EZGS`mET(^?axKC{+$Bz{<`@zXD0AprRdmQt4j)RLj)jCB#w@Peq-*6 zo>6I3)e-OeLiXf?b@Eh1!N@y>nNh zj;7Iy3X~^U4>FZ>w0WG0x0m;Jue84w zOKh#{J5R~^aKdu}O;b0pR6s@f@Z-lzWsO^Tpir-~U(U_Xx1gfuRP>S>r@9+5Sf9o3`#3QMx zEJ>YQGkIV)92NoU*fj`|7_fS|^A2*WIZ4 zj<4PJcd#7plTT^Jm)2ovi7UbVb+99BHXb-4^55s=cn{n`L-#mTcF2a2k#28sKj6iQ z6BY9QJ0(|IwLb@6S6E*9MD!wUas5?m|Dwu~tuJKP-GR6(m2eDSekFnizfmbJ!I?oU zt_liWab~EryHxD3^OlOUKk#5-qj_6NRwCa~V)`n@(_4L(Lv!hDW-;m40{V9cE9za= zp9!-KwLjo4cQ;3L_(qH+oLGgs44Q81`MK|n>)`^_l3g%tepVq{1!{U+bAyOR@b+(N zk!zRerrd6~3zEM{Wl8fpPQ}TPT7nhVgp6a<%(uP0`WJ^mMFwlW$YNvTH;vM~yD2P_ zZ)kaf4z~O6%Eodkz28ZV$b36fAic!l5l)xWckgSS>zkx3RfeG$aMljDzY_am_Uh%; z8WFKb4LZJc;}yltRu(%Y%7`<`l6^}L&6hUH_P@aqrs%bXgf~>Q=){OvZuZB$h1C-! zy%Tdci>BbR8geVg!ACy$6*w~)cXaJ;-q%Au?U94KJP!29*RD@s;0aoZjD%x8e~epo zo#vZA6VTstwxB(ayQjmmRm8e3gB{_H*kivQROKe>ntj71@OKIO-Bx`QH}`#h52K1A zkH-OXV=Kd>ocaubw{AVX?iF;og>xp4V2JWD4RPIh`5k^1z`L5zbj{4 zvAiDYmh7hBY6Q;mn6XG?Ix!%DPf#G`q@?Q#Xj-nZ{8U{YS;gOmMs${z2R_P3l~EQ2 z?G$P{a*09W?OQ#*+zcA+yWK3A)g0m$b%*wzH`>vK%ClXBs;!*xx@2L?i_$2pRJMYb z65=+uR05ob5L;braI=4!;;teMiTwk6yjx!euBJRTEU#el|fu0d0F*WipVhs;;v?`zT5d(FTH7@pi#N}k!^RU ztwNF71bvJ{>|xKa28&soCH~FwUytpy>V?g!70su`EXU&-ZeB;hM{=8?OR-pe8B zOHn2tC6Z+& zbcZb(J&7oKd0!r0Ys-!C^@Ta|+A;{(vJpmhhtR&cnpe zg;8@{$gl0yg1zWptck`VxXDE-I#4#Cm&P~*mA=*a-UrPEz}cR1u_&V+tExV;XE`GG2@I9jti8&)1N3lD2`OvMeIe-VU)pX2N(Sfv(?YP zc@qVE-)#!~G~T^7XlX&gARfA%m#i-t!!Gx;gH}5O-(&-}Y&qY*+n6ElrgrCk3{M$` zgjJH;`6`LK{^M9gn)7T6hin!E;xqn>ho_$_noAmsUXAJ^0dt=$)jj=5IVM{wpBm4l zaq?uCtT2es=(Fpzvj&15kiy8FAS{oEJ64BM$7D(;D>iCC4u?f(;64XY#&sb*UCtuj z%M$Xz6Wne~#T>mOdf;3Ax7=g2Wm6IW^zPHUlxH=85N$9sRuQo}-ef=)x$Ji>EcRJ3 zI?daI3;rMh#fy8t)U^82wVuO4mJD48IU58b(q9UjOw_Bx*iEGeYL`rS8v`{g>fO8G z#MJ=(yh%RJeaY}likiQYZylRbPsdOxZage|VfCGd7Zwv_xoc6JT=00OJN+DxF?fKo zRX?MluxJ22R9zC!{;?;*)yJp3?Sm|Q-b!)smWP*6XA+B|qKC}Q?8NL+kI27j&J@XCm=8IahSON_Krp?tI8g{Xo`j!!Z3#;o)pD~A)%4Ke1Z zACLHxB%O!d5>kcUb*B|mfQQhd@{tM2RITY&+DuRM@;UGX2Hn{Zze`qemO%&Wc@7GH z)p`W#6DCG#o@X=io0yFHTIE3?ZW;__jiHHfSJRh1%YXGET6@0i@9(jK(ufV5pOw1} z0x>ApSJ-pp$86DgV}QB*k6ir!pIode+L-!JE#QBoAhm?E-#v@H2~;3aA84eHpE-4LBXvKR>w(#h<16(C`t+72nP=r6HEUrJg({7A!x*5usZFMQqZ>s5($zw)r2T($eO?mGk zCVX$jb-qE%0%;9&j6vx2^u4t|m1Uir=rFHtoL4KcS^p(!dx<-{n~w`USgU>6|2hn>L{P@{v zfK3Uk4~jI(#KtDA1{q|&;lI{n<(BX!6njvK1zbw8Iy!ss?r%j@pz7#o=~O-F!qJf@ z)-OLG8&xKb1og{^3D^oTxZmE8AgMpQ;n_crTs`Q#!E~QF`tkl$bY4t)BcTJUK}Tm* zEzp;LN{vs+s?OX*W6|>lMvpcp=EtYxrN-sNlHIe;JzgIa6R!$#(d>$P!}U(KS?o*8 zLGm}qlCVxMF<~ySlyH5@D@Wos#(qlu*yjqmigd7ap(cF;o6^yPw|dWWj006rE@aOQ zES}Z+{U|pO_4#|yD5KYp^4MN-uwp1Ks@X4^4bc21s;X>u>B<^olO811c7*44&&NCa zl@`c{j-dYTE{ zyvqsu(y^AkJ6)b}#^D*1J<%zpGe!o7hDjJr#R9@=Ma9AC&e;sFT;SLXJ|`>1*`H4T zD#>82dy4ozwEXI_UfgJT8 z6dAIcA_0~_Jh2}3?PD^EjPDb~d>dEv-3dJ)S>SR8faqAn)@gW8!=is^(;+K|u;;yy z616*ts!D9>EeiRLt>0rMu;-(ry~1Lwg7zc1xoAqv5ceVX>49qE|BjA!gHoz~xn@Ei zuT+~-Li79g7lU1Kz6-{>i47I;&j)vQojCtwDX9hCR>J2HsKI=U>Oqr?kiR!fbb z=Yp|cZP&v)I4z`19{#kmK<&*;*Xq(#BZK1SM#S_*Ri`}za~9i^+f|2?%diKkXnIb~ zdfUSoJ~N-%>S3=B4Pp#sL8o)FGaP@0BoZl_n8+P()?Z0h$uL-OZ{Z{7aihMnm=7Pq zqP8yP#3?CmkA0&bcQ(4>%7`sotWNiK^zKZ`2mXKJ>Z=x?I9cx+Q-hcXR^#;n5v8Sb z@7I!C<#?#ndGvf!-}!4M#W>~*W`>!v&TT}ko|ndhwfLZ#eAANmNI%?!g){;|w>v6d z_Pgv*rX7N5^U%}2-_bl!fog$zn%))PnXi=WVq|#d@PSUqiyYKm-G#(i$}m2_9jf)8Jo#U{x7s>f1Gr20~J4$WoNjCItFB2kQlFVdH z+JZfMQ^cX5dE}n8GJ(geru|Chq1t@L2jC-v)xR${(n`W2{}>* zDs()vzt6GyAy%Vh{Z`UEhKE}bR+)+|<1JdO@YNpmkME;jXPw```JYBF%iI@MC}qoK z*;=tI!|ED9{VjG*y>8a63|09%cmopJniAQgl?JLz^rqb~Q$xPAczj3Hw}4vaV^0j0 zCb=5@+_Bke2*E|QTHvt(aY7%NEFD*6wvNr9tp1dr-`3fwQF~z?u#YF6_aTlMxYq^mK0X&lf2*+P8sv z54Pt;!){ft8*W3d?gPHZ9z217YW=%cCAZX^$+6tn#Yp|M&zVmm)>5fC5kUT@Kg!9F zk3MH~h~Lx1m1euY4W+2z1G{5L*lddb@H@WDccs`kg8FVQq)GTlRBpP`?LRkR-~G%I z8c;fJ{Mbk=wfoPm-r>(FgJpN)tjL_nHJlS*E2!sL!cw!2k-da3x0RJGLzi{p6vchI z$Fv?iK+xq3*k!Ruw8{GTVhPSD8 zUvJBKhiv6Y;~bbUi=``36Qxd(OC2kbWCogiO69}j%G~Ym?dJLj9>Ek>XhAyMt6o3N zy~zb8i@Htk;#OW>_MKtU#%Y?xp<|hub+7}8fg2`{!~^Uww|(brWAqw7y?pNW-r(%()lhHy`~CJU4~`uR z&qfvZ%1Ogn<*f0!n?-g3qrXm=Nfjq%sXm$n%GNSnHPE$OM+>W0@>V8xka-;&2_mC# z>6M1PCB4L4#hxN};3z@D-ji>~=K)eYF7DK|Z6(~lK4>gT;W^)^Snt($t~~WhsWHex_PLWnzuUoyM)uX1egLY!zv$Vs{jBF@>PUus5Xtw<5B?$sGT zgLy8|vu5zobEw_u`DEaXGRs2sJ~jN}>XYP)r3Rl>;S1ov$VX;(gnh`3Ij6$&Pc6Wg z@}r;2XwTNd{d9jG9`zk8Bcm(ATA6DS)1uRN0iXNvif*Eg+6Nde*5a$Tf{fdw8&T%* z&f{^*F;$O|tu8Z}sMvxE?8L6#uTop1=^lJf^>|sOLO%D_vuDQ~fA8FhR%p|iFPX8H zNjh#ZU%!%Ftfr=z1CHl@;88(s!-qvZKEMJmW31E#obhsV2Dyz5ZXH8?aayM_kQmhJ zfx6>t!{`Xm3HM|Ro9*xN(q1nu-R7nfGkkf8wh7B0msPow5;jvs6`kd5>%0%UXPzWv z8btg^nRi^)pz;wltH0?6t#wrm{M`V&i2S>t8s_B9_rBkdut!C)PTH+xzG=o##e8r`mHaLj#SU3Ul`*X%m-xua~!a_>&8>8E^4Y>{$ zzCv3a5AIEht$zz}UqC{W>@mu~Q;4??l*^P0D-?V5;Ks8bcWYP-gd^AF0U``}|`37?1tb zQ1-F3Wx*wX6&%Bxn5W6E;IZiM$Q`~`YhBnYpCZcomXZSCo>@oPqe50u(T(GpY0I+dwxX5K{}*v`7!on@gmt zz04c!W<7u?jXc=emBV`h8dc7(?(%Fs z3a}CD2}om6z23D{IewtSXlTmA{o9kFLq*_2&Dqk}JuziUeowqUQ;qUN4x=5HuP7+!d9Id@mz%+tM(Tu3%hIGawSSOK zOwsL_Qa1@}vW|cBb137<3}&l^rZ?*fIS-YNb4#0COO-L91P>%I)C1)#mD&==rijbH z@b4WvGS*jaH*lhM_NH~v(2JhaTlsUSat~@XKHZ9nisc4?w_=mHEkRP6E2iqq;@G86 z?r|T(NhG2qumGgM`N6&pGGGnF{8h^j?sp#y#XkQ9{{26;l*zyQ?+~c#z@O3f^u%A5 zc8iOeU}H;u<9(S&P!RKoAJlL<52aG~_EW1|`^Af$BVIKqyrf%MogG%C2*Z&55*8{t zP80kn0syrR$^PB;ErpOU2jG?grNQ^{=ytgcmT0U#Ho!EBc)=A;tM zkp{U@bQ!7V!BY=Xh-}Dp>+i2_k%Kq#{B+#i6$>CuHlUd2<>kaw%lURDaR5h0<4h8! zs=)6DnJR}EO*`{ISmZL&D%W1ue9v|<(blt6uXgV**wb}dezIuAya`|ahoiJ=YP8j&Thrz_Qy0UBGlnGA%*Xqm&}gYeP^0FA$0_(el0 zHFU8=SzNqgBI9rs5aa(Ko|6H(oo2DiG_B$Tep1%q2OE0v%pR8J5S)Ip%TrqJV)tyk z%|YygJ7y|L3t+q=31^C1Z=*rCI7OHs`ATY9pGh-Fjgu?o8YxRZvYvu|dk~51zsfG0 zqHW?KUq=tehqzl|-N%Bz$x0c40}_<4C(Wo$+gi6fOai!q>|i-`ieBBlwEK2?xAZ8S zJy$#BqEe%2g9sm=4)EGMI6x}Pme*3suV0=+ey6B)Az`d);G+5u@WZ3>e8DakDe1rC z59e4}b(?8(aA9mDw{d2US`X3)<|TOZWGC3+wOpFz$}YSl%cR$Vh^4)*B zkZuzJtuXtpWgz0zwoNq`iO=odJyE%?=SWHrKN=| zxAo=#+&kE4msg=Uh_Q0ih1{j^MC7Ozk4QP3c%*=?90-|h*nGx#Qr;?oa0_~Q|G`js z&mT_a4Z$uJO8wZ>5-(7y+f0BKait;(N{axCXE4r)K&S~MKqE3j^D6aYuo6a^&OIB(x?OZ|tbJ_5cDE?vDl zfh6Ih8$2|Cvl zze5Jpz=H9A|13IDF!$Y$S+360?e0ESS#(;g{?d=lmwZYuDy_^(ty)R>HkPYx@oV&E zG#~tw5FCrJ9iVcMPftm)L*mmBRu{x~?%}9s%;w2iG@|I;@#y(&9g*!z^w4{d(zMxw zy3sRL8Q_NjauHV9-YD+X{^`VxdRYt)47Lx@X=g_*-B3u8k)Yo z{_LltOH4h5&Zf}*PfOnnCS6}McasP!muUWhsKS(;IOyeX1cJEQm`@SUbxpF+ulwW1 z?%7Shl_^kI8E11qdfB+wp+vG(VY*H*U(7;xBe+$qfn1Zx^-6gHpxY}dKGk{K+xyDcdJXE%F(F=q1q zKE^4{;8LdQo+-r|x%kS+{ygO!U+WltlD59flGgM(CfN1$CrJ#-w>hVkCyDp99hDMF@-XuU<;7Ym|QSM`!n6l4meA-8p zDW(nG*tq$0N&Voji=lL%$Kwck4iAwM`8eVjvjushIsKsZTZt)?qkiHHiICkMciw@o zWB1~DRit^N=8zC6s9{EPbOy6Jq_$#Oze595bXlMt>JPR_|A=D_ z;-vu^egPHx!Stc!QqRu?A7{nI&|r5P)H1t~FO*Pk7%QkSicnG%TQ^=3rTSL7?AL=-VBADcBjWhy!gEi3Tn$+#!JIaz7Ld+@?-)o5mpbvH5}xx< z+R6EzfuU&G&;OhqZiC0@TjiWIYftHh-WdO<7Eq3F-rjxn#4dfU1wOZM!>d?ebr7^j ztsU-d%b`N22j9@TB4EZvR+@nNY!q*U>wZb1V=GH-aIP}Sxsno-K1Z$qs=Q#SQc26DdHh!A|%nJl}NC(GX7VmK5O=jYsla0c$_4R9K5j&FZ9=r;2>yICK zF1)yeOp4BAeP3U{S6Vx>63uVT-pShXajo&ZiJq1l}D&VmA90zj!R(>)yJdI2WD`f~XL%0b0OflwZA3*!cbaFDrkfH8R zilk;w)6hu7zB9A1!@K1=g=bvW5YJW|G+bHu(t>|;<$r_n#O#J}be^Q`X4z^Me|g z`Scjmau@u9AiC{6z)*D?VW%q##S4lT0G-YiSx`|T#$P4SU9>xv0iqB`KYN%lp>>AN z>RZ_wXH=hs;^L0s^C&Pz)8LA@fU}z(tg*qW?nj>)>?p;56>xS{_cNo&rzPX+Y);V` zH8as}tp)bFzlSzkWc395Rs{hN55YimtrQ0OXAuEEcy)0A2c+{Ypy$Z zS&q*ie`TJIBf$Hbe0hZ3X7ROnnTnA}&g(@QG|Vt(BH?yD&*R>apOtCgX% zv{3HnkQwMUT$bspa|d|5o&1%6fvw%EEk)MyS6VMr7TvZq4hV>GlhanVI6Ivx3xoq0 z8+bNPA3p$@Y=v{|5AF7|$;JWLN|P;3&fIR{xY%#8alPTM#)Gj;mqvC>h=LxTGZ)6Z1dDOE*>=#({r7Jbi~Nkf}m)CsQc_!l6sFUjh% z4N8nMv;4^Dm+k*_;x)+1Y5iB2=%!YvTO#P*8zuBQXBiCI?h(2p+x)^y=?siE`GvNW zl0$PxbRWtlk6m+jr_}o*`n=A=LFziDME%?902+bc1t*XMoh6e_njr6&xe674CUy#Ms+ zvZ$!&-xzomJZ*#2(#LHkbK8~r6QU^bVc%}x+_f~RHnsmplc8Cr3B;dVuAXe$>^#4B z=7!aOa2O!^I-oFzgZ2T6CubAe_07$NXn-WoF6Ue0DbXj0dZ?*$3N-A2G7^-tj;!ZC zuw1@^Si#n6)Pf?Jpf1V=oPq6sI0F_)U-2Z6lAS@)l;r_!hv)rVNz@RxXMaj;pp#$d z1Ou$lX`9dFY##!E{LZdTarrW6EF{xy;N>sKJbLrb*qR#W*4v$+um|Zh-KC(%knY`a z8c@$BK7aW#y4H0hYIT^+IeSIcb+`CGO!ne4=Q37r;M2+d{(Z>|<(p+eVCqwz2TZv1 zpVr4~WGNq4sQSA35mHw--{vnLY7k3bPjJ>AO_KtEDgmdD z{*G*sCw}+Fg=7Pg{shD=@(*6`*rL2G1qN{Rb$Ttg+(t%?x+IN>0{=knkAG0W?J9Bz z{Rs$vexpQ1RUK7;;Io6y9-!niv$EoMox#Ys_yVxC0If7WYg0cZ9QxDmGbTaZH|%ev z%UUp=?RB1cbt?^U_E(N5c=?POG}vwlf+NU3f8>PxU7|Spk^lBL1QOppnW}Ewy9L?_ zfMs(7d<67wGBdYVCaqwpLgxjCgSK<915C_gA2%vRQT$3pBbI(XMg(624O=5$9d__L zlH{2M7lML0os;++juQ{k7E^A3HJ%G|H;U*wYWthuP9Bq%ZcGX<^Q2^3OZ zF*D|`(3d&^Q(~yuGDWMP!z_vL_fitE41b;HUbc&ws4X8Hp-z-8u@dg=WELH%uuf;c z%Rp-JDRS}47dBaQ>ohF!QS#YtnN%>yNcL`rqddr(>HF)OPc4H6*s>NV^&5@D&7bLsxvGj?Xtdc58ew|E9nl%};si0!=2K?CI_>EFud|^_ z7TMEt>sgV2XjW6pfPR}eD?N8#z?C~_?4+Yi6u0%bzY$?8R5VR=ybax%NC;f4LC!p3 zh{LVug$6y?l6cbg#N)w-bHqo66!=-@T}BaEFPG2i}@6+}2PggC7r)H6kgZ6mZhZ zw|L4~&Ye@0XytpfbSp&Nork`&oO@1v)`!ndOC8To_F~44HeeS3&;hPPRxk;5N}EG)9KOXb<(!JWghI; zXHJ@u2u;kEdQ{f~v(WoPQ^Gpxbiyh@bL~p{!7Yy9%<9vSyUJ7B^(V$28Tddi61KY8 z2o?n=xuW6*jX3)#BLiPf=G<;h@_ zclqWV5B(_JQ^TR}m;g#X`Qqb9b(;@-T%u4(F zspKyD|NH<&5ms~})kb3%C-Fmxas5ZP^LAkmUfJ2{15lg`;qyx>uUNf|?I4!z#;Ak)Q58-AfsHG}@P za>i2Z)C}3t1P-n`koSDdszzN{W#0wRLT<1ft(CI4;qKthT3A?D;bF1y^x!9ftmuODPp5(^YgS(U!kLIbvAl>1gd(LDhXA(J@b z%cevHnH;3W$%)$8fd#DyFz~eWRph8NF+;^?H~3etmBh}4FdB~w+>p|N+TbY(`YqF= zV2#nX*__E{XiC&SGBr#d{VH$PXc(xp^r2b2T5ScS;bF2IZ6inI+b3MD-mk@R{%x%GPvGDm3GevL zx#n4}Q0Nfl|DI&B?E}UQ!nxOXNz#s(Tsz}oy0gK7XE?Bv=(6}@5Cqu+vYaXE1LAC( z7N`Iq)lonK2VehplGwn=35Z`pVj|1`#_=<2fFuk5V_*){>SVTSYLc~u+#SNQnD>)V zrN(;ze>e$NEb_*awKmW?uRZDdGr~ZtA+A9%FVA~wGyPL83FqG zQR9SNr{{aTZeeh$+p>dFzulkiiT>j!PCSqBOUAgbq+V6?Ur!JPgUAkm`0lra-B_T} zd2ssVl<66v<5TKXv9|RGy&~x$w27@R<3lJu(4C^C4LCsS2or5Bw^uB-S~=fKP1m9i zH-I5B3^?8=7Yg=&m{o}KZG%SF5CyY1R__?H0j5i9xjVXN=FX82AHTV^7s_?R{|-gi z>myg~tLU$#zOwU5g|ER+%6LuqTRKe5TJ8WZk%jzy-HYJ3;F&h)b1cb~4a%x-z$>bP3k*bZ+`YRj>%X=J1PtcacP*^#D{BKP zKWL|yM!uSp^!)w-KsN$zSB0stBYunUxwaTg>x2|I5sXKFAN!NwIgD`aCIHjcl_7Yx z5#iswy$JJCYtdE+Hgv;;L}zSoeFe}?AgD+UqQ6j5S&CBJ5P*t;6_+0w*1O`aZ`0*(h+>fvvO>iH*3#3>wB4nBLbrp>HnVumytALi>HyLxtKIwUDTqxA7es}g^`Z3*QQ;DvXOjrtWaPILLltqu5h%Wv2jhv@=HpOhU{EawG}h>ZAY~ zKQI6udZqQWY>4*OSgh?WiksuN$+8m$Zl`i3lQ)Ky2&TI#u9rr3nCG1J1Dijk8`JY$ zT|yXGWr(!R?tUUHB2lzBPHEi%#m><88}$6cANSTLY~dQtfyj*gt@>m(X}4^Mf5X3Q zfdDXL0K`J{)}*GUZhuciyHBor-3&Wa;i@l(6YzH{9%9Zw79gNsS$<^Y-M(X9rG7cl z{0lLyKCL)Ss1d?tphwOu(RDM(=#tux0s|32;=etudMjRG2P8ij;s)RB$xr3V@!zNP zX@7M#e2JNG{~UE6=mKU37eZb~SDUyQ>r21IX`3yD(9)}NS4c}K5_~#or9;};XDa-* zckMFK$#oG}kVUO7aH+$FhnwACXm^<`cb z+qL@&`=#>cXYn2S=WhE)+NDGRPr*WCBssmYT??nQQ#2_FVg~@4p(KKv5yuc)A%m~ zA%DdBg+k`;4Lp8(B}(a>Umuui*IFbueq*n0no95W1`6&@DlkQkon@*FR#EF?1L@^p zkYa-U>iNU4=d%0xGflue5*A!Od7jO7pPX!R`eRGebXpn~fY`2P9lznD2Br;!4)f{K zJFQV2LVccnGb+&{sDl@@2vIGZA)XRH$af$;<@FPl!BRpN zPJ)Y92bPo(@@r(Y>lxu)FjCJeLC$hG_wJJ(g+`7PsCd@JysHWdl>&0TR#cmari0<_ zhCuuclO4jOmD`=}hutQTC&7Kd}Z0~kI&|GPf*PiPV=>YiKNq3?Se4I+Oh^kL<6fBY#|wqCo4 z-82OY+7%EL&qVn-9hhrF7UCyWHR5G>WDM#`NLf~lwSfj3x%mFaLS>Qoy{EXF>9#@4 z?V{D|6gbt8{h0pCGt#Dt{Z3nA$AEYPZa4n#Eb>k(-WYh|FDI#3>^w=)N+Qhn!Yu@+ zA0)O9r8U(mVX`3>8jZ zoVTTKNUu^BO$OG`n^a>BpEq)z5O2~oPqVszABJilzQu2${VJT&pIFykDre;I+WB3 z=?>iuC_DCN^4z!RxwC}rgN40`aT1+_QkDGl?7(H@$rrOUC=`lN{KMeVt@V^VF1?|I z)me>Xz`eLJnllu}TZxqfK?~!`Lgw~}pT2)r9aUfh*4t8;yq*O0d?gavkL<5G3M3w~>xy2GFIrv_M ztmx>dQ|pK31?^WW1ru={Q4LlC&d*?%E0<-y>Ap(xtSS^I$~q6bURg2Rt~Z}9DCh&D zN&>#}YM$Tkstm1G$Une5Z4kV)H&@PWGldvvQu`X#iQ4Uu@boelZT#KhUMEZ}L6})R z(15Jn$h}L$3=&mhWxQDCU!Zy?^A}PNu)F}lsGQFlzGvm6S8Y8WN9D^E|KiuLUkRk< zLSvqQ2l3&mMWby>X{bVL4rhkU6ke5We!uDh-1Y!sZyDDdvVr(=) zogxegpvnI_poKTl(in)fo=W%1qW@O=ds{>%egL}UM()xB1Ndmb<_fGt1#mq7VJo4XPpyGM{Cy*FQ5)cZ zXOB)Jrw)D=Bp$+2LB8Vu*C=wBn&yqQF{^`%N@NE8sGg!~L!$$?SIauIn4@9N4J2K+ zh0`?4J;c%EiMN>sE@j9%K`0W8K@|jbDR>X>$<3T+K*Vs$h56K>5`FM8H)F_P-X$_S z8(L)K4RpwI&Qu_5Tdkr6g|5EAefj3jA>;dOIcwh0fu%bWgE$OplN8L>!o1n)50&d2 zM#P_c6UQNA4wP?L%KBQDLkID_*Q)fV` z*1c8ih||*Z_SB)WB%IxA7MIb_+Od}-gQx2Z;Bo^=LjDN5A~2MuE^vA^1C&v9wwQ?; zat?*o``w#m4g2J`?-T_#Gd9PiTzxz9#U?|mHCv5rP{-SH4tLjnsOfQY{hpl_-kUWm zd4|B)HGh4!hZ5^W*YCV^x%zXXF;k?^gRs;4eNuien_CBEz50sH`zlO|LD4n5iv4cF zXSaGmD5;UtaFul7hHXu8ygk+Qqs-7{E;wos`f?lg zT>i0thP*fT&zr9eOE%qNw(e1xodRPFEk&NEhDmb1)cu$c;{@?C06kA z5Z{tBZI+KZYopVjY=fj7r1zpDvI3mt7EiUME_9;2$%FRm`Ho&(TwJZ23?(sI*;=lUl>jJd)W1J3jEM+E94ITl%5^tXqZ(lm`n_7q=hpaH~r?no8dHhn&B#5GzG zA52O8>7I*-bE4r;)B4h{o&7+8vVL_BQRmndCzUQ6QRiAcJlNSNU& zRGcO^h8#5sfXNdA?yhg9e}R0BKiPrhg|Z5NK}o-9cT4rd>6=>@1nK9WepmQL8gtbJ zaXm7gC2kjg%4BoPTb&EbIhhJeu|89p)Z*(pb5&`1#NO}LLku{vjmE7HFR-kE1Ju!3wpZGI${szu&bgZqPnGdBO0_DTQ}mh|hF*R0UQgu#9h zc7)oGk3(yq6DXS9-*gT@>JfD9No^$$VAE+&o#~@|_YL|I!{iC8{=5gSoXJem`wXAa z@)1Fm3&E}5>1R;|s|zOYUFpYrT2+!Z)!LxUbFV1E8!YQapoD}7mx!B!&c8s@$L5zB zW3{j39vU9vcyea2`sAQaw)6I)Ef@nr`$0ra2mLk^#0>+zHrcWFSu-FbZGb!jwRR^+ z7w%d@xAOqk+Pdj};PH4P*U0vh4QBC7fm1@=$}X&PHF&CAP>gy5bjj!sVT&Li6v4;} zX2J4(y3;4nYP+|xt<7T9JaC+#?#8-DlQlmF0C>B%clrr8g# z7!yJ4aX1h;z?>6^!y;6xCpR6Z$qw@tAdSU#!!U2~OTc*|9RE=WCwosz8Sg+c(`Pmo zTFB;?Bw@K32auTjPc;J?At{}rU;eGwh`O~eqF`&`^qV(Cq!KE-&~avj+Q{t*9;8I+ z0fZT3l3?nD$6TanIlAgkLefCd2PT#bw=Vu}z?sWQuk>61mf#|@D1XYq!@ZiHAt6S# z6I76VGCf3dhO1}D12-DIXDogVGT;b!@gxy55gNUfT8lsGRLxDKVF+Sk;D^V4QhF;; zfme1hi>qyDN4?c0?0i((#xV$ttWn3PWyw8{MYtM`}l_7_3^K= zp2W+L(j#5De|2GDcqBxiq0F!qh(+mF=KTf>X`enl00GP3T0TBdyc>9Wh&Rpz0v~);du!td`bMC<+oX0QFg{Bk!R6U0B7K+tSQA$?ojMk{O<;g3%XMbK2S~KVCP?2ztnC+<-?p@nwKL4Go3~1VE$|26&Re57N`R}OAwmhiHsX{N z7M}tw4#XCN9AfTt)%K1KI0YV@G*4wzjtIt{@Rxw82s^z#ptY zu)0uQd0U_mf`Bz>27;`uNh`KWjVsD3Ct8+C^EK3}U_n;;wKiHs!*s9E&7vKA>4fK3R&;tc%+sJGU)gDU+ukjP*ZET_8mk)LBK*szycy&Kzg&$ zr6|3l^sXR;8c;+O1f)v`={3|)0t7`sy7XQIBE8oHLcS-s_j}Iy&YAh%Z|42;b!N|Y z99BqH)_T_cT=#YVE_X{*N|6FZEn#|uERTdA=WtR~MG540FYgl+)io)=g+86uu=-8+ z%6&=q6YHQH_{k)3XDPVQrYrw3U^ShJ(=BB&7u;{9Hv2a?1{jJEjPZ(sphqbUQf1CT ziU|_{XIi6NbsS~Zt6^p+cW{G1aa-}N=!Mo0l-2cnAdDN_lW*J%qpsOBfnf5Y0MfVw%D5z6RN7+DqGs3%62Sb@SiVSDzCAv+!_+1vNfCNzZomdU z-cxEF9MYjbeS=Mr2x7+uZYqHC1SU>q0J-ZNeI3&LwfOF7!mdV++VJDYyP#L7VPm66 zBN`n`?6it>GkiVYvS#kg1U~%7)8D!N(yL}=9ASk8`x23l4Lj3@b+bo%?KN!XIVm#S zRgSXq0K-7AMi44eu9lfM#PB%CHJbW3aFw;4P6>sjbn-!cL##r z-Jy4;9$Q7_*8SFie~2Dme)y{IBQ-cPglS%f8qWo0-2~O!&dx2Fko($7MT3f{|CFMu z#>RjIFoT*5M<|(C4qW8{)Ba#GIGBpH0)X^Ash(#? zIQ;751s|=!(bg@YC|{}v=ZS8uQ|@t4fj55Ii88{c-^kSZ`G?z%7~ zbR&KOFSU>&dC=TC3B>J+zv8wXBRMa@E#rB*J(zkNT+mXeOV!=bakUSA8(4l8Eth`V zhBN{@&8hnBT=oJjQGeHN;1_}!v#N+SzxYlBY_C%O0>D-K`VcFD+^u06DXoh!U~9K| z?KIDQMzySLe<=_I@}NCGnJ@r{QO{q8(e2g{mEDI%M%MrDe;&s|&`999!D$?%D>^c>tCqw#mQ=o$;%9_JO+*MR(L zAi#738+;74^%7#A7|JqT1CAN*cDnSZG5LazEiLnJNe=omX)9`xGmX?s9)-nI#m2^t zicr>A%&6?706zZH;*PGaj$oz z!?MK~6WVal&?@3M#mK<0^@phkF69YK^d-g=kTD{llh=K+L>uOVfsWfX=#9|4qhfm{ zr^nshuCG<*ip1{w?wD%(DAp)ap77PY_E}M`zHnhnY2hF?@W+p{Zt|nqrV(+_MeC}r z$QOlC>UggezVR5;Sj8aUepMLT)bl2ij)%k5BPAso=hB!NfTe8mEz8!82L?p`aljze zn0U|OSQbTsN~l{dtDMFhoL_Yh#QrG|{`R0WAc#5IuH@NJ3w%4P!~_G^?A269KFGNE zV!l&!0p+{&`3;D$ep>wbz8Jnse3|EGn|PXzqoJdxf2G<`p)STSdAj72kWcpai;K|KqPj|R-RLv?$NHQ&ga3^?(j-)C%a`1M7T1IT&i?pEW#L` zA=>BA$`-0((FLx6OjBc2%(HUpq?mqMUN803Y(O#GF3Ftk>vKW6y^h=MLXbEeap}OP zW)7)+DNA0FRlJ%Tc+j}Ruz;Ns!M!QlCd-)g2#GpZ!w{fBxCquHZ$Lb50ioLYg&?U2 zOzC{#g+fkpUT5XH<;S}*-{FG;zcQR>LNyDej|><)^B4V9nAF#YIb<3NBs^K**BI4O zys8AuM(DkHf&y}Q-;X4lAJ6x@qL@q+*eMcc>f> zw;#1${^^W6B5n)4lB4UF!nGo`xe_0S6!R##A6X>Dh5U?^e^PlXP-ooLpFMB6fAh8Q z;nz1f!}E>O-YMMZ7smaujmLXK#(h~Q#NAcK)~;hB(mSfll5l0~*9<;5>^%(SqVDqp zjq*&Y4jE&%75ZszH$i9O%_l9+xyN=)C`4HHv`q8iWSl5BM%CH#iVS4)CBPOp7U&(b zZCS++)ctHE=Ps}sN>m?!X={@46<4krIIqhV{!ZjQ`Ax(nW>gvbv@g2R){wcYP5iF= z>Py>RA*&?@&9yTnk6W99LM*;1<_w3j?tsgDEJhvuawqZ|XTEI$1M-3a#j}qwL7(DN z*?%b07#Q@Srt}RA_`Ff*oT!1kz(8R&ZFb|B`%FyUVXyCpnMx+_1Qf8ZxPEdA9JPra zKAD8J?iSDsZwunixCI_-$(gm$GOkU zw(y|g^>b7E4^o>ueFrL@i+} ziWpr=)~uUc3ZcsuP8Eq_W3QxMuF%IZ3cjuIiDBd6)XHxyTamPy@BwzJPnsW0y*$&) z;`;~pX~bnzOuVPQJ?ci0F%M|>XT=!YVJF(WBtIJijr{&K!|UCLGZ0pSBVKe%y+*@w zt**M|S>(p>C|pnOUe~chA4BykG8_A3?Res4`{P0J1rnpA+2hzyA#Zk*+oDULLje185s-FWT=yUL2nB_%uIuWl>!<4;96jb9YK8}FCVHBD8L z$P2RIkUVSUJ9)eH8KO>N!8GuMa*>?8qurVfPb@a~X@_$@xr^TWg?FS}dFEpt=gZqH zH&;5CzGK*SLB0Q13$W#vqlBb<0##Asm#arQ&KRpdHG4vX4{DB-VY9Vi(vn{aOel<; zL&>!0j4jD7#O_0KL~Wq+9HgH7#~1-$hl8lZ78N!G^O+*J+-@i9!f5DTC3k_ct>st+ z6T7Rj(KTVOGP|PKD)Wm5L-mm{>XnzJ$qbSU*~YtO>@M~Yj%!t_#d6J$H&;G`INw0c zZKubaBr{86x3|m4aPq$uWf+m>EnUa%#5SyivaRE+v2Y!>TCT@8#`kV+#hu;F-*;?9 z^^e`Kj>6)!70JEr?#pqs_q>f&eJ%3hvepx7=Y^$F6$K7KhW0J zWqzvE<0(3l?DPBe7{oUYtpUf#18{}Wk&%(rx_?4{@37MzmolEq{jvM<_jP* zoudfeCSu|eg7lsP>{yNQIq;}V$PCZj_$N9A+O)B7BLMKCM?VML^KY+b8QmZ=55!0b zeDK8AAHoL63v`_UT^6~`b_$XC&t)5Sf6b9cR(3(SC;qiEXsrsYkL_QDNCB~daPUPI zpY+l?Z|Hs@Da*mTe-SdY1m!*Guks%C$~6z;DI!y+GTaL}d;IM^huwstv&E+qn3y+gTU8~8=tV%c{kHhy#9 zc}$ayOrdVx*xPRJe`c?mh=F4ag24JCdG;J>Tr4%*u<{AP5mz(|hcv1fNpEH24Nq)S zVZ_%~N^8WuPcYcJu>?ka7J)zcujZUfd@BwQO9SEE%=au7muXVlB_?ORpWxBN-@e^= zcE9_`-A~ocP2aYS-%6UtzCq?d|Kn)c=X=-DLktItyndVAU&l96XeMv8jHp;>RTT2` zdd;8`vz>cYco|4D@@Q- zufvnJek&s}8}Jl>+$YFG9-+CE0(e(+B_4NOyHz*C_{@By-w6)eR;4UcSX^wdSw&|) zCo*c;JeII*Jl@CZHS6j5^<58i+8}ps@f&fCEWX({QfluiDP@CJ&@+97Q#^V-z@u|J zt55H4oP#Ti<`?9%;Y@^o%a11iFt72(Z=`%)yT~LiZ(vbbuOA;SeWW`+Wk>vsMCl=m zOtvx$R>WF8XN1qIyag*y`eLqjiD#|)GC0lE?|{@-2RJf67byWdMig$^zcz99A9hR) z@8tT&HxIrIKS?MQnn!1}&@zkj0P+3m5!;fNi%ttAM#86a(92g};ji1 zS9TY6=cP(c^?9?V#$Gx+F!0kZjQ_{TTc4OIWtLc9rC%}KD{9IrzaxM!2Z2~q0*A7R z)iH%8U4ulGI*JGpnrx1w?+7g|uT|cj&r{CsWtIRbR^kYi3XznXD+5}(S`^ z!kYY~BEjG zQ-x2IzSqj_>v_z_658H=7e0T|BBrFZY{D2=;w=*88 z!N01ESFBlY({1SP;)gomcQ)kRR$uDemO#{Ze|Cu&aFaFN-{KWo7iWsP3)jOo*tOS_ zL44W}jto^Ns|_%vk9i@cTE#1}HeXG?NUyj9jf-IShDwSSW5SO^!nC=1qUyb^JFKaz z5>6F&K?i`+W(BUGY#-I-aTOweZ-NgIxwpnp@1!DhRRPV)Co5Z;u}%Lh#TdCP1mH0e z{j@!P?5+z4aJP?yhLAu^OJF!FjD&{<;76?!1xZnz~mTDR!G`8u(mpyb^dY+3vE@-7iw#S{3> z3-b2vGcB?(xeuZzna78n#d9H$wc*qhZ~cZTku~g&S85D_k8VkLCG+jU`YW}g>ZJ8I zq)}-lK;Rq87^RYliHMlw=Zxr}BeLNp{FoIs)VJy3TF&f|nJ3~5q(U+hCVS+ds-^Wq zD0~_YNecx6W<~sZ>GOlh1c$gcRjX2UK9jI>5F37kW5*Ylokl>MU%tQ}dp8($v#{Cp-wM=QTKPNZHFbefD(#1_C|CXbB$* z0Mc34Ywoz|cV{yzd_R+~62{!we&{u1EpFaQnJQuVU5=Ra?AcAVa}bk^>S`^QjeX|$ zm0{gT@X@^p^q31`g=c3j-!`4JpYeo+OrKcnZEMqpXdCDh8*%(gkE#2Y9&=k2 zM1gh=#Xn%p(v}&Q$YGl`G4p!|ML?SEQyVTz#s5`$p6Me z`4}`AB7=kT0k$tIYoooh9Za0)>F9vSL~uHpzI0`O_^|0@EXlFAChVbse|qcvyTCvY z(M=D&+mshQQ_gfKTpKAXyzJjNO-T@^31p6%weE?7sUU84D)ofBe})p_l5i*$sDJ#y z!}?YxU&m<0oT8rIb}n~DnP7vA$93z~k>4QMvD#+F-ybps;4${#P6tU!j?)n}Fx(#s zrhX8oxrVSE?n!Y&p7BStJZJY8ao<#IKD`V&EHTyFn)wRDFlj5<0qeil!uVqah3rhW zj`_-nkWYJnD#?9Fd|Sv z478+h+{sBZJ|Qk?!W#OgVcbdv+wFG(8g*-gEB4gyo%HmnzVo=pmOq~^fN))HEiC}#=Pw4E{z-%tOQk`g?FJ1i2tFu^OiZkLfv&BnSo1zu zm-Q+f0r;45fRBNG`nW!cT$uSr7sezhb>qfV8%KM>BYvie=jK>&mA9A*Jp&P{gqD}# z(8;XTIMVGo(A;0fKc3b#HQZhDlhs{kXFx6@&0WS1wXt<^v__z;Ms6hhvAD=0vU=r0 zqwNwBT>cXTf5*hy{Rf`qYsoJjf(w&oqu~Ic8gr1cIElzp|4etG&+l_e+rmQJ3Sc!p zeTd1m79=``U!(#lDmnFA2UVH7b`#9Pc8f9Q3YnQ~t<7%xJ3E@z`oL57;K6V3DCp2F z28DlsUH%2xlOG|iVKhCUtd31BAaBM5k%3VK!b}cQTW3&GBtbRX9|AOY|MJWsyUb}A z4l5;gR2;(JpivWHvARAvH!lJ^{=#dB06US&No0AJu@}wNtG@lA!-&m^#YIJofsRfw zq_JLX;_Qr5*I$$OOy>JS3x-R)a1P>b^2q@e@)(`*3;+<|6%g3kOL-f zSx|pN{6+OkU6M`ch3#y{!+hM?Z~`O`34^o$%G$QJ<9px18N1W-7Zc+_37*5CQopUQ zfAm1OfA3!{VE3W2a@n@jUzgOvKbxt!W_GU|e8uJe>)#1a)eRl(=yE)`lwCx*A)6Hc zgFa5!sg@fo$Ki3m)xOcBXCaGdpzk=sb-|}@JQ&DnJOWM^)=71BsdD6>nQMSL0m z3}0Fu*3g}tv927*>?)EiX^ZDz6wr1GKRKzKbwxx@2wB0+*XX5tS_BzML8*X)z1{?Nafiu6>!L901FekpGt=)X%gyM#keOjBA zu8DJ5!V0wRY21nd^WjPnGB`Ml5eoF^%8H67HF2+JmhE$v9;w$bF)|7|jF{;!GSV^8 z(&j@NLtDa_ovPAWGe>-l&O@d?5J1ocojrI4S`F4^=L`GgB=I=ab|cq9i68vGDnu(A zzRiL##SU|MdHKik?-$U_?IXzqu|1#j!^y&ARxNJk+Zi-pAJNkL)nS`>*by}#l8L#R zz?N)rCc}dI*27^vp~z=^uq}S)I}jT>0@+7dxH%{!{7)cO|1DDo%1RbJeE2bC5LH`J zyMFj;rGdSYK_z*5?5o_ylBT0NN$)1mgy)``zf7B3d>~z-!IFH?_tvW8R;|N}PuW8; zCX+7)ywUoo+Jl3#>oE&$nj5h+7Q*HoWzg}R;L!9>h@&!QL~;W5a0w+Qu*9QeP?OdPDe*Js@YWd ztPU$;0H*6Oc?5GD%I|lw#yV#jH4l+|uc%Oa0ovVUK^xhuNXCHSF!C()p zTtBy6`>Iu9cRpYGiXOD0mTzvZ-zmqs$2F>ZgLi4GK-Dm-a0^Jfpx<&tB{uDztd=an zcl5fqm^rn>aYt7r$VgB_ypv*QTSdJiqelw&x%Xx%CJ>}$@pfPdG0Cx2Cph5KWQvFZ zvUkn_cTZ$aLH3Q&yp1}pqey9y@o`W{dlnaFkgBZ6EOseZ5me{In35tu5&R@7;E5NQ z8okyQ1jSM)|3vDLI|DZx4NzhH4?yhd-hF%3lF=(yW>~2&If#%Z@DSyHGgUe_GHac8 zP|l^|>zU?fGhr)ALpvlk86Ev{7_J@?Dy?9bF6-a7-c53H^&)aaeKiBYaQBZi6U$AqgoFi*1dBd4YOlR zDH8SWZ9Ku|?$&OZpX{tZ3vPs5V_1MNIWuqD8Z4+WqeHV);y~}+hmcq~m2=(I*d|T* z-cMP!J8XBwI{B~vTxB&A5v6>GFgWgfb_wz0izEdlYN@K*VXf~5@!p-qI1`gx2PzXA zQ%TkybhEv~e=&7aNOK{?BJlmJFRkT%LBVU=axdzNdG*$tJfw=Uhz+C&+)4%fSriOb{8OoM3{EBELX}3WP%5KV(M_;lVp#r_M@oZ z5Yf+fwYmq(H02eG@@*)(uNlQ(yN`VS-?o-c*o>8HO#f=K8c+}CHd4P2%g-$KsD$<~ zkM2*01WzGz<}d;=drEJ#kB%8B@5IcAt{$}OGQ^1pSRsjib?>6gDojQy=UmYdNlU-a zahmhes;r!Z?Xuo`%HK(-Y9YD=n^)Y0#Xd=|Rp`0n(vB9>Y9 z)%3MXLVLXEra#?@c+evQwvlLCT$aIQh|RI z*W$zth*(P<4d0WJm=Pf2>R_J=*%*SGrc#)~av_HoquMMg4J@By+AHT;!=JLqdu7pO z0*A5G#f4KJa};H7gxB-EWeP)tgKo-W?5+B~4noSM>*@ZaPHDQKJ`}yMQ1?eXN==Di zf;RroQtY|2F$HI1i5F zfH7Ydg}+8=4>qt*2j=dqc<9uXt5%9+HrFB-{CzK3$OSU{d zSf6y=oFQ)wW$p$$-@3n1mMb92(sO~A5M`7pvTOa${s7DYRAE`uR9!>OTOGGrjD($z zDG=ebtg?vB!J2^g7me#@HT820eZrSeKG(&>9+Um_oL-0L74vcPVwwwsEW3smWVivD zBa$R@0%#`nXsRjw*GH4(UnPIgdw={C9J`k#=RDytbjXxlc;r*Vlf##Qf^~6?hOq5d z7RZ!OrXf5|xPgke2wPrPO^{GE0b~w$-??>7s%crzu}|cr6i>nDw;@EY!7mTq_~Tbx zw~u1YTt2j`Q1Lg~?m?N#VszSq1_!mtPxfCpOxy-pmWG=3_J-I;>Qemb(VLicLvpuk z(jM0jfR?5nnFE_`{1&&Y!L_1zSV7TOMh8r%=(VN9llmF$C3!dPn9S*d+Y@(?Q64_ zD8XP7M_64%BeWP8Ccimd-~F>^Uz?nmb`_ra_(G~=Ro1w)z}-8C2})6J@9TU9M!wz( zVh3TCFH+fz)w|nAZkPsz*BHS0SN@W|M?(mt?~J8_AI5FPEyd{cohbg2$vn3G1R$?> zZ3WUBeunoMwl$UUU#RW#oK&*qlyU(MKG69gSfx8Yp`I^)*In3O5^Y43x;oIA*9f@l z=qja1X}8_0ebkzkcD>d8<4}vHMRLNj9db3H#&tZS+-e!!n3gs>rw~p(KfXQ|_V@*- zJ|6Qi$T8?i>ICf5s@it>vYUzR))or~sT=r4`9Yc`Zv*LRnq|@h!m@T7JAZ$1UD}I; zlypTNq*=CYnLqe;gO`wI$;j4ovDEYd0ribCT{t}8262{21j+Be##tsIev=PU*P-Rw ziY>#TQwN3%r*W44Dunjp00M|#@`DD?1n<2csIO&H#l0^ohX(b}oJqoIf%`>s zhiZP=5}0m}=?-u^djx-Uc{w}KyA|Z+R){*4h@Y*J)#T@u9Ea#rp+IxvFPvlFW$#Dg ziOfb1wm2ng)P4BPg9l}W>JNv??^Cfz95=CtBB-A5B+z&oq@$wD4p zw>{8wu!6(}?y5H27is4C1sRK)oqdL3ynLCk)&1{nPH9a0a190>w&r&-nH$F3-ToKO z!3X@&3mq~ImMzlO=2!mEs7&js0yu|1Kj<0G4W&*F^K%NgZH2VE;Yd{n$jhxK!K>2{ zs54FrEyDN+zur=1v}YM~!vXVYR_8R$QssRpu!Wf;HhxCKm3+Mkr|ncRZII3wcC@vc z%{^{HFL1bPu{>%ox7YQ}=6I=NJvNJhTdYrrc#d5}zUqa{kPpG9c}z*EmO*2QP|Z&` znmtp9QqpI(s&8-OW-0Q@Ex-Tp51iHWS&hxaAyfC%%)FuXL6SN!~;z{$S;@Uc-`4N_v6v)0fY`-gi2WUwe`Ga_||f|B%jfyiH_6B!L8Ido~~UX;ZHSR ztAPGl4%831km=!alRXvXQ?K;>anND0F@HvS(j-ln^^atD->t1QK%c?12g~z6N`}ho zlMT0Aao8>=ks$w9)cyY! zbp?5S)`ZW^D2VA8#nx|_nZSNS6;!wi>QC7!$KVilHKM|T|C>Y5oqlM4~5*}`Eg zUI##I732U8aJ*C#CnQ*w@%83<{ZWplpGzqA6_zR?*XMnk1sTN!i5zn+pQ>)(E6CQz z3U?r~pvJP1{@B3OFDO_}b;oq?7IDb{EY;f;X^gG+xF6@8Z44#>);d;6Q?aY%!jW(A zTP;GeWt6L$L?dd?QeF9(lAc^7F$m4G^w$}Sh$!EjPQ|!jaa;dgEH19KvrtkR%?QV9 zr&rE5KBAPJzy6rWh6W_n8_X5#cJiafY$sHoJXsv)C^f7a0b6_1FJ&IxfIPZj^wzv#%)l(qt=z$&TqHl#n4dbx2_z}3@e2U z+8LKxsaU#oWd%f1L+C&eYIK_w$+#pcg6NNKyL#K$Pa6eW0FVrqZ!8?lQqQ(V<4Bpu zgN!GPK{E(GExpT)!G_DRt^*PL?bXiPG&C0LbXV_k3RozomW*h~PVxoa9C-6aXU2^A zWmoBJS&V)EUKusa8MBmmcvwuv2V!)mO9gIoiE)-@fG@`s)a%>dN_lu-M_13$H7guW zy1?{ay!LP4CS`Nc`wFQBFu!UmbA$IO3;K4bs~n#)=Eunfg(*xkS=}8rK#l-}_3qjb z)BFl(SLEmBK6@EK{f=Xwtk=y<{CY&wm2-_W!0uLkU&dP4s-97NRTZ@htO1$-ZSa{|aPragWWwxfV)w#ShT!k7a*; zb&!B%zrkxQ8{Ga{FC#2wA|?akEOc|T#Y?|XGrqh)ME~-^e#6zAqT9D`-`%bmEZe6O z9iJ*9?>8m#?_k3=V8^}^3??r8Z6k8i%u(Yl@V7&=* z_xdOQ1+TMO>ojb>ZE1{+Z@#8hG+fp6Q6l!$%QKA(V7Z~ArcJ(M?7l)<@XO%;q?)ei zR5Z-H`a`z#^cS5qYdohtSm|}+=G$+zMY{Uh8zVL%@n_D^1KLOTDMjUIZ)Im@70K6d z_z*20S&D>)M~xqDnIjxVPmYs@-8Z^p-IuhVJlO?1uJP}#85vCQX+xl}|1!AzhU3JB zk(2|H^zHZUnaHcIq5`t!&QQ)0c6OZwaNfQhDU~(odPq3(dE<^P!Bx}8I(U!P zz$+Khxbt`4_>o#SR3FZgq^VhhJ$vp)8u5Ag>ER;+*9n zBt9ybl!@<=Z_oQ_dK$Y~=m$&>kJ9>;*YwnofjYwJ17m!yR?*l|u>_C}Zi6*z3|0Hq zTux;u_SpK7r@@~$J*e{~UwG3`++aib*xL8sGwkO0R#J$`azKdZE)d7}P-eSt168QO zIsl-Q(FYQMuw^G3Ie`91=HCMx+ZBGTGGp|5_WSlF36!iHOv?_z3%=G2taU&(E~%*KwDH7bU<)2JZ9i2$4i)#?w^T_9W~DpkD|3Tx>{lxf@K!gn^D zEvPv~r$4s)3Giz>V)vf(sbhxPL^X)kx<^Wz)@Y@cNl$|Oyv}HUd3NI)$3~zZUh!iH z$IAQL`O%sEbiv4$6ump$*Kud2K9Jr0bt0!)TeupfDjZKrY}fThAe}kj7$vD=#Ox4; zbn;M~5La`~K3s%)r3XA~-Rnz#`}OC~muUG)Z@K|yevRiR?RNRR1r&nX19bVdKCZ^b zA1d>u*rXb-Em{X}3aP|PKGP>p6=*(z&&jjN^BL@>V4qKFU$IpSJ9__i#h2pmPy|>o zqTX67DaBJZM!vJG8U4C0falJz3RGHLupktUm(&w5Cf6v)C5tY^KRDil3();*s9+n2zEmX-V~{IQcLZ zn+Nf@!PV%m{-N4QQPs#p&Qs1;siv)GLV%0MY5qFMwEV#0Gs&(RhY)kRXSo3|hT8Q| zePcPtM{WU?Q&*^&wT3mED@l%^A`1Jq=hv9sJ=7og<_$h`D2>)HjG?txMeG>H63a-R zjEZ4Yr!!}{Y;#6)=&jFnI4+`xH>Q%;Mqv2~ZCQ^Nu8R<7H(p__0hZ@nphQvnI>EaW8F||fKyrBDN z+0nB8Y2!;NR2AB`o@lnK>EhinZ4z*dTzur7L5$@@@Q?0abTmiF`x(FnN~;j~gt9c# z!q<8!H#=0~iW!C0t#C>|z+E;|@zR5TF75!aL=`<6tsQDgisLr_BoV>tfiz7Ld5+KF z$h0@VQzTu?NQygK;L0c~rn#rB=Ah)Vp`PdjVtYf+&8z08RV=Al^gj|!elnnXtWFtU z%&QBjyGlgAd7)<1d*|wlc1ii@IISUO@G5r12dlcp!3~Sns2cly=WIp_Q)dvaT!5&q z;Diht+N}^15p=bG=GJv_>jptBnR zt^u>LV(a|Uvy;%^;FnUYdJ7gGfvfd(q)T9waUXyC2loV?>BFdaL@N>#L^+LL!4E!) zwoL4Y!SJ`jQ_fepK-W#D2Rg3kII76IOgJ1p@Gryw?U-5eIvfio5B6aEVD5&y#SjLx zQFByTT#(-Z$G9KTighr48c~IvZg#@5F1=(5izn?q|FNt^K?=d~;-~T4Bi0^kaJK6n z%?*z|dN%5K?K1P!y=%3{6{sSPf`)T=9s7OUOhjRpU1%?T?iG1IyaxJmR!`A|Q2c|t zBs0?I8}A=M?;ebsD|Eb&cr69R4ifp!adX@A3Tt!mDAr_)IriGSntUFYl<%io>5uWo zKvxY-CofU=_u!LUc`Iq7nk^7NIdD3IW-$R*%G<_vI2K}Jb(9|JY}?FJd~{{UBp3r?Yh!{^vGPSaXFM$&@*Wt+5GI(e7eVvQQI5`8 z&f8u08hDFS-0x`fOnTxr><^rWS2Gg(5lfwLZ~MAfxsa)HiXQB=7Z?c$W0kcU46fRf z=+pKLmRwLH?xr@+=Ek&%!d98v!mD5$TB@>}U?yaK{;) zS}TXO2)l*biz|gUlGX3E;&w?jO~hjZ{WMm2b>*@+MEcx5oS6v(--@K}>$*T|7- zYLod;2!yOy>G1=tksl}Z^G=8F(oYr+azj3dn7Wr@d&8(%jO*RYT{3>Aqe7IcM_i8m z^OkMug&P|9>IASN*`@_$dRmSF`K7OG$}}HoaL{<8iZC{-K^<$%l0$lyTfo{{&z|AEQ4XyU}sdBCL z!N2i6bbFNS3!D405c0`8AZJpz+V*!=9NoY`(&2UAHe`o<=u5jROG4krG)!IDI{2?w zuiDsldg{17(c62oJoEFqr7&lKzT8nka7(Xk`hrgGud~)7cLWYWS9SONbIzx*ovx|r z<9$x!bGTvIKUS;VP9nrAe37r`M~bQLWaZl$y?)%#Zso}T%AzyXV*Uw+i<>?pnmtO? zTQ$R^o{?WWYo(LVcvq3qF>x(2ia(zwAz7_cBt%5AWjySM8Rq(ZQ!1qfbI`8VunvmK za>LVcCnDv z$ul%$M+HDYwEW-fuWr8CcriEY(7(ZOnU>a$$FJtMJ(Tth?&h5Ks(KhJ9|a{v9%rF= zEn4yZ@DGU#^`UNNA^8p(0uP4FW0$*r$C?l3jD2|g>&Op>3gf7A3k``0%G9v!r1Z`A zOh-#z=Prn9dJ%e~zs29gxM^UH#F0A{;ShAzuk3Y<8j3esK1(J~fd_RMC+fic)z53+ za;}p8NRUL7QEi-k?$|KHyr#x8etK2Am@YN6(B@+8P*uO0Eca3Jt5%2BhPH^>r@n)M zqnce!JfU6_Wh~uA1%|w``TV;#r}3Y%oij=;F&qfoQ_?Xa={t|Y&kFFpVhiOprB#Ee z&d14lHnQn$FUX%P(nFihOu%kHEd2M}R-J^K&l&_5W-ohbW|Gm14Nl}_^$T`%aNSm~ zA1{72Qu6KFx6mB8Zp?(OH# zV>KaFjPl2RPi5_Aio59WWQ>h88|=-6mYc%hDzjU8wQLOPwdqy2pKEg!7YsX8tbD2C z5iq_ON9*pG7@w6x$6keHiJ+=uxcL$PP5IEeC;8}>EwA$M+gdu} z`#L{m`}10)m`*t#Ws>eDZCir8ZZ##2xDgpY3A~C zaxj!2IRbj{Wzd7>3kk)2%X@L;vYm1~$K4+Kwb=Ws2%F%~Ylgm3ccGJ0t|ao>S|kPA z60UWolP8-AxXCVNeuv*nElMdQZJ!CA3I=)g?m{MZYUzX3Ti`0XCb?XbtQB(2)uFsc zn5(Ttysay8oiOJ&H?OCfqwmYJ@}wt}uWe0~sygTLP*Vwpa=F$jh(1paK7B25PJSlm{N*3{H3hj7plSO!Ero78_~YL^Nn zurLkgjNTp7F7^kYeH(2=Wkv-I#$I0G!92P}_`ok7T>vR_RtAS-_GJF9e~x*z`8!X# zjN60I3#_oCWU4jBjrxUH)6Oreo&7nowcVvp$$mP^cvreKL;X-INF096>p{*;$!9JV zjhB{YW(#C0#}`pDvbviBEm*nxzK ztY3d`<2TJB+#f&-I1D=-P3rc5C#a%+6WwN2M}c7w;E$@1M`iAa6K^ZXEd}=uBe{H8r*S21||eKFqq&kpW`e!?xPPJyq4r+3fzM&9o5y1m@pjLqhZY~&K zIMlLR?t>oN;#2EeCjrS8*Kt}!H+!LA?RJ2BSa4)%ngv}qh0jJ0VzJg+gdXafp`Sgn zvvWjxM(#^#ot@Ic!j)YzT%>$1A!`#`!OsKu7)Rjh8}Eu=C4X;fnh=p-gp3t{UVLj< z+|TdJgG@j;fo(O@afRQ&MVKsEF69WRm+BRw(KJD1!DR=3VD)YXpmqwUM!e4ZeI4+V zUp1JV&|qj%JV?Z5Fo3)M@>>!Lr;yh|HF@spAW`YsllX)fpSix zy;mfRNkPUhQZq(Z$9Y`L>POn=nd|2o#RUZYF)2qWpT2UCO{mM!00ypU=8p8{OGdy^`{V{AD4iAijO&(n(@LwJ4PXC$vm;K+cs#D689WZxx@E@<+C|PP#xFzmC zma(Mz&*D4&keCu?FedQtu|g;PXATKLA>dCV2LTX#`j=B7?tiOga>5FM;PKYl-P7Pe d5<=xl4C%H=!l#ZPr3kO3B&YVc_@UX`{|8^zBgFs! literal 0 HcmV?d00001 diff --git a/docs/code-layout.md b/docs/code-layout.md index d76e3d42a..eab1d2fd4 100644 --- a/docs/code-layout.md +++ b/docs/code-layout.md @@ -48,6 +48,10 @@ They're in `core/registry.ts`, and each is a field of `ctx`. Every registration ## Server +### AI Office structure + +`src/server/office-structure.ts` owns the versioned Zone/Department metadata and resolves old floors to Main Office / Software Engineering without changing `floors.json`. `src/server/http/routes/structure.ts` exposes signed-in reads and admin-only edits. Types live in `src/shared/protocol/zones.ts` and `departments.ts`; the 3D and `/lite` views share `src/client/ui/structure.ts`. The data and later delivery phases are described in [AI Office architecture](ai-office-architecture.md). + - **`server.ts`** is the composition root: `startServer` builds the office's context a stage at a time, starts the hook server, opens the floors, and hands the context to the HTTP handler and the WebSocket. - **`office/`** is that context (`Ctx` in `office/context.ts`: the building-wide services, sending to browsers, the floors, the people in the office, going between floors, and the checks before something happens) and the pieces that make it. - **`ws/`** takes the sockets. Each domain's messages are handled in `ws/handlers/.ts`, a map from message type to handler that `satisfies HandlerMap`, and `ws/handlers/index.ts` puts them all in one `HandlerMap`: a message type with no handler, or a handler for a type that doesn't exist, fails the typecheck. The same file lists the features that keep something per person on a floor (`features`: what each lets go of when someone is `leaving` a floor, and once their socket has `closed`) and the pieces of what someone arriving on a floor is sent (`views`, one for each field of `FloorView`, put together by `office/views.ts`). diff --git a/docs/features.md b/docs/features.md index f320d9b99..d19dc3609 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,5 +1,7 @@ # Features +- **AI Office zones.** Training Campus and Main Office organize project floors by department. The zone browser works in 3D and `/lite`; admins configure departments and project assignments. Existing floors resolve to Main Office / Software Engineering. Training Campus does not yet isolate running workers; see [AI Office architecture](ai-office-architecture.md). + Everything in the office, room by room. Back to the [README](../README.md). - **A floor per project.** The first time the office starts in a terminal it walks you through picking your first project (see [Run locally](../README.md#run-locally)). Otherwise you start inside the elevator, and it asks for one: pick one of the repositories your `gh` login can see (or type `owner/name`) and the office clones it into the workspace folder, `~/agent-office//` unless you pick another (**📁 Change folder** right there, for admins). While it clones, its floor on the panel says how far along it is (*Downloading 64% · 231 MiB · 1.5 MiB/s*), and you can close the panel and carry on: everyone hears when the floor opens. Whoever added it, or an admin, can stop it with the **⏹️** beside it. A clone that hears nothing from GitHub for 3 minutes is stopped as stalled, one that would have to ask something (an ssh host key, a password) fails straight away and says what to run on the office's machine, and restarting the office doesn't stop one. To add another project, or go to one, walk into the elevator on the north wall and press **E**. Or click the project name in the top-left corner for the list of floors: pick one and you're there in a blink, standing on the same spot in the office. From outside it (the balcony, the street, the garage) it takes you in, and you step out of that floor's elevator. Each floor has its own desks, workers, issues and PR boards, task queue, services and pictures, and its own wall and floor colors, so you always know where you are. You only see and hear the people on your floor. The elevator panel shows how many workers are busy or waiting on someone on each floor, and you get a heads-up when a worker on another floor starts waiting. An admin can take a project off the building with the **🗑** beside its floor in the elevator: its workers stop, and everyone on it rides the elevator to another floor. Nothing is deleted. The checkout stays on disk with its workers, queue and pictures, and adding the same repository again moves back into it (as long as the workspace folder hasn't moved since). That goes for the project the office was started in (`agent-office `) too: it stays off after a restart, and the office keeps its own data in that folder as before. diff --git a/src/client/features/hud/index.ts b/src/client/features/hud/index.ts index a54730173..4875eb49c 100644 --- a/src/client/features/hud/index.ts +++ b/src/client/features/hud/index.ts @@ -18,6 +18,7 @@ import { toggleFloorMenu } from '../../ui/floormenu'; import { openHelp } from '../../ui/hud'; import { mountHud } from '../../ui/menu'; import { openServices } from '../../ui/services'; +import { openStructure } from '../../ui/structure'; import { openSettings, type SettingsPane } from '../../ui/settings'; import { needsSigningIn, openSignIns } from '../../ui/signins'; import { openTeam } from '../../ui/team'; @@ -69,6 +70,7 @@ export function installHud(ctx: Ctx, core: CoreState, parts: HudParts) { // The office has its bookshelf for them; a map of its own may not. { id: 'docs', icon: '📚', label: 'Docs', section: 'Open', shown: () => !inOffice(), title: () => 'Read the project’s docs', run: parts.bookshelf.showBookshelf }, { id: 'elevator', icon: '🛗', label: () => (inOffice() ? 'Elevator' : 'Floors'), section: 'Open', count: () => store.floors.reduce((n, f) => n + (f.id === store.floor ? 0 : f.waiting), 0), title: () => (inOffice() ? 'Ride to another project' : 'Go to another project, or add one'), run: travel.showElevator }, + { id: 'zones', icon: '🏢', label: 'Training Campus & Main Office', section: 'Open', run: () => openStructure(travel.switchFloor) }, { id: 'roof', icon: '🍸', label: 'Rooftop bar', section: 'Open', shown: () => !core.upTop && inOffice() && builtFloors().length > 0, title: () => 'Ride the elevator up to the roof: a DJ, drinks and the city', run: () => travel.ride(ROOF) }, // In voice, V is push to talk, so leaving is only from here. { id: 'voice', icon: '🎙️', label: () => (voice.inVoice ? 'Leave voice' : 'Join voice'), section: 'Together', key: () => (voice.inVoice ? undefined : 'V'), on: () => voice.inVoice, blocked: noMedia, run: () => void talk.toggleVoice() }, diff --git a/src/client/lite.html b/src/client/lite.html index 50f7f23fe..0d74dad38 100644 --- a/src/client/lite.html +++ b/src/client/lite.html @@ -15,6 +15,7 @@ Floor + 🏢 3D
diff --git a/src/client/lite.ts b/src/client/lite.ts index 49aaf6f0e..2d36078cd 100644 --- a/src/client/lite.ts +++ b/src/client/lite.ts @@ -23,6 +23,7 @@ import { openQueue } from './ui/queue'; import { openAsk } from './ui/ask'; import { openMeeting, type MeetingPreset } from './ui/meeting'; import { openSignIns } from './ui/signins'; +import { openStructure } from './ui/structure'; import { modelBadge, providerLabel } from './ui/provider'; import { byUrgency, waitingInOrder, waitingLabel } from './nextup'; import { askNotifyPermission, DesktopNotifier, notifyPermission, waitingOnSomeone } from './notify'; @@ -95,6 +96,7 @@ function offTheRoof() { // ---- The floor you're on ------------------------------------------------------------------------ const floorSelect = $('floor') as HTMLSelectElement; +$('btn-zones').addEventListener('click', () => openStructure((floor) => net.send({ t: 'floor.go', floor }))); const floorLabel = (f: FloorInfo) => `${f.name}${f.cloning ? ` (${cloneLabel(f.clone)})` : f.waiting ? ` · 🙋 ${f.waiting}` : ''}`; function renderFloors() { diff --git a/src/client/ui/structure.css b/src/client/ui/structure.css new file mode 100644 index 000000000..6420042f1 --- /dev/null +++ b/src/client/ui/structure.css @@ -0,0 +1,14 @@ +.structure-window { width: min(680px, 96vw); max-height: 88dvh; display: flex; flex-direction: column; } +.structure-window > header { display: flex; align-items: center; justify-content: space-between; } +.structure-body { overflow: auto; padding: 12px 18px 20px; display: grid; gap: 10px; } +.structure-tabs { display: flex; gap: 8px; flex-wrap: wrap; } +.structure-note { margin: 0; color: var(--muted); font-size: 13px; } +.structure-department { border: 2px solid var(--ink); border-left: 8px solid; border-radius: var(--radius); padding: 9px; display: grid; gap: 7px; background: #fff; } +.structure-department-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; } +.structure-floor { width: 100%; justify-content: flex-start; } +.structure-empty { color: var(--muted); font-size: 13px; } +.structure-assign, .structure-add, .structure-edit { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 10px; border-top: 1px solid var(--ink); } +.structure-assign h3, .structure-add h3 { width: 100%; margin: 0; font-size: 15px; } +.structure-assign select, .structure-add input, .structure-edit input { min-height: 38px; } +.structure-editor { width: min(420px, 94vw); padding: 16px; } +.structure-edit label { display: flex; align-items: center; gap: 8px; } diff --git a/src/client/ui/structure.ts b/src/client/ui/structure.ts new file mode 100644 index 000000000..8587e5d40 --- /dev/null +++ b/src/client/ui/structure.ts @@ -0,0 +1,98 @@ +import './structure.css'; +import type { Department, OfficeStructure, OfficeZone } from '../../shared/protocol'; +import { store } from '../state'; +import { h, openModal, toast } from './dom'; + +const CHOICE = 'agent-office-zone'; + +async function structure(): Promise { + const response = await fetch('/api/zones', { cache: 'no-store' }); + if (!response.ok) throw new Error(response.status === 401 ? 'Sign in again' : 'Could not load office zones'); + return response.json() as Promise; +} + +async function mutate(url: string, method: 'POST' | 'PATCH', body: unknown): Promise { + const response = await fetch(url, { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); + const data = await response.json() as OfficeStructure & { error?: string }; + if (!response.ok) throw new Error(data.error ?? 'Could not save office structure'); + return data; +} + +export function selectedZone(zones: readonly OfficeZone[]): string { + const saved = localStorage.getItem(CHOICE); + return zones.some((z) => z.id === saved) ? saved! : 'main'; +} + +/** Zone and department navigation shared by 3D and /lite. Floor travel remains the existing path. */ +export function openStructure(go: (floorId: string) => void) { + const body = h('div.structure-body', {}, 'Loading office structure…'); + const panel = h('section.modal.structure-window', { 'aria-label': 'Office zones' }, h('header', {}, h('h2', {}, '🏢 AI Office')), body); + const modal = openModal(panel); + let data: OfficeStructure | undefined; + let zoneId = 'main'; + + const fail = (err: unknown) => toast((err as Error).message, 'error'); + const save = async (url: string, method: 'POST' | 'PATCH', value: unknown) => { + try { data = await mutate(url, method, value); render(); return true; } catch (err) { fail(err); return false; } + }; + + function editor(d: Department) { + const name = h('input', { type: 'text', value: d.name, maxlength: 100, 'aria-label': 'Department name' }) as HTMLInputElement; + const color = h('input', { type: 'color', value: d.color, 'aria-label': 'Department color' }) as HTMLInputElement; + const enabled = h('input', { type: 'checkbox', 'aria-label': 'Enabled' }) as HTMLInputElement; + enabled.checked = d.enabled; + enabled.disabled = d.id === 'software-engineering'; + const form = h('form.structure-edit', {}, h('label', {}, 'Name', name), h('label', {}, 'Color', color), h('label', {}, 'Enabled', enabled), h('button.btn.primary', { type: 'submit' }, 'Save')); + const editModal = openModal(h('section.modal.structure-editor', { 'aria-label': `Edit ${d.name}` }, h('header', {}, h('h2', {}, `Edit ${d.name}`)), form)); + form.addEventListener('submit', (event) => { + event.preventDefault(); + void save(`/api/departments/${encodeURIComponent(d.id)}`, 'PATCH', { name: name.value, color: color.value, enabled: enabled.checked }).then((ok) => { if (ok) editModal.close(); }); + }); + } + + function render() { + if (!data) return; + if (!data.zones.some((z) => z.id === zoneId)) zoneId = 'main'; + const zone = data.zones.find((z) => z.id === zoneId)!; + const tabs = h('nav.structure-tabs', { 'aria-label': 'Office zones' }, ...data.zones.map((z) => h('button.btn', { + type: 'button', 'aria-pressed': String(z.id === zoneId), class: z.id === zoneId ? 'primary' : '', + onclick: () => { zoneId = z.id; localStorage.setItem(CHOICE, zoneId); render(); }, + }, z.name))); + const departments = data.departments.filter((d) => d.zoneId === zoneId); + const sections = departments.map((d) => { + const floors = store.floors.filter((f) => data!.floorDepartments[f.id] === d.id); + const card = h('section.structure-department', { style: `border-left-color:${d.color}` }, + h('div.structure-department-head', {}, h('strong', {}, `${d.icon ?? '🏢'} ${d.name}`), + store.me.admin ? h('button.btn', { type: 'button', onclick: () => editor(d) }, 'Edit') : null), + ...floors.map((f) => h('button.btn.structure-floor', { type: 'button', disabled: !!f.cloning, onclick: () => { modal.close(); go(f.id); } }, f.name)), + floors.length ? null : h('span.structure-empty', {}, 'No projects yet'), + ); + return card; + }); + const assign = store.me.admin && store.floors.length ? assignment(data) : null; + const add = store.me.admin ? addDepartment() : null; + body.replaceChildren(tabs, h('p.structure-note', {}, zone.type === 'training' ? 'Training Campus is organizational only. Workers are not sandboxed yet.' : 'Main Office projects and departments.'), ...sections, ...(assign ? [assign] : []), ...(add ? [add] : [])); + } + + function assignment(view: OfficeStructure) { + const floors = h('select', { 'aria-label': 'Project' }) as HTMLSelectElement; + floors.replaceChildren(...store.floors.filter((f) => !f.cloning).map((f) => h('option', { value: f.id }, f.name))); + const depts = h('select', { 'aria-label': 'Department' }) as HTMLSelectElement; + depts.replaceChildren(...view.departments.filter((d) => d.enabled).map((d) => h('option', { value: d.id }, `${d.zoneId}: ${d.name}`))); + floors.addEventListener('change', () => { depts.value = view.floorDepartments[floors.value] ?? 'software-engineering'; }); + depts.value = view.floorDepartments[floors.value] ?? 'software-engineering'; + const form = h('form.structure-assign', {}, h('h3', {}, 'Assign project'), floors, depts, h('button.btn.primary', { type: 'submit' }, 'Assign')); + form.addEventListener('submit', (event) => { event.preventDefault(); void save('/api/floor-department', 'POST', { floorId: floors.value, departmentId: depts.value }); }); + return form; + } + + function addDepartment() { + const slug = h('input', { type: 'text', placeholder: 'slug', required: true, maxlength: 40, pattern: '[a-z0-9][a-z0-9-]*', 'aria-label': 'Department slug' }) as HTMLInputElement; + const name = h('input', { type: 'text', placeholder: 'Name', required: true, maxlength: 100, 'aria-label': 'Department name' }) as HTMLInputElement; + const form = h('form.structure-add', {}, h('h3', {}, 'Add department'), slug, name, h('button.btn', { type: 'submit' }, 'Add')); + form.addEventListener('submit', (event) => { event.preventDefault(); void save('/api/departments', 'POST', { zoneId, slug: slug.value, name: name.value, color: zoneId === 'training' ? '#76a88a' : '#6175b5' }); }); + return form; + } + + void structure().then((view) => { if (!modal.backdrop.isConnected) return; data = view; zoneId = selectedZone(view.zones); render(); }).catch((err) => { body.textContent = (err as Error).message; }); +} diff --git a/src/server/http/router.ts b/src/server/http/router.ts index 42d82b2da..6b0fa18c2 100644 --- a/src/server/http/router.ts +++ b/src/server/http/router.ts @@ -18,7 +18,7 @@ type Where = { path: string | readonly string[]; prefix?: never } | { prefix: st interface Answers { /** Only requests with this method; any method when missing, and the route answers the rest itself. */ - method?: 'GET' | 'POST'; + method?: 'GET' | 'POST' | 'PATCH'; } /** diff --git a/src/server/http/routes/index.ts b/src/server/http/routes/index.ts index 982acdd1b..39c50d8ee 100644 --- a/src/server/http/routes/index.ts +++ b/src/server/http/routes/index.ts @@ -9,6 +9,7 @@ import { githubRoutes } from './github.js'; import { pageRoutes } from './pages.js'; import { searchRoutes } from './search.js'; import { serviceRoutes } from './services.js'; +import { structureRoutes } from './structure.js'; export const routes: readonly Route[] = [ // Anyone. @@ -27,6 +28,11 @@ export const routes: readonly Route[] = [ pageRoutes.favicon, // Signed in. authRoutes.whoami, + structureRoutes.zones, + structureRoutes.departments, + structureRoutes.addDepartment, + structureRoutes.updateDepartment, + structureRoutes.assignFloor, agentRoutes.models, fileRoutes.image, fileRoutes.whiteboardFile, diff --git a/src/server/http/routes/structure.ts b/src/server/http/routes/structure.ts new file mode 100644 index 000000000..22954828c --- /dev/null +++ b/src/server/http/routes/structure.ts @@ -0,0 +1,30 @@ +import type { Route } from '../router.js'; +import { readBody, sameOrigin, send } from '../util.js'; +import type { Ctx } from '../../office/context.js'; + +async function edit(ctx: Ctx, req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, accountId: string | undefined, action: (body: unknown) => string | undefined) { + if (!ctx.meOf(accountId).admin) return send(res, 403, { error: 'Only admins can configure the office' }); + if (!sameOrigin(req, ctx.cfg)) return send(res, 403, { error: 'Origin mismatch' }); + let body: unknown; + try { body = JSON.parse(await readBody(req, 8192)); } catch { return send(res, 400, { error: 'Invalid JSON' }); } + const error = action(body); + if (error) return send(res, 400, { error }); + return send(res, 200, ctx.structure.snapshot()); +} + +export const structureRoutes = { + zones: { method: 'GET', path: '/api/zones', auth: 'session', handle: (ctx, { res }) => send(res, 200, ctx.structure.snapshot()) }, + departments: { method: 'GET', path: '/api/departments', auth: 'session', handle: (ctx, { res }) => send(res, 200, ctx.structure.snapshot().departments) }, + addDepartment: { method: 'POST', path: '/api/departments', auth: 'session', handle: (ctx, { req, res, session }) => edit(ctx, req, res, session.account?.id, (body) => ctx.structure.addDepartment(body, session.account?.id ?? 'shared-password-admin')) }, + updateDepartment: { + method: 'PATCH', prefix: '/api/departments/', auth: 'session', + handle: (ctx, { req, res, path, session }) => edit(ctx, req, res, session.account?.id, (body) => ctx.structure.updateDepartment(path.slice('/api/departments/'.length), body, session.account?.id ?? 'shared-password-admin')), + }, + assignFloor: { + method: 'POST', path: '/api/floor-department', auth: 'session', + handle: (ctx, { req, res, session }) => edit(ctx, req, res, session.account?.id, (body) => { + if (!body || typeof body !== 'object' || !('floorId' in body) || !('departmentId' in body) || typeof body.floorId !== 'string' || typeof body.departmentId !== 'string') return 'Invalid assignment'; + return ctx.structure.assignFloor(body.floorId, body.departmentId, session.account?.id ?? 'shared-password-admin'); + }), + }, +} satisfies Record; diff --git a/src/server/office-structure.ts b/src/server/office-structure.ts new file mode 100644 index 000000000..5bde77542 --- /dev/null +++ b/src/server/office-structure.ts @@ -0,0 +1,165 @@ +import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import type { Department, OfficeStructure, OfficeZone } from '../shared/protocol.js'; +import type { Building } from './building.js'; + +const SCHEMA = 1; +export const MAIN_ENGINEERING = 'software-engineering'; +const names = [ + ['executive', 'Executive Center', '🏛️'], + [MAIN_ENGINEERING, 'Software Engineering', '💻'], + ['product', 'Product Management', '🧭'], + ['research-data', 'Research & Data', '🔬'], + ['design-media', 'Design & Media', '🎨'], + ['marketing', 'Marketing', '📣'], + ['sales-support', 'Sales & Customer Support', '🤝'], + ['finance', 'Finance', '💰'], + ['legal', 'Legal & Compliance', '⚖️'], + ['operations', 'Operations', '⚙️'], + ['knowledge', 'Knowledge Center', '📚'], +] as const; +const labs = [ + ['academy', 'Agent Academy', '🎓'], + ['incubator', 'Project Incubator', '🌱'], + ['sandbox', 'Sandbox Lab', '🧪'], + ['evaluation', 'Evaluation Lab', '📊'], + ['workflow', 'Workflow Lab', '🔀'], + ['skills', 'Skill Workshop', '🛠️'], + ['graduation', 'Graduation Gate', '🚪'], +] as const; + +function defaults(): OfficeStructure { + const departments: Department[] = [ + ...names.map(([slug, name, icon]) => ({ id: slug, slug, name, icon, zoneId: 'main', color: '#6175b5', enabled: true })), + ...labs.map(([slug, name, icon]) => ({ id: `training-${slug}`, slug, name, icon, zoneId: 'training', color: '#76a88a', enabled: true })), + ]; + const zones: OfficeZone[] = [ + { id: 'training', type: 'training', name: 'Training Campus', order: 0, departmentIds: departments.filter((d) => d.zoneId === 'training').map((d) => d.id) }, + { id: 'main', type: 'main', name: 'Main Office', order: 1, departmentIds: departments.filter((d) => d.zoneId === 'main').map((d) => d.id) }, + ]; + return { zones, departments, floorDepartments: {} }; +} + +const idOk = (v: unknown): v is string => typeof v === 'string' && /^[a-z0-9][a-z0-9-]{0,39}$/.test(v); +const colorOk = (v: unknown): v is string => typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v); +const record = (v: unknown): v is Record => !!v && typeof v === 'object' && !Array.isArray(v); +interface AuditEvent { at: number; actor: string; action: string; target: string } +type StoredStructure = OfficeStructure & { audit: AuditEvent[] }; + +function parse(input: unknown): StoredStructure { + if (!record(input) || input.schemaVersion !== SCHEMA || !record(input.data)) throw new Error('Unsupported structure schema'); + const { zones, departments, floorDepartments, audit } = input.data; + if (!Array.isArray(zones) || !Array.isArray(departments) || !record(floorDepartments)) throw new Error('Invalid structure data'); + const zIds = new Set(); + const dIds = new Set(); + const slugs = new Set(); + for (const z of zones) { + if (!record(z) || !idOk(z.id) || zIds.has(z.id) || (z.type !== 'main' && z.type !== 'training') || typeof z.name !== 'string' || !Number.isInteger(z.order) || !Array.isArray(z.departmentIds)) throw new Error('Invalid zone'); + zIds.add(z.id); + } + if (!zIds.has('main') || !zIds.has('training')) throw new Error('Default zones missing'); + for (const d of departments) { + if (!record(d) || !idOk(d.id) || dIds.has(d.id) || !idOk(d.slug) || typeof d.name !== 'string' || !colorOk(d.color) || typeof d.enabled !== 'boolean' || typeof d.zoneId !== 'string' || !zIds.has(d.zoneId)) throw new Error('Invalid department'); + const key = `${d.zoneId}/${d.slug}`; + if (slugs.has(key)) throw new Error('Duplicate department slug'); + dIds.add(d.id); + slugs.add(key); + } + if (!dIds.has(MAIN_ENGINEERING)) throw new Error('Default engineering department missing'); + for (const z of zones) { + if ((z.departmentIds as unknown[]).some((id) => typeof id !== 'string' || !dIds.has(id) || departments.find((d) => d.id === id)?.zoneId !== z.id)) throw new Error('Invalid zone departments'); + } + for (const [floorId, deptId] of Object.entries(floorDepartments)) if (!idOk(floorId) || typeof deptId !== 'string' || !dIds.has(deptId)) throw new Error('Invalid floor assignment'); + if (audit !== undefined && (!Array.isArray(audit) || audit.some((e) => !record(e) || !Number.isFinite(e.at) || typeof e.actor !== 'string' || typeof e.action !== 'string' || typeof e.target !== 'string'))) throw new Error('Invalid audit record'); + return { zones: zones as OfficeZone[], departments: departments as Department[], floorDepartments: floorDepartments as Record, audit: (audit ?? []) as AuditEvent[] }; +} + +/** Organizational metadata around Building. It never edits floors.json or worker state. */ +export class OfficeStructureStore { + private file: string; + private data: StoredStructure = { ...defaults(), audit: [] }; + private writable = true; + + constructor(dataDir: string, private building: Pick) { + this.file = path.join(dataDir, 'zones.json'); + if (!existsSync(this.file)) { + this.save(); + } else { + try { + this.data = parse(JSON.parse(readFileSync(this.file, 'utf8'))); + } catch (err) { + this.writable = false; + console.error(`agent-office: ${this.file} couldn't be loaded; structure edits disabled: ${(err as Error).message}`); + } + } + } + + snapshot(): OfficeStructure { + const floorDepartments = Object.fromEntries(this.building.list().map((f) => [f.id, this.data.floorDepartments[f.id] ?? MAIN_ENGINEERING])); + return { zones: this.data.zones.map((z) => ({ ...z, departmentIds: [...z.departmentIds] })), departments: this.data.departments.map((d) => ({ ...d })), floorDepartments }; + } + + addDepartment(input: unknown, actor = 'system'): string | undefined { + if (!this.writable) return 'Office structure is read-only until zones.json is repaired'; + if (!record(input) || !idOk(input.slug) || typeof input.name !== 'string' || !input.name.trim() || input.name.length > 100 || typeof input.zoneId !== 'string' || !colorOk(input.color)) return 'Invalid department'; + if (!this.data.zones.some((z) => z.id === input.zoneId)) return 'Unknown zone'; + if (this.data.departments.some((d) => d.id === input.slug || (d.zoneId === input.zoneId && d.slug === input.slug))) return 'Department slug already exists'; + const d: Department = { id: input.slug, slug: input.slug, name: input.name.trim(), zoneId: input.zoneId, color: input.color, enabled: true }; + const next = structuredClone(this.data); + next.departments.push(d); + next.zones.find((z) => z.id === d.zoneId)!.departmentIds.push(d.id); + return this.commit(next, { at: Date.now(), actor, action: 'department.add', target: d.id }); + } + + updateDepartment(id: string, input: unknown, actor = 'system'): string | undefined { + if (!this.writable) return 'Office structure is read-only until zones.json is repaired'; + if (!record(input)) return 'Invalid department'; + const next = structuredClone(this.data); + const d = next.departments.find((item) => item.id === id); + if (!d) return 'Unknown department'; + if (input.name !== undefined) { + if (typeof input.name !== 'string' || !input.name.trim() || input.name.length > 100) return 'Invalid name'; + d.name = input.name.trim(); + } + if (input.color !== undefined) { + if (!colorOk(input.color)) return 'Invalid color'; + d.color = input.color; + } + if (input.enabled !== undefined) { + if (typeof input.enabled !== 'boolean' || (id === MAIN_ENGINEERING && !input.enabled)) return 'Invalid enabled state'; + d.enabled = input.enabled; + } + return this.commit(next, { at: Date.now(), actor, action: 'department.update', target: id }); + } + + assignFloor(floorId: string, departmentId: string, actor = 'system'): string | undefined { + if (!this.writable) return 'Office structure is read-only until zones.json is repaired'; + if (!this.building.list().some((f) => f.id === floorId)) return 'Unknown floor'; + if (!this.data.departments.some((d) => d.id === departmentId && d.enabled)) return 'Unknown or disabled department'; + const next = structuredClone(this.data); + if (departmentId === MAIN_ENGINEERING) delete next.floorDepartments[floorId]; + else next.floorDepartments[floorId] = departmentId; + return this.commit(next, { at: Date.now(), actor, action: 'floor.assign', target: floorId }); + } + + private commit(next: StoredStructure, event: AuditEvent): string | undefined { + const previous = this.data; + next.audit.push(event); + this.data = next; + const error = this.save(); + if (error) this.data = previous; + return error; + } + + private save(): string | undefined { + const tmp = `${this.file}.${process.pid}.tmp`; + try { + writeFileSync(tmp, JSON.stringify({ schemaVersion: SCHEMA, data: this.data }, null, 2), { mode: 0o600 }); + renameSync(tmp, this.file); + } catch (err) { + try { rmSync(tmp, { force: true }); } catch { /* keep original error */ } + return `Could not save office structure: ${(err as Error).message}`; + } + return undefined; + } +} diff --git a/src/server/office/context.ts b/src/server/office/context.ts index 96090a2e0..1d6398a6c 100644 --- a/src/server/office/context.ts +++ b/src/server/office/context.ts @@ -17,6 +17,7 @@ import type { PlanLimitsReader } from '../limits.js'; import type { Webhook } from '../webhook.js'; import type { Machine } from '../machine.js'; import type { Building, FloorDef } from '../building.js'; +import type { OfficeStructureStore } from '../office-structure.js'; import type { Floor } from '../floor.js'; import type { Sky } from '../sky.js'; import type { Themes } from '../theme.js'; @@ -51,6 +52,7 @@ export interface Core { models: Partial>; /** The building: a floor per project, each with its own workers, boards and queue. */ building: Building; + structure: OfficeStructureStore; floors: Map; } diff --git a/src/server/office/core.ts b/src/server/office/core.ts index 47ec9f80c..88c168805 100644 --- a/src/server/office/core.ts +++ b/src/server/office/core.ts @@ -6,6 +6,7 @@ import { providerCommand } from '../agents.js'; import type { AgentProvider } from '../../shared/providers.js'; import { createModelCatalogues } from '../models.js'; import { Building } from '../building.js'; +import { OfficeStructureStore } from '../office-structure.js'; import type { Floor } from '../floor.js'; import { ChatLog } from '../history.js'; import { Arcade, HighScores } from '../cabinet.js'; @@ -39,10 +40,11 @@ export function createCore(ctx: Ctx, cfg: Config, publicDir: string): Core { // --- The building: a floor per project, each with its own workers, boards and queue ----------- const building = new Building(cfg.dataDir, cfg.projectsDir); + const structure = new OfficeStructureStore(cfg.dataDir, building); if (cfg.projects) { const err = building.setProjectsDir(cfg.projects, 'the command line'); if (err) console.error(`agent-office: --projects: ${err}`); } const floors = new Map(); - return { cfg, publicDir, accounts, auth, clients, chat, highScores, arcade, officeName, models, building, floors }; + return { cfg, publicDir, accounts, auth, clients, chat, highScores, arcade, officeName, models, building, structure, floors }; } diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index ee0d258ef..0d4ab75fe 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -30,6 +30,8 @@ export * from './protocol/settings.js'; export * from './protocol/toys.js'; export * from './protocol/usage.js'; export * from './protocol/workers.js'; +export * from './protocol/zones.js'; +export * from './protocol/departments.js'; export type ClientMsg = | PresenceClientMsg diff --git a/src/shared/protocol/departments.ts b/src/shared/protocol/departments.ts new file mode 100644 index 000000000..f5b0434de --- /dev/null +++ b/src/shared/protocol/departments.ts @@ -0,0 +1,10 @@ +export interface Department { + id: string; + zoneId: string; + name: string; + slug: string; + description?: string; + color: string; + icon?: string; + enabled: boolean; +} diff --git a/src/shared/protocol/zones.ts b/src/shared/protocol/zones.ts new file mode 100644 index 000000000..483033910 --- /dev/null +++ b/src/shared/protocol/zones.ts @@ -0,0 +1,17 @@ +export type OfficeZoneType = 'training' | 'main'; + +export interface OfficeZone { + id: string; + type: OfficeZoneType; + name: string; + description?: string; + order: number; + departmentIds: string[]; +} + +export interface OfficeStructure { + zones: OfficeZone[]; + departments: import('./departments.js').Department[]; + /** Floor IDs map to a department. Missing IDs belong to Main Office / Software Engineering. */ + floorDepartments: Record; +} diff --git a/tests/client-registry.test.ts b/tests/client-registry.test.ts index 09487039e..f4e1c8b22 100644 --- a/tests/client-registry.test.ts +++ b/tests/client-registry.test.ts @@ -379,7 +379,7 @@ function interactKinds(): { kind: string; file: string }[] { } } // Declared any other way, the scan above wouldn't see it. - assert.equal([...src.matchAll(/\bInteractKinds\b/g)].length - [...src.matchAll(/interface InteractKinds\b/g)].length, file === 'world/types.ts' ? 1 : 0, `${file} names InteractKinds other than to add kinds to it`); + assert.equal([...src.matchAll(/\bInteractKinds\b/g)].length - [...src.matchAll(/interface InteractKinds\b/g)].length, file.replaceAll('\\', '/') === 'world/types.ts' ? 1 : 0, `${file} names InteractKinds other than to add kinds to it`); } return out; } diff --git a/tests/office-structure.test.ts b/tests/office-structure.test.ts new file mode 100644 index 000000000..ff280c849 --- /dev/null +++ b/tests/office-structure.test.ts @@ -0,0 +1,59 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { OfficeStructureStore, MAIN_ENGINEERING } from '../src/server/office-structure.js'; + +function setup(t: { after(fn: () => void): void }) { + const dir = mkdtempSync(path.join(tmpdir(), 'agent-office-zones-')); + t.after(() => rmSync(dir, { recursive: true, force: true })); + const floors = [{ id: 'old-project' }]; + const building = { list: () => floors }; + return { dir, floors, building }; +} + +test('defaults are created once and legacy floors resolve to main engineering without changing floors.json', (t) => { + const { dir, building } = setup(t); + const old = '[{"id":"old-project"}]'; + writeFileSync(path.join(dir, 'floors.json'), old); + const first = new OfficeStructureStore(dir, building); + const saved = readFileSync(path.join(dir, 'zones.json'), 'utf8'); + assert.deepEqual(first.snapshot().zones.map((z) => z.id), ['training', 'main']); + assert.equal(first.snapshot().departments.length, 18); + assert.equal(first.snapshot().floorDepartments['old-project'], MAIN_ENGINEERING); + new OfficeStructureStore(dir, building); + assert.equal(readFileSync(path.join(dir, 'zones.json'), 'utf8'), saved); + assert.equal(readFileSync(path.join(dir, 'floors.json'), 'utf8'), old); +}); + +test('department assignments survive restart and removed floors disappear from the view', (t) => { + const { dir, building, floors } = setup(t); + const first = new OfficeStructureStore(dir, building); + assert.equal(first.assignFloor('old-project', 'training-incubator'), undefined); + assert.equal(new OfficeStructureStore(dir, building).snapshot().floorDepartments['old-project'], 'training-incubator'); + floors.length = 0; + assert.deepEqual(new OfficeStructureStore(dir, building).snapshot().floorDepartments, {}); +}); + +test('department creation validates slug uniqueness and unknown assignment', (t) => { + const { dir, building } = setup(t); + const office = new OfficeStructureStore(dir, building); + const input = { zoneId: 'main', slug: 'quality', name: 'Quality', color: '#123456' }; + assert.equal(office.addDepartment(input), undefined); + assert.match(office.addDepartment(input) ?? '', /already exists/); + assert.equal(office.assignFloor('old-project', 'missing'), 'Unknown or disabled department'); + assert.equal(office.snapshot().departments.filter((d) => d.id === 'quality').length, 1); + const saved = JSON.parse(readFileSync(path.join(dir, 'zones.json'), 'utf8')); + assert.equal(saved.data.audit[0].action, 'department.add'); +}); + +test('malformed metadata remains untouched and disables edits', (t) => { + const { dir, building } = setup(t); + const file = path.join(dir, 'zones.json'); + writeFileSync(file, '{broken'); + const office = new OfficeStructureStore(dir, building); + assert.match(office.addDepartment({}) ?? '', /read-only/); + assert.equal(readFileSync(file, 'utf8'), '{broken'); + assert.equal(office.snapshot().floorDepartments['old-project'], MAIN_ENGINEERING); +});