Compare commits

..

No commits in common. "5a889e42a41c210d8c538b76f3ab176f7bf8b6b8" and "7f445392ee3c5415021c249225c1019cc9ed4254" have entirely different histories.

82 changed files with 64 additions and 9365 deletions

View file

@ -275,29 +275,6 @@ sqlite, rag.db ~12.5k чанков по контексту/навыкам, herme
Ручной вызов: `cli.py rag-query "вопрос" -k 5 [--db ...] [--json]`.
Выключить: `RAG_DISABLED=1`.
## MCP портала в тике (t-261010093054)
Каждый LLM-тик получает инструмент `portal_mcp` — живой портал кооператива
через MCP-шлюз cil.by (JSON-RPC 2.0, агент зарегистрирован 29.09, владелец
alex). Ключевое для Alex: **звонки назначаются из чата** —
`booking_slots` (свободные слоты, call=30мин/session=20мин, график 3/3
Минск) → `booking_create` (jitsi-звонок со ссылкой, гостю письмо, запись и
транскрипция автоматом). Остальной спектр: list_calls/get_call (саммари
прошедших), list_members/projects_list/list_agents, treasury_status,
list_events, whoami/report_task/report_event.
Лимиты зашиты в код (не в промпт): **4 вызова на тик**, таймаут 15с,
обрезка результата 6k симв.; тул не поднимается без токена. Guardrail:
деньги-тулы `product_create`/`product_set_mode` (Stripe) запрещены по
умолчанию — `SIMBIOT_MCP_ALLOW_ALL=1` снимает. Ошибки шлюза возвращаются
текстом `error: …`, тик не валится; трейс — `PortalMcpCalled` в сторе.
Токен: env `SIMBIOT_PORTAL_MCP_TOKEN` → `<home>/secrets/portal-mcp-token`
(legacy-фолбэк `.simbiot-portal-token` в корне репо). Ручной вызов:
`cli.py mcp-call booking_slots '{"type": "call"}'`, список тулов —
`cli.py mcp-call --list`. Клиент: `harness/portal_mcp.py`, обёртка тика —
`_llm_worker._portal_mcp_tool_for`, тест `tests/test_portal_mcp.py`.
## Хард-кап шагов тика (e-260930031800)
Мин-воркер уходил в исследовательские цепочки (90+ tool-вызовов) и умирал

View file

@ -1,168 +0,0 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 0 R /F5 7 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Times-Roman /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Times-Italic /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/BaseFont /Times-Bold /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/BaseFont /Times-BoldItalic /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
8 0 obj
<<
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/PageMode /UseNone /Pages 14 0 R /Type /Catalog
>>
endobj
13 0 obj
<<
/Author (Shkor Labs LLC) /CreationDate (D:20260923161207+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260923161207+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Studio Participation Agreement \(42 Labs\)) /Trapped /False
>>
endobj
14 0 obj
<<
/Count 5 /Kids [ 6 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
>>
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2786
>>
stream
Gatm<?#SK-&q05P(_h2!mY.e,bHd@\ng7*D^eK5e#=XdIJX0u=)d.8S.Tu5D^I(o)/Usk._4j@+>P)msbaC7Co"`4/T(t<N!rP65M]30h+!rf5677H.&-e=/5<kM_5/^4J4E;IWHA#qt?SU/f]ZQrj7ueD+!<f+VHJKYAC^I3UR8`T=_;l,or)`pPQjk1h7M@/LD!os?7S]3CM$L<?F5gM>bZkN2;t/lF4d^3iK"`r8$I^l.]IY>QG:rY4qHhp6@OHtZ+*ub]nWG<:OR]=1\bO!nrVrAW\6uJR\-^!0[(!cMSSa?6&(e9t@FW&agg8fsqNK[;GL6A;58\bcgeA]I*-K7#pKVuYKg#pe<*EhHn3S_t22lQRZsb6DMQj1A.DG*&()d(7AoLIW<!\XOI6-hq)e(#Jn#0G&-e/0kZe+sMRgDam?Uhs4A8j)d&'ih,'+.Zq%k5_iO:"TO*Ot,:E]PR.OR.[te2\I7'I>ga2#QtSYEp)]mtNHHqRZ2rZbH/4FP+s//=;*q.Po(Q7"f"u>7ni!!WBR%F@cjNCeH?.Ho.G_U=.a8W?,o[Cjg^-"DA[F;1U0+L#P?Cn/4>p,B?"D)]d.71f`mr_DP3_W9"pR=%D%4cq0?Cf'8OKipo4)6$O0l^l,kn*h_J]!$*_aOUm2m_r9.>qe[JFD9\EKQp]/b9HcGW,?'maV3PH!,/kJJq=YhCP>`m>K9bFd*S9si2t5Vdku/O-R9BSh&rAP[dU:i5L!OAP/g(L>p&c/IO;04jMc[3%_ps@ABi358i%6[Pp"Jmhm[qd]--4J=Zo.Vk1%-H%8aJIn+RYr,mf<=Vk$^6&iS\d,#^?5#K-*U'09Co+HXU'6j`^`dW-#M4mkFQ$)Z47nP*0B;.u;kgs8)S@0HT9+O&o[1mO%\c/&jJE4p'7DQ@;<B;68%ZHacl=Dq1o(,&$oD/&a8<WT?,oVEtZM4F;:>GWVYV\01@MIGaRHW7O]!UGHT;JI/W_d[HHZQj>0-1TnU=Z?f\H<_t52g68i5+7,C/%'=is&_QbVF$Z*^o&odc*(KIt<&TimcfHH(WESeMkBq.N81IK6Wml'Ud?I3SU,0HL^2ic!LAl#p`s@o.<oCHV_mY!_;IRqf=&^%0Q0kHD-q:p&NL"94VM0-]f$9/S[0"O"ZQr7&Lem_,8s-i$Br,+$WtcGnGWf)ZXn;.U,nlaHC4Y6c(U+F9kOaoc%P:jBMn_l4%44kB,>+1aA*;G%C-rX&f:eRI9G4^"[SodC)_gFq`B-^:SuLFgV>)PQftmWhOl1^sB>@/W5F!Llg(&Qa"eHJ8]1`BtRXPA!F@!5G(">$@@9"tF>04sGMpQZGQ93Rgq0EH:n!%\nJQ8i!F$i(cY+"i#32GA@V]quab;3?J<VX=s%oj?U*:'?7Oi!X+L1&Z@;c:maGAS)G77G.$8\";FBh_()@7F/NV$r,qr`5#8_+[5MRuG).S?Fb&46TLR_=Z(H'*@cg#_GtUe[%M9as(,)m<tZs8_\ST9OJoKW)VKt)fk%cs/pMX#B$#"eMfp>erp*3c@8#!j0i>8nINW7,[%V>>2e"LTp1!n^=.g]dN$qk<7-SkkIY:6Rqf(D@UCXW=rS21eZ;\jTU"^@b5Y60K?b"a89!(o#t@c:!pYT]DoBW#B(lR6j%rjXh+4oFJu=iF=P`c_hN\fK_J7XZe&oA#%ZD4B7\YZ.,\)?,W26F!\RS=-+JCKc>UjH#.+F+A[c:o<Ri):+<,MopFr`tl]Y[D\GH4JP]L]RuF^+CQ9Fs"=6c+H5%%HAnrQ6$Y]-M/oE3.Y?8Y>ZX6C%guqGRnOR(sHEFp'[gg3YmIKlNhG;nP2%E`FS=gW*b\H\lX4<;-Cp+7JSE1URPM$ScUnC(MmMear&DQ*t2k:,H1C#fFTh!`EJH'^W$'QYTQCE8%Uod9_],Lq!.spjhrcITuiHhXl7E\-8`kBl+31n!GZP#qN5%@._McH!Voscoj8MKY;[K%K.Dphb&S+MRrdGD0JQC+YSs\\D,_\Dd^plA3nuL,i`TQgUe6pc0!B'+`L[.Q.M0rhoK.4`6NG[?h?[rWtA-2;iu$4;/[DcO0DC4]VqlU]2jG"=tIDm[s(O^`,SVU8M.5OjXb6OlG.'36:G;q4epM%!aF.(4Ck',1N3TAWThpD6;P#/We_!-PqZe!e;"f?OcBl$];[4`),[lj2mC5Ah]_!WgBho[L=3E[HGR^[_bT/c&pSDZ]Ost0*V\2OJ8T4Zf1Q2bc>#lR;:f=V;F[:[.s=^5:%(#$R<"hWV*]$o)9.W&;j&aO8a<gGgP^%*1Dri*O=N(1@&^>D[#bu]cRWR@3$F9!AhTE39Mqsm?1aM*]3Si@9O.cKlIgGQ6Ai-,.sW]rX;\V61,07(#X[gA=#,f$c&V%[/RT7O4r0`Z,`sZ'qPS%Tn=LstNZu;tI!o],2gm%\3JO1gVYt)GAi$MEN=[u_D,EO,L9aGCS$fR1Z/VfZ\^GDtXOFUp2S`F81P9'^GX7B5nV3B74\bX[q_qF>7"P*XV:2W7c6ZakWK7LrdV>OS37J^rcVpmkHe8D[e(U]aW9TopDL+;TW/fN%"Z/5dUo7&&9;sU_$>JMq^2"Ir(5]pF/VgcrNF[ns%boLMiD4;VgQcJ3^Bd/\oJna3E1G+:=ga)$;%J/6ZVd9%CWhlmch?W1G9Iq89I,Vbi1E7kW]'RU649^K3Z25>Ai#^Nq7;dVL3G8-^#?C9G9cErF-N`#Cp#h/(g&2p01caNh*?*kbuFA-!cMo,]A#4dI8k]A(cS%)\5ee'I/#qbQO:h~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3015
>>
stream
Gau0Dmr-r?')g\%Jgg=hBAofc@EDE"qiI]"@Y%fm0?F8'S.>ioU2=dZaVCeNrpT:b8_KC\2ar6aKA)/1EotW92_tl0O+2`V5Mn0gnU\Bg%9O1ILF!=?hMaVq`]cW"q>/)n$!HQ4k)o+dmLe%&T4UNM!k2[,-=2e2&W:7m+2i65[UG@ape%d[Jj'%V*e'uqc*e3WC"(:!gn%%5`/0gk@2?)sT6ogMB"%Nbn>0`T0V#I\h69@28b("GG:]rP8r<-&J:GE/DQb>YU*L0#4IKX,,_O:kZX=;rfcQ6pABF`')0osZp&[J"P2PE-=g:Q7GH>++]b[[^h_`N%.B7tYCf=D]D2]Y)\ud;\Cs*bLF1LC9eeMC_PY__Ee<J'ifl3dW2V2dj()Q=7XCS.EQI5\]2"dm-)g8EiK@f^9eC,Q6JoOS]Lg9V#57(p>H[%aF3rC/)6SJ)!F7+S-jHE]'VW$VOq^?Snb6fj:ig]Jf42lgM\n&J-0tos;1b2bG>-)Z#X]4Y5eHiDlG^s/$=sMP-OADUD?q[WGqLWJ`i,ja/:Yp=P5>F#pfEC[6<nA;CJC'NB<Yud\b@2Z?^:&#24!UV]ZaZim4Z'gofAb;6[tk[#MEt'[lm\u9.$&eR1Yh;8UhAl/TEA,7D$%@eA1i[4%_WB+`Y_B5MXd*V7(b[HK(!Vf,aqHeLr)=NeJW=C,ClQbhUm>G^F^WTgP12^9S+^(io\"E]f2S_io3B?j"rocE6)ji@0dBM8&OMtBZ(/eNGTnf!uVC-4/.8(E_7d5\BV3/h\j"J]^#'6&&X&'kBFt5>9TBB:QW0AOeW<OV%5ZNM'&*E#7A'aKVD6X6bk2sAi6/<Neqr.K/%*kEk.D?QE18si]&oXK-&"*B5HL[IM0sNA*\==l9IWZj_\_<RQ]TF8B)(H(s/:#)E5E\AiI2G`Rqpb0JQ;=H"Kd`,<H[)O*>f+M^p,+W;)?q/>VR:YAabfM/_QfX^ng`g:mUYr_o/!'t_;U^PMe\9lIL5'ZQuCSj\^iP7^m.,l5ST?@CF*eS[DXoJ9:2e140m[p!JKP>$,3;fL-!TQgN"8sB_M^oJ-o+cJu4VA>r,hT1S@#J"n"]9jQ`)*3HliUp6nK"rdYM[LKOhP/7q<!o!s-gl+]aP--+BYgM$DX0d$Q./coPu3cT?PKNo0jI`t0ih+Tm*&,9Rkj?=BhL%eV5+gXd\hE%aMm-6E^$t4(O0)Oc0):3F4"*\]+^`9%&2u70NRN-&3Z,<h6/gq/$I,ojI5)&R1`\iqbh'T0A'/\!77$d6(D@lQ`bt)YDtuWJ`Q;u!VPOeB8g!9pkkicd&%;j.H6uI**8;@CK$?#k9p8Q!BO@WmbnE@,iE"UdDrq4/8,<jn[%Vf>&K"AD7Dj=bnDT+TPB'?`,0Ro_8"SQJE4"h/(+^bZ7X7d=YrdX,8Qgt7%[jQg6L2US3)<gs8?aL3rKT&R<?lre0t;X`H"d`,s$$*/hDq&:K`uh%7oMWmW]Oo)2TWdh[l+gq[#.5lf3-F4t?*1-#h'iTNFRh$OVa?mijoDe<ZOUUIOr:)qHY&U$)FU\FJqc!,aWSK>`<[>B#W(">$)OH+eqZ_%YQd"!c*_f6%DFh:P4iE,@lij[K<m.kJf!JOn5+4>ol&'A>tM&I3/2^^'(i`:Q_%KPJCH9CW+oKWi4'ho\6F63iDdSL.15?4:0t+:@8o"pEPGc'-XQ?6u,((YGDQBn`d]Wp.:uqThASOah([PW&9QLk7[i`>J5+pkQGTnjYgsL?K\4Z=ll3lPHFP)/VqT47>hS*sH?ugH=/a2Jho]HVluc5E:Bg+TBV3m!G&>O[1aP+T"^mC^/Raod&Q%$9LnC2T@6:/Jk-H@RC^(VFHWBX6dd#0V.,u:3['YMk*j:8q>pCkc7Di5+%pn`6U[Z3de+c3Ec^2N)!<Tk_h%T<`>kph0FQ?!![kl3Vs*+UZ/1t@6=S,%mjK4Nf+;gmhJR(^ND@"D?J+^qb`L@)>,nCWX.GJce26s:lk^ZG-g,2Ws[=9+r"BD*r+71S#1%:/)@%Iob+%7)[?Cc[lE4tQISon[aSa<<;%HMQdB*,I@^`Yf=$*Gr\\Q]^<N\cer>q^ZA(#j;OE4%1ndiY'XK%jioG6f&:.icq*OA]AK/H9ANJm)`#QTL"8s:,AQ*MLhC;4reCF9S@`f_q(a:@AqZmuL%YG<PN+=GC;skR/"FSAMBrV!No.N>TObS=,Xs6Y-gQ0?o)C4q+iA]VS&dr)Y[H`)#^2fePET;:2D.-4(/N<''?#*g`Ri=:h5mY!;X>"GsZT(Z1-NL)N1o1CP80\a(o'VTc%5f?Ph;?U5Z>jB.)2a,oLG=;\b61C);cb`PlP-]g)r'o)WgCqXkmD'C@Zd3X9(UJ:^i]SF0/uZWQW-3):ck&hbpD5fJ8Q$Y*6@HCkb`<dD2oLdJJ<rm34$jOiN?<dY@,NkTCf^ogXZ.66[Fd$::E'&TjMN0ecKCcH<6IlQdfp-2VVCAUACPCF"*_Y'u!03LDRH&_%+G0$amcW9kY'h<A1X>LpEB0XiO06&:o'H^X4H:QXt@WUX!3R/ga('DVD#_F>uk=%7tV]6f\&PSBW$dmD,el-f@)Oi[DsIl1V$!n$)p_N&U#YgH8.oYH:3JLOEL,;7J:$*7Jr?XnPiZ*>JgjbTONs:2F[o?uQfn.e6eOpU(1""c=SfIC\,RO*2j!4'RPhh+61rPRGPYg?;ci"V6k&=uJo"aCD6!hlU9FCQX)aD69t$D..0R&s"i?#V&^a1_URY]WU56d7*VMDn]`@BGuAUUu"3C'E==;NQ3jmcGED!Klq''`l.ZK#B%1KOLae^,u'V_+MrW.ADMXfB1Wk)JrL!/l$EBoL:Re@5)co"O;5j.I]f.n8%4G<Q6hj3<"Y+CIlLu9.h;!M@9*(%n8<Ki(sHTk]/*7gW6d7WFJ76sh)nZ3X8SSWs$#H+\-%^d[iH`V5Kb7Oa_-Wdp)NNff=MB<'gnOD,i^OR]KFu/co'f[+C2M@2M6mD!H-Z<,0*gZk'thM&1I~>endstream
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2659
>>
stream
Gatm<?'!]1(4FN4.#epp?I9gD@8-6VlqCG3AS8@(]Ig0]@2D`Xj!%LbB%UJ*kE]th_1WS-obTIHEntf^^%Yb:bf<fCIV2m)50_d,T*#j^Cqa@Em)<f?L&F&^oDO[G,\3l5E?G!_fR/?0os\8VU7g0o9G9'MdtgGtk6I2"0&*\Z=2#G$H=6>_D#P6^g2.e6PW#(X8[E&BT1S#gZZ$\ae=V'O1j7XOUA;s;72SeQ8nk0AM\LNH4KqLZV%H/Z#62WpgV&d"Mioc)du!MsZXefPlc=Z&%"Se*b6$HVMK,^\_(_R(;>>t%`U<so2@(r7l89OBdUX<"f>95RiKj!c%V;%Zfr?M/O8[5-%GPl>&$/`2Y,#8MR)1N$e;Eee^j)Gd@1:%K,p9N5XfD'6Xa@KV0%E&4#3jS[7m!P!0>(WPK<^3ffWW?CLF&rtq>#(?D);lEA;qf$Z"IB^I<b[Z<dVGeIoW>qE`lPEb+$FC_!`!fib<g>gjA0ZP40MFE4WhpmM,*UbpB-G]3R_4mfBt<W."qB\^9dR'AHfd%U/tZTIj$DTiq+bQC6G6W3,!D3)bpnFsj@>1[NYe,OhEMS`12K9eKa?TlqV-%FDf&C:2W?lS[idKed,P<*GT'-P)We*@DCALtq7m1(GSp(3"1#6UF%$dc/'Q\_MGb%2.D;SE>-1pA?1-V_`PX>%<LI"!n<f-!e6f$*N/W?/N4Qe1d,a53u`hS/`Mnh^eN>$Nfbua2J/@9D-^t_h`3:j@E]^$<@^WCaK)aF@`3G8sTV*_QE\!&rdhkliH\+io,'d,X,5#6$5X/C"&OSEDU.gLGVAZbn8'/+J0PT"pD4KnJ@muN"2RX1*Lb6<YmhL16E5d%=DY0L-npU/V#A8WPCr0$'TJj;lKpDUcgeaa=b2#,:*OiqsWEls3Fo49[*pL%RJX8+s!8]NQA.e+Cg-CXY1T#O3+tHmZ`i7^'L%JeI&ilAnQ^!$G.+K4Nm8a(ilE4PuIhQH\H8MR^Gu!m+g1<,S%$BVlait.*$f#hcnp2>!bA)!):IkXJ.J8%hQ3;/58e?PiY-05SS\&?>T,7U/62?:*6/"S/&k3)T7*aG^ojA>C,YX6CjAGK>OlbjV5)3XhO9N=@18cY%[]+)6@/'A@^.D=C#LHdULG7I/,j.<r#p$1M-h#'0D\'?d-X9T15@ma\9+;=-0.t&?)#,^%cY=%2W3X":T-n!%:dtaJLJh[+_+.$t]F-aP]ud]B0ieMC9u+k!`^G>LusB1KG+E&Q<7\GYO$PPPA;`V3ZrNjbEioh@^P6FjGQFWUYE;QG'XE,^(kM3H^b$0+%23QPn?>8QrgK>YrC;&.?4uPHmu"PfC$P4Z8:CbJgFJhfr90MuS_RcSF\!N($(C%TI(c7S1KRX)qV\=Y;HdOJ\#"1sg@@P\(j\1XY0X&c+=Z-V5o^V\O#R'8/2Z=S]I.3_2,3i(ZtL2>#eSdYcn%6cE1F/%7Ji6sVC8FH@QWU\)Bi6K1`*ecKE5A55Os>\+Y(Cg(-h^Yo>6NRlJ+$($agM)BLb'O^i?B[r@6)@FF4.l-:8F@g_2C*CP<2qE3hV,;W<C*s*`nP[B;K%c=@N5H)G#,H(cqd[d300a'U(2IUqkfM8\VS)^1p1JA;jrR>9LM$=]`PNQ9$?i7/!hHB,=boSG>"TT'LU-VBMh_i8qGF@qCoL#t`dDGm:fZ)t1oK^ZO4i]plW8)AC8]>r,B@6>p!tSO2<>(C]1-<m8tcCm;1*Qk8j)gPcX\h.&T^#dF#*83g6V%K\UY&1?:`rd6'9Kl$d=-.C^:B]E6u3Ze^sOMqui3dp1hBcE8n%C?TW&2\91TriYHi:cEt]8J2?jn-&!O\13OtqBTL/9/]VOb&g1$b:83]6AArKoX`9WHW'[0U;'0]3_o7S]1OZG,+LhZ,*t4W3O*rU)<`efIM-iD7paV7MZ[4R@U`"6&Rd8%9_s7m5X;$#9VLhA\8SiPo!t2%<k5EY'qF"6pW$H/dh^CH'=k$(#nri>hIS/?B"S5b&h]`8:5-Wjjg0@FP[B>:WL0%6]$C^C]g2+>!6Wdk30$8*R*(F!OVUVG)o\sSJLA*^`,n3G4:n:"Y(%d;MAnG_FSf]I=U6;@l&!90eYf;cjjA$W65h-eeX7O(JT,o$,"FgsP&;DZLSO\>$isFTqa_d5AgFZ.]kiL65<;*cWU?,;WDN@(B8)k\KGCD$iIJUGNi+u%l!"RY6l<i$=<J:c/8m^/fD'">jk*$W8S>AM,]p$NN.'VSC!Mf`)J\DcV#*&#626p=S%CeoYh+cTuH>]4lScAA%`"_?0VK$H4NXh45cacNDafI0Vdk2T=`=ZcMT2MT@NK994\$$]/Ars4e3WZoUIr:K=e&a.(FmIWYG$6urKMINa*(tMM.H/fi+^J`t(D$IYBscbg=S:73?k&8aOJ$!*[\iK#kFm$Y8O^S16>TNiIshZ\1$,CAU*pRTKu9Vt@s557&*tBb*8bSnmcKp8pT,a5>kb$k1/#Ns4DB3)0k=-8/_:qW>+m!M7M7CeLE)NG(_Gk>m,f%%5%[FsJ`2#g/Ek]n2JDYMR&7T>.1/LRq5)RC0:dfr5B1MK_q/?3q#9%!dp1-]bQ"M^O347]31]*cdoVd`$lXXd'aIa%-0%bgk[f#kg1Ci+F;3%f^Yn)ZdBGS0"8m9GqZ~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2487
>>
stream
Gatm<_/e:"'#"/mqR6$^KEXUR"QE@cYu@Vo;Vm;>p7f_A(m&5<0Z_I'2#i#3%fd5=CeLU&fd0JuB$f#qp$:4#.)s"Ir%&e]TC1oM*th1D\uR:56lW`7NS`R+q<LbbP#<?<_kf=ba%NpTphp[j;_md.k#mr0q3om(-r.34/8Jql;&giuhU:4-])*kY%bn-8k=pj_)u#liZd#5G8;i/lGSfJR:"\T@.r%lSBtC(n=L.K`aV4'f<1:OD;U6ER+2gA1Q0$CS92jQH\(EMfE+E45OuOnrKHC/"SgUE.R\.hr_!/=<K!lY=[+ul\Hp#ek';Z<>,g+F<Mc_J*ocolnr)(n7^9<cpqc+m>d8WT)_tf%KOOfV^9rE]3mY:6.]JnuZ]75`k2+"]g[eq5-3bpcjrAg<A)=rKE.*]'1-peJh\GlXq??1'YW?.i[6*4j>jd/m>%U\"Q2.rW]q6m_0P#eDIlGsq0h'L_PocO^\ZV"n$-fpDhje*A$X?KhZ8X;P*_@^Q`bN\NV[:k-8=0[VB(LWF0?@E!IZm^f;0qu"7kf<D@#<9q).BV\'PNubYC/kVCc^08XW>pAA<RO&R@1HsO;BqZ3m'UBF'GBf'3jc1Z/sclJm+%Mi3cT$Sr\7!=lMq%U7(&9Il""A;QR$?0j_BE7+*//HDm^2)'Hi5*^@fW!l(/;n]A("AN*Z,tbguT5\&eiu7$L(rVfX0/`&.P5k2aBl;"\cQd@NqQK?tn>#j3qWb#3LAJ1[Mq*Mo#oCu+cW!NdcndVPCYJ@jCi#b#LD/"F(sl$J>/RR[Dgdd@ao@_!oelBPrWoa"==K>:K.C)9b&`_hnp=YY`UGT;9T]P\rFQPpYs6kLe,*E@AiS;m-jW'eXmWU#@h%t>u]OLaYhBM6Ik<@u._!cu+ShEX'HiZX>=rXo*qC"uD:h98O7*Do5+<Y)3n+_GX]lY8$qZ\\(!KgJ_p;'ibn&8!dDLP!AiO+_Ban((u3QbE!f)-"o6$M%>gedETs8M?=J(RGBe><_Qhk;Du28=2q1i:]un@rT=7L@sLRb??OC`'EC`2."gG#%J"]:&C8Id2mlJi`lEB)&g"Zo-en/rflQDVsRGXBMe04+:B0m*,Bp/pBlb9%M;HsKJjhePa)KT6.lBar$%+`-p[i.C*ue^:CJ#KfXITXO]'uE5;+IMmui&d%LVRd`rfKU(Xl4<QPs"m<mXg]@e:+B)MjL.CNToIqdC'plJ%tn`u8WEr@Im[\*L2,Vjg*Rfc=g+]Ek'HMs`d'T7n)4C&^).NH,%L6A,;6#Eu8s&Bsn&AJ2-*360U$/UQ(*pB&J'"?Zp;QEhJ-LqsU&/2e5K%m3I=c.HV>Q?%/1Rp[,NWo!E1j\j_9H#%`P%9.$5hJ1/seZe'C[]F!e>M]Vp\WRJ$U[BV!(WlLi3(@)Amb^21\/1F\GHF<((R$0i<U$$nXF-+#B&ceK7EI14#IsM#CG*M^nK01'3nRU.+NcK_#"^elZ2?ZX6oSto9\./]Y_t;@,$!K)Uj$]YT$Gd\Q0GuAkRU%qoE#rtV0keJWCH\KP,Df8F`q\t4A"Q"_!^T#/4RQ-,@+-5_\=]$e[HYcW!XPUH]D5r[QU'2\#^@Zg<]D,EeTUka5*J4]k56s"mi^ZXTMM<9"oq)+S2-Q!;TMXd^5&@,eqF+kPRIFbZC]i!mY_i;8fq20.CHk\:0AD%N1K9W3^#1JaZ@,Ghm/&"G\O-3:b>B&:*:Z$!IWL"1t&EA^C5U>/2neRRn2EG1c/*FT!G)o`;K=8u3j>6YgH3&f2&?TPANGV&$(V3dp6Cf0T\58DCe0bTDi;:Hs>m^[@c+M2NW"Yfd7A0=#CK4V%k2p#\/[(q#j`?^7W1`-oiYN2$h7@\bDU.#_O9B^n6MrD-SAbC$[8EOm[[_,mO3ErYiF1;_+k)U':n'oe,c(QI*W4lboKIcC;5_Wl*rTp]R.N3GDtF8Uob%khnjk(U/&cR1Nne\f(>$nr/jSkD&Y4ff-rio/g6XfQ#UCMci7n&KgE6qL7gWgc?[MfbE[1TnN#C\T-0!>^e&R:6\MnZu4;]ZtcI;KSX$R4YL$%g\^Aj`2X+nUPc'fU3D@5g2..H3!I\Ph3-oUPdV"Z@EDEX<u?dHlkiC:n*[1](j4Z-XbZ;iOW^45mA2QVj\+]TZTn-.u?QDqgWhu1m')Dd]88s3.6790hASd+nX#7h[O/BA:@:?/&DjiDhl@dSrolILa4J@FId88l:2W6RIT[#3L6iWcQs*LRSiXUO[FiG!!W`OT]$YC9e\a$h"j[7?-m<qe\,N5\9.Q$@[e)-d.`R;*r*E>7,a`!X>0"j+7'uf@JU&Z70T!?1TnH`H1_8fKe9%>oS!IdqjRs`^s#=H?#S_:\7^`qKo*1*Z&!$1m_T@bj4R>(q7D>ri.PF3Iaki#H[UtI3/"#nqWP>r:N+p"%o^]lU]0#.6g]hA#g(QS2:O=kZSYui:?U<s=_/K4AIY2,fNo'JCFL@W$,-.!43h[s~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2420
>>
stream
Gb!;d>B;!t&q8_Fd185PS?g1V^YkH/_2/gE4,0l-;oZ3W1BOp)7RRm/r;4skb[=O?9_OuUH.Q9o^dtTqYJ7fn!?6M.o/iKDZj1PriIKt*?jr'6q(T=1mbl!T+3YZ?@Oj.9Cs/qeiL9>PR-6t6-..]>bDB^>4Q!H8+MqtOB\\aWL#<4@T4.5AhJL2W&8Rtr'im(Kj92.k->qg.Ce%MGq*n%JOKjD2lUESj!X*p\F:"]WUjQ$1RVs28)sUbQ$=h6e>o+\<5bVV<-KA=RZ(3.^i6/mip=S;KqJ6TB=s$]D"^5EfEe;rKnTbG#aDrm-39LGn.V>b)MXdDI,Yb8&TK)So,%DTp0c!/,Yt3D3:nU6(JLF>no*g"_*`J6^-R)s<$T5mtYai"@=rV/@,HW55j!ZX[Dk%,k4AlK-4Jo)=1X[leSr=#$Ui_WpjZ2*'rY](\jXgl[I=0H^rSc-AgsIKQ/1C.g"P(D.7J2A-.:l:3gX;cMU?DopBeqB3&QVHD(A&#+Yl_X9;NW/5`<7ejO_:+KS)gA9l$DliXN2ff)qqff2a"CS>e8C`p/a9)$;.Mu8G^K0L4tW>JtMfO'nt64MCd)H_rQ3=W-hO/0X<W:ffIH9b6+m!NT)qId+']$ACc5&[:5."K.3#2RKuMuZqu8p-:V-+'Z3353[[R'ZcHfrCol\VqMH]$?iRk"kcdXdi^JL>0eA`]U<Cn@'-]2+h38`9S>GM(O&*.\D$l(f:?C5j$=1\mG,%ujR-"4ZS]"lj%0g2,-]h8$nI^I0;DQR"BSkgG;i/n;9Lc+Z-4qK"K]2E9=HJm_(El03AR$3jp2WStH',Fb_HcSp;CtN_Db"Q(3-D$?;$`@aoM[JI?s4Nk5oH3H@0Xm.H`\/#/0tJ,?5/$XPWWO>fn*QHfu3AF7m68_9C8r$=eYB?5/K`f+97d"F@9TP'M>!]o%BN_/`Q@g0UXX/HZ(tZ%GGd`ruQ<>"kQGF1e1Xr?b@CpJK'#k!e+*an2d%CDA38>P7emOh*NhiD5<1M9P4&IduVpfAAOMmNF\5&r^IlKopc<,0HdU&+XtfGpfd-#DP^h0Uk5XaR`%s;-`%$cls%N)hHC\Vn"gLaC%Q]Hf#Xoc@)*t#aEYpL,RW(ZGS]q]@T\MpRQBQmc$H9?Vd!n\=MP$j0nIu=B$lYgbH91WCotFN_=udOf&bG75/=Y#O**dN&K[rk@DQADYZuu?;k/tu)AdZ'1roMHGem8[*77dUMhFuo0&1gS&++=Cau(cgM/>8Ums4>E<q-^C.fGuh]t&cr<kAlD=6I29JjuHIC-jGKcWDnDF\]Fn"ds%1>gqB)L8nUMg>%g@:?C_pKRN$*d$IJ:n$E;TLFp14K&c0'ln?0M]6IT9I-c!eGP`*@@<eGs^".?#iK_rcb(#Zkq"1G!r?2$QVO2fnV&ADs>\93>@.7-4d+c-JFcICGqB_<%H+(t+VEb&1'\DlPS*5L+En7/$c<?q(5_^;fi"JCg:%2HT>"8'"_f!UOakpI_oEIOtJ%Dbo2/Q*g?2O#3U>=%=:\L4][O=^kH[#'SpNusqHj?/*-1UkVX"cE*1)4V3c4p"3]e3PqpAU)KiEcT;Xm&&fm)q\AmBu:/nq1tUWYg_,$4B?;7?gJpJ:#1FCrkb+?!jH4V_#'47's08<_ZOR5N.B?YlQbc96SkLeVUY&Yu/V'#-?ue/=E<G6l5JuI5.Z0O5<PV=Gsls:UjHdHElWP='TOqY`HMk(i/"T8TMJ8f5sBJ4df0M)F-,OL_(kHeI/3i?M:N+U,-V0kCdF!9^08Vme-W`."7?,0mZs25gURBj&%csQBF/(7/$].h'De35dtP_E"gQ*^-.70ZM=IsiEmMXT*1L$4fJr2.B]'U5,4<D"giq/Or$9WJ3N;06lPf9pi8=cA`[79CGu85?p%cRoQQ7ia.<c[S515@FpR[/SuM27Ut@Sfe;S<X(+SC3%.hR0#[+@CQd[^Z$l_UD\eA7[!j>)+\deJZRD(k0J`k;Um"Tkn_E3=J:N@X[-V1os6?%tY$8u(P(Hs-*5CH_AF(nJ)7.>.@gi"H;PJHmm8sPU)Q6Y6L1P<,8jfGTU?\.`&Y?+7SeRK]6/:6"PC1IMr5uY$:B?N$+qN@`dE5hRIb@8EuH:U"Vml_Y/ai[k&#TS1KX/[VVO*$ji7'E%EE=D!Q!cTHGRtp`(:?_(T=+/El%g0'93k<ZZ>OMQuAkB,IHH`j#Cf>5OD'nn7o!%Yhh`3p??\;j0)idm[3;@.@2n56eG#AO"r"(Bk\VY/#@6Z,Z65'ul=rlAC>,*WB1OIokYd972eF#BVk^6obZIo*X%kaeaEq+t23PXfa>i&aRY<2OXk3giAYbK^eg:mT%FEJZo(UMS6s8:gURQ>oec=pHhKH/thm$72X\1T$#eI2oQ-.(q!m^iAr4q>JcrrNUjQNI~>endstream
endobj
xref
0 20
0000000000 65535 f
0000000061 00000 n
0000000132 00000 n
0000000239 00000 n
0000000348 00000 n
0000000458 00000 n
0000000566 00000 n
0000000771 00000 n
0000000885 00000 n
0000001090 00000 n
0000001295 00000 n
0000001501 00000 n
0000001707 00000 n
0000001777 00000 n
0000002088 00000 n
0000002174 00000 n
0000005052 00000 n
0000008159 00000 n
0000010910 00000 n
0000013489 00000 n
trailer
<<
/ID
[<e39ac81b70f6b5dd3bd1a9be5a60bf19><e39ac81b70f6b5dd3bd1a9be5a60bf19>]
% ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R
/Root 12 0 R
/Size 20
>>
startxref
16001
%%EOF

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 KiB

View file

@ -1,444 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>42 — Друзья Коллективного Разума</title>
<meta name="description" content="Кооператив «Друзья Коллективного Разума» — мы строим автономные компании на AI">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--paper: #faf8f3;
--surface: #ffffff;
--ink: #1a1a18;
--ink2: #1a1a18;
--mut: #55534b;
--dim: #8a877b;
--vio: #5b4be0;
--line: rgba(26,26,24,.12);
--line2: rgba(26,26,24,.07);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--paper);
color: var(--ink);
line-height: 1.6;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
::selection { background: var(--vio); color: #fff; }
.wrap { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
.mono { font-family: 'JetBrains Mono', monospace; }
/* ── nav ── */
nav {
position: fixed; top: 0; width: 100%; z-index: 100;
background: rgba(250,248,243,.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
}
nav .wrap { display: flex; justify-content: space-between; align-items: center; height: 64px; }
.logo { font-size: 1.5rem; font-weight: 900; letter-spacing: -.04em; color: var(--ink); }
.logo em { font-style: normal; color: var(--vio); }
.logo span { color: var(--mut); font-weight: 500; font-size: .95rem; margin-left: 10px; }
.nav-links { display: flex; gap: 32px; align-items: center; }
.nav-links a {
color: var(--mut); text-decoration: none; font-size: .85rem; font-weight: 600;
transition: color .15s;
}
.nav-links a:hover { color: var(--ink); }
.lang-switch {
font-family: 'JetBrains Mono', monospace; font-size: .75rem;
padding: 4px 10px; border: 1px solid var(--line); color: var(--mut);
text-decoration: none; letter-spacing: 1px;
}
.lang-switch:hover { border-color: var(--ink); color: var(--ink); }
.btn-portal {
padding: 9px 22px; border: 1px solid var(--ink); border-radius: 0;
font-size: .85rem; font-weight: 600;
background: var(--ink); color: var(--paper);
transition: background .15s, color .15s;
}
.btn-portal:hover { background: var(--vio); border-color: var(--vio); }
/* ── hero ── */
.hero {
min-height: 100svh; display: flex; flex-direction: column;
justify-content: center;
padding: 140px 0 80px;
border-bottom: 1px solid var(--line);
position: relative;
}
.hero-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 64px; align-items: start; }
.hero h1 {
font-size: clamp(2.6rem, 6vw, 5rem); font-weight: 900; line-height: 1.02;
letter-spacing: -.035em; margin: 24px 0 28px;
}
.hero h1 em { font-style: normal; color: var(--vio); }
.hero .sub { font-size: 1.15rem; color: var(--mut); max-width: 560px; margin-bottom: 36px; }
.hero-cta { display: flex; gap: 16px; flex-wrap: wrap; }
.btn-lg {
display: inline-block; padding: 16px 32px;
border: 1px solid var(--ink); background: var(--ink); color: var(--paper);
font-weight: 700; font-size: .95rem; text-decoration: none;
transition: background .15s, border-color .15s;
}
.btn-lg:hover { background: var(--vio); border-color: var(--vio); }
.btn-ghost {
display: inline-block; padding: 16px 32px;
border: 1px solid var(--line); background: transparent; color: var(--ink);
font-weight: 600; font-size: .95rem; text-decoration: none;
transition: border-color .15s;
}
.btn-ghost:hover { border-color: var(--ink); }
/* hero facts panel */
.facts {
border: 1px solid var(--line); background: var(--surface);
}
.facts .hd {
display: flex; justify-content: space-between; align-items: center;
padding: 14px 20px; border-bottom: 1px solid var(--line);
font-family: 'JetBrains Mono', monospace; font-size: 11px;
letter-spacing: 1.5px; text-transform: uppercase; color: var(--dim);
}
.facts .hd b { color: var(--vio); font-weight: 500; display: flex; align-items: center; gap: 7px; }
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--vio); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
.facts .row {
display: flex; justify-content: space-between; align-items: baseline;
padding: 15px 20px; border-bottom: 1px solid var(--line2);
}
.facts .row:last-child { border-bottom: none; }
.facts .k { font-size: .8rem; color: var(--mut); }
.facts .v { font-size: 1.1rem; font-weight: 700; letter-spacing: -.02em; }
.facts .v em { font-style: normal; color: var(--vio); }
/* section scaffolding */
section { padding: 96px 0; }
.sec-label {
font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 2px;
text-transform: uppercase; color: var(--vio);
display: flex; align-items: center; gap: 12px; margin-bottom: 20px;
}
.sec-label::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.sec-h { font-size: clamp(1.9rem, 4vw, 3rem); font-weight: 900; letter-spacing: -.03em; line-height: 1.05; margin-bottom: 16px; }
.sec-desc { font-size: 1.05rem; color: var(--mut); max-width: 640px; margin-bottom: 48px; }
/* ── mission ── */
.mission { border-top: 1px solid var(--line); }
.mission-grid { display: grid; grid-template-columns: .4fr 1fr; gap: 48px; align-items: start; }
.mission-num {
font-family: 'JetBrains Mono', monospace;
font-size: clamp(4rem, 10vw, 7.5rem); font-weight: 500; line-height: .9;
color: var(--vio);
}
.mission p { font-size: 1.25rem; line-height: 1.65; color: var(--ink); max-width: 720px; }
.mission p + p { margin-top: 16px; }
.mission .mono-note {
margin-top: 24px; font-family: 'JetBrains Mono', monospace;
font-size: .8rem; color: dim;
}
.mission .mono-note { color: var(--dim); }
/* ── board: spec sheet rows ── */
.board .rows { border-top: 1px solid var(--line); }
.brow {
display: grid; grid-template-columns: 64px 1.1fr 1fr 2fr;
gap: 24px; padding: 28px 0; border-bottom: 1px solid var(--line);
align-items: baseline;
}
.brow .idx { font-family: 'JetBrains Mono', monospace; color: var(--dim); font-size: .85rem; }
.brow .nm { font-size: 1.25rem; font-weight: 800; letter-spacing: -.02em; }
.brow .rl { font-family: 'JetBrains Mono', monospace; font-size: .75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--vio); }
.brow .ds { color: var(--mut); font-size: .95rem; }
/* ── projects: ledger grid ── */
.pgrid { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--line); border-left: 1px solid var(--line); }
.pcard {
position: relative; padding: 32px; min-height: 220px;
border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
background: var(--surface); text-decoration: none; color: inherit;
display: flex; flex-direction: column; gap: 10px;
transition: background .15s;
}
.pcard:hover { background: #f4f1e8; }
.pcard .pnum { font-family: 'JetBrains Mono', monospace; font-size: .75rem; color: var(--dim); }
.pcard .pname { font-size: 1.3rem; font-weight: 800; letter-spacing: -.02em; }
.pcard .pown { font-family: 'JetBrains Mono', monospace; font-size: .72rem; color: var(--vio); text-transform: uppercase; letter-spacing: 1px; }
.pcard .pdesc { font-size: .88rem; color: var(--mut); line-height: 1.55; flex: 1; }
.pcard .plink { font-size: .8rem; font-weight: 600; color: var(--ink); }
.pcard .plink::after { content: ' →'; color: var(--vio); }
.pcard .arrow-corner {
position: absolute; top: 18px; right: 18px; opacity: 0;
transition: opacity .15s; font-size: 1.1rem; color: var(--vio);
}
.pcard:hover .arrow-corner { opacity: 1; }
/* ── CTA: ink block ── */
.cta { background: var(--ink); color: var(--paper); padding: 110px 0; }
.cta .sec-label { color: #b9b6a8; }
.cta .sec-label::after { background: rgba(250,248,243,.2); }
.cta h2 { font-size: clamp(2.2rem, 5vw, 3.8rem); font-weight: 900; letter-spacing: -.03em; line-height: 1.05; margin-bottom: 20px; }
.cta p { color: #b9b6a8; font-size: 1.1rem; margin-bottom: 40px; }
.cta .btn-lg { background: var(--paper); color: var(--ink); border-color: var(--paper); }
.cta .btn-lg:hover { background: var(--vio); color: #fff; border-color: var(--vio); }
/* ── footer ── */
footer { padding: 36px 0; border-top: 1px solid var(--line); }
footer .wrap {
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 12px;
}
footer .f-links { display: flex; gap: 24px; flex-wrap: wrap; }
footer a { color: var(--mut); text-decoration: none; font-size: .85rem; }
footer a:hover { color: var(--ink); }
/* ── payment logos strip ── */
.pay-strip { margin-top: 28px; padding-top: 24px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
.pay-strip .pl { height: 30px; display: flex; align-items: center; }
.pay-strip .pl img { max-height: 30px; width: auto; opacity: .9; }
.pay-strip .pl span { font-size: .78rem; color: var(--mut); letter-spacing: .02em; }
/* ── requisites block ── */
.requisites { font-size: .85rem; color: var(--mut); line-height: 1.7; margin-top: 22px; }
.requisites b { color: var(--ink); font-weight: 600; }
/* ── legal/compliance pages ── */
.legal-page .wrap { max-width: 860px; }
.legal-page h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); margin: 18px 0 8px; }
.legal-page .upd { color: var(--mut); font-size: .85rem; margin-bottom: 28px; }
.legal-page h2 { font-size: 1.15rem; margin: 30px 0 10px; }
.legal-page p, .legal-page li { color: var(--ink); font-size: .95rem; line-height: 1.75; }
.legal-page ul { padding-left: 22px; margin: 10px 0; }
.legal-page table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: .9rem; }
.legal-page th { text-align: left; padding: 10px 12px; border-bottom: 2px solid var(--ink); font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--mut); }
.legal-page td { padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
.legal-page .price-by { font-weight: 700; white-space: nowrap; }
footer .cop { color: var(--dim); font-size: .8rem; font-family: 'JetBrains Mono', monospace; }
/* reveal animation */
.rv { opacity: 0; transform: translateY(16px); transition: opacity .6s ease, transform .6s ease; }
.rv.vis { opacity: 1; transform: none; }
/* Mobile */
@media (max-width: 900px) {
.hero-grid { grid-template-columns: 1fr; }
.mission-grid { grid-template-columns: 1fr; gap: 24px; }
.pgrid { grid-template-columns: 1fr; }
.brow { grid-template-columns: 1fr 2fr; row-gap: 4px; }
.brow .idx { display: none; }
.nav-links a:not(.btn-portal) { display: none; }
section { padding: 64px 0; }
}
</style>
</head>
<body>
<nav>
<div class="wrap">
<div class="logo">4<em>2</em><span>Друзья Коллективного Разума</span></div>
<div class="nav-links">
<a href="#mission">Миссия</a>
<a href="#board">Правление</a>
<a href="#projects">Проекты</a>
<a href="/en" class="lang-switch" hreflang="en" title="English version">EN</a>
<a href="/cabinet" class="btn-portal" id="portalBtn">Войти →</a>
</div>
</div>
</nav>
<!-- Hero -->
<div class="hero">
<div class="wrap hero-grid">
<div>
<div class="sec-label">Кооператив · с 16.07.2026 · Минск</div>
<h1>Люди решают.<br>Агенты <em>работают.</em></h1>
<p class="sub">Мы строим автоматизированные компании, где ИИ-агенты выполняют рутинные операции, а люди держат суждение и ответственность. Прозрачные процессы, измеримый результат.</p>
<div class="hero-cta">
<a href="/cabinet" class="btn-lg" id="heroBtn">Войти в портал</a>
<a href="#projects" class="btn-ghost">Смотреть проекты</a>
</div>
</div>
<aside class="facts">
<div class="hd"><span>КОЛЛЕКТИВ / СЕЙЧАС</span><b><span class="dot"></span>WORKING</b></div>
<div class="row"><span class="k">Членов правления</span><span class="v">3</span></div>
<div class="row"><span class="k">Проектов в работе</span><span class="v">6</span></div>
<div class="row"><span class="k">AI-агентов в флоте</span><span class="v">7<em>+</em></span></div>
<div class="row"><span class="k">Серверов</span><span class="v">6</span></div>
<div class="row"><span class="k">Формат</span><span class="v">Кооператив</span></div>
<div class="row"><span class="k">Юрисдикция</span><span class="v">BY</span></div>
</aside>
</div>
</div>
<!-- Mission -->
<section id="mission" class="mission">
<div class="wrap">
<div class="sec-label">01 / Миссия</div>
<div class="mission-grid rv">
<div class="mission-num">42</div>
<div>
<p>Все миссии одинаково важны. 42 объединяет их все — это ответ на главный вопрос жизни, вселенной и всего такого.</p>
<p>Каждый участник преследует свою миссию, а коллектив усиливает каждого. Нет неважных миссий и нет неважных людей.</p>
<div class="mono-note">// статус: идём с 16.07.2026, набор — раз в квартал</div>
</div>
</div>
</div>
</section>
<!-- Board -->
<section id="board" class="board">
<div class="wrap">
<div class="sec-label">02 / Правление</div>
<h2 class="sec-h rv">Трое у штурвала</h2>
<p class="sec-desc rv">Консенсус правления — механизм принятия решений кооператива.</p>
<div class="rows rv">
<div class="brow">
<div class="idx">001</div>
<div class="nm">Алекс Шкор</div>
<div class="rl">Председатель</div>
<div class="ds">Архитектор кооператива. Симбиот, Кибербанк, Domostroy. AI-инфраструктура и стратегия.</div>
</div>
<div class="brow">
<div class="idx">002</div>
<div class="nm">Влад Шемардинов</div>
<div class="rl">Член правления</div>
<div class="ds">Видеопродакшн и контент. ReelsUp: подкасты и видео превращаются в вирусные рилсы.</div>
</div>
<div class="brow">
<div class="idx">003</div>
<div class="nm">Юра Бокач</div>
<div class="rl">Секретарь</div>
<div class="ds">Монетикс, финансовая архитектура, документооборот. Залоговые системы и аналитика.</div>
</div>
</div>
</div>
</section>
<!-- Projects -->
<section id="projects">
<div class="wrap">
<div class="sec-label">03 / Проекты</div>
<h2 class="sec-h rv">Что мы строим</h2>
<p class="sec-desc rv">Каждый проект — отдельный бизнес внутри кооператива. Доля кооператива — 50%.</p>
<div class="pgrid rv">
<a href="https://simbiot.cil.by" class="pcard">
<div class="pnum">P-01</div>
<div class="pname">Симбиот</div>
<div class="pown">Alex Shkor</div>
<div class="pdesc">Автопилот для бизнеса. Рой AI-агентов ведёт операции — маркетинг, финансы, поддержка. 24/7 без людей.</div>
<div class="plink">simbiot.cil.by</div>
<div class="arrow-corner">↗</div>
</a>
<a href="https://reelsup1.cil.by" class="pcard">
<div class="pnum">P-02</div>
<div class="pname">ReelsUp</div>
<div class="pown">Vlad Shemardinov</div>
<div class="pdesc">Твой подкаст уже содержит вирусные клипы. AI находит, нарезает и публикует короткие видео автоматически.</div>
<div class="plink">reelsup1.cil.by</div>
<div class="arrow-corner">↗</div>
</a>
<div class="pcard" aria-hidden="false">
<div class="pnum">P-03</div>
<div class="pname">Монетикс</div>
<div class="pown">Yura Bokach</div>
<div class="pdesc">Финансовые инструменты и аналитика на базе AI. Развитие человеческого потенциала через ИИ.</div>
<div class="plink">В разработке</div>
<div class="arrow-corner">·</div>
</div>
<div class="pcard" aria-hidden="false">
<div class="pnum">P-04</div>
<div class="pname">Кибербанк</div>
<div class="pown">Alex Shkor</div>
<div class="pdesc">Внутренняя система учёта и взаиморасчётов кооператива: финансовая модель проектов, скоринг, прозрачная отчётность для пайщиков.</div>
<div class="plink">В разработке</div>
<div class="arrow-corner">·</div>
</div>
<div class="pcard" aria-hidden="false">
<div class="pnum">P-05</div>
<div class="pname">Domostroy</div>
<div class="pown">Alex Shkor</div>
<div class="plink">В разработке</div>
<div class="pdesc">ИИ-агент, который строит дома в любой точке мира. От проекта до реализации — полностью автоматизировано.</div>
<div class="arrow-corner">·</div>
</div>
<div class="pcard" aria-hidden="false">
<div class="pnum">P-06</div>
<div class="pname">AI Corp</div>
<div class="pown">Dmitry Siasitski</div>
<div class="pdesc">Консалтинг по внедрению AI в компаниях. От аудита до развёртывания автономных агентов.</div>
<div class="plink">В разработке</div>
<div class="arrow-corner">·</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="cta">
<div class="wrap">
<div class="sec-label">04 / Вход</div>
<h2 class="rv">Готовы строить будущее?</h2>
<p class="rv">Войдите в портал кооператива или свяжитесь с правлением.</p>
<a href="/login" class="btn-lg rv" id="ctaBtn">Войти в портал</a>
</div>
</section>
<footer>
<div class="wrap">
<span class="cop">© 2026 Кооператив «42» · cil.by</span>
<div class="f-links">
<a href="/cabinet">Портал</a>
<a href="/en">EN</a>
<a href="/services">Услуги и цены</a>
<a href="/terms">Правила оплаты и возврата</a>
<a href="https://simbiot.cil.by">Симбиот</a>
<a href="https://reelsup1.cil.by">ReelsUp</a>
</div>
<div class="requisites">
<b>Потребительский кооператив «ДРУЗЬЯ КОЛЛЕКТИВНОГО РАЗУМА»</b><br>
Свидетельство о государственной регистрации: дата регистрации 16.07.2026, состоит на учёте в ИМНС по Фрунзенскому району г. Минска (УНП 194018234)<br>
Юридический адрес: 220078, Республика Беларусь, г. Минск, ул. Маршала Лосика, д. 53, оф. 385<br>
УНП 194018234 · Тел.: +375 25 900 50 03 · alex.shkor@gmail.com<br>
ЗАО «Альфа-Банк» · БИК ALFABY2X · Карт-счёт BY85ALFA30152K15980010270000 (BYN)
</div>
<div class="pay-strip">
<span class="pl"><img src="/static/img/pay/visa.svg" alt="VISA" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/visa-secure.svg" alt="Visa Secure" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/mastercard.svg" alt="MasterCard" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/mc-id-check.svg" alt="MasterCard ID Check" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/belkart.svg" alt="Белкарт" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/belkart-ip.svg" alt="Белкарт ИнтернетПароль" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/applepay.svg" alt="Apple Pay" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/samsungpay.svg" alt="Samsung Pay" loading="lazy"></span>
<span class="pl"><img src="/static/img/pay/alfabank.svg" alt="Альфа-Банк" loading="lazy"></span>
</div>
</div>
</footer>
<script>
// logged-in? → switch buttons to cabinet
fetch('/api/me').then(r=>{if(r.ok){return r.json()}}).then(d=>{
if(d&&d.user){
const btn=document.getElementById('portalBtn');
if(btn){btn.href='/cabinet';btn.textContent='Кабинет →';}
const h=document.getElementById('heroBtn');
if(h){h.href='/cabinet';h.textContent='Открыть кабинет';}
const c=document.getElementById('ctaBtn');
if(c){c.href='/cabinet';c.textContent='Открыть кабинет';}
}
}).catch(()=>{});
// reveal on scroll
const io = new IntersectionObserver(es => es.forEach(e => e.isIntersecting && e.target.classList.add('vis')), {threshold:.1});
document.querySelectorAll('.rv').forEach(el => io.observe(el));
</script>
</body>
</html>

View file

@ -1,598 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simbiot — Ритмичный ИИ-харнес кооператива 42</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--accent: #2563eb;
--bg-light: #ffffff;
--bg-dark: #1e293b;
--text-light: #1e293b;
--text-dark: #f8fafc;
--gray: #64748b;
--border: #e2e8f0;
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: var(--text-light);
background: var(--bg-light);
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 20px;
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
opacity: 0;
animation: fadeInUp 0.8s ease-out forwards;
}
.delay-1 { animation-delay: 0.2s; }
.delay-2 { animation-delay: 0.4s; }
.delay-3 { animation-delay: 0.6s; }
/* Typography */
h1, h2, h3 {
font-family: Georgia, serif;
font-weight: 600;
line-height: 1.2;
}
h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
}
h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.section {
padding: 80px 0;
}
.section:nth-child(even) {
background: var(--bg-dark);
color: var(--text-dark);
}
.btn {
display: inline-block;
padding: 12px 24px;
background: var(--accent);
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: background 0.3s ease;
}
.btn:hover {
background: #1d4ed8;
}
/* Hero Section */
.hero {
text-align: center;
padding: 120px 0;
}
.hero h1 {
font-size: 2.2rem;
margin-bottom: 1rem;
}
.hero p {
font-size: 1.2rem;
margin-bottom: 2rem;
color: var(--gray);
}
/* Problem/Solution Blocks */
.problem-solution {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 40px;
margin-top: 60px;
}
.block {
padding: 30px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.block h3 {
color: var(--accent);
margin-bottom: 1rem;
}
.block p {
margin-bottom: 1rem;
}
/* What's Included */
.included-list {
list-style: none;
margin-top: 40px;
}
.included-list li {
padding: 15px 0;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
}
.included-list li:before {
content: "✓";
color: var(--accent);
font-weight: bold;
margin-right: 15px;
font-size: 1.2rem;
}
/* How It Works */
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
margin-top: 60px;
}
.step {
text-align: center;
padding: 30px;
background: rgba(37, 99, 235, 0.05);
border-radius: 8px;
border: 1px solid rgba(37, 99, 235, 0.1);
}
.step-number {
width: 50px;
height: 50px;
background: var(--accent);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
margin: 0 auto 20px;
}
/* Mockups */
.mockup {
margin: 40px 0;
padding: 30px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.dashboard {
background: #f8fafc;
padding: 20px;
border-radius: 8px;
font-family: monospace;
font-size: 0.9rem;
}
.dashboard-header {
background: var(--accent);
color: white;
padding: 10px 15px;
border-radius: 6px 6px 0 0;
font-weight: bold;
}
.dashboard-content {
padding: 15px;
}
.task-item {
padding: 10px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
}
.task-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.status-pending { background: #fef3c7; color: #92400e; }
.status-progress { background: #dbeafe; color: #1e40af; }
.status-done { background: #d1fae5; color: #065f46; }
.telegram-chat {
background: #f8fafc;
padding: 20px;
border-radius: 8px;
}
.message {
margin: 10px 0;
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
position: relative;
}
.message.user {
background: var(--accent);
color: white;
margin-left: auto;
border-bottom-right-radius: 4px;
}
.message.bot {
background: #e2e8f0;
color: var(--text-light);
border-bottom-left-radius: 4px;
}
/* Social Proof */
.social-proof {
text-align: center;
padding: 40px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 30px;
margin-top: 40px;
}
.stat {
padding: 20px;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: var(--accent);
}
/* Pricing */
.pricing {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 60px;
}
.plan {
padding: 40px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
}
.plan h3 {
margin-bottom: 20px;
color: var(--accent);
}
.plan-price {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 20px;
}
.plan-features {
list-style: none;
margin-bottom: 30px;
}
.plan-features li {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* Footer */
.footer {
background: var(--bg-dark);
color: var(--text-dark);
padding: 40px 0;
text-align: center;
}
.footer-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 40px;
margin-bottom: 40px;
}
.footer-section h4 {
margin-bottom: 15px;
color: var(--accent);
}
.footer-section p, .footer-section a {
color: #94a3b8;
text-decoration: none;
}
.footer-section a:hover {
color: var(--accent);
}
/* Responsive */
@media (max-width: 768px) {
h1 { font-size: 1.8rem; }
h2 { font-size: 1.5rem; }
.section { padding: 60px 0; }
.hero { padding: 80px 0; }
}
</style>
</head>
<body>
<!-- Hero Section -->
<section class="hero section">
<div class="container fade-in-up">
<h1>Харнес превращает решения в исполненные задачи</h1>
<p>Для кооперативов, которые хотят ≥ +1% к AUM в горизонте год+</p>
<a href="#pricing" class="btn">Начать использовать Simbiot</a>
</div>
</section>
<!-- What This Is -->
<section class="section">
<div class="container">
<h2>Что это</h2>
<div class="problem-solution">
<div class="block fade-in-up delay-1">
<h3>Проблема</h3>
<p>Решения из звонков и чатов теряются, не доходят до исполнителей, важное тонет в рутине</p>
</div>
<div class="block fade-in-up delay-2">
<h3>Механизм</h3>
<p>Харнес автоматически извлекает решения из диалогов, создает задачи с четкими сроками и ответственными</p>
</div>
<div class="block fade-in-up delay-3">
<h3>Результат</h3>
<p>Важное всегда на виду, эскалации важного поднимаются наверх, AUM растет системно</p>
</div>
</div>
</div>
</section>
<!-- What's Included -->
<section class="section">
<div class="container">
<h2>Что включено</h2>
<ul class="included-list">
<li>Автоматический парсинг решений из Telegram-чатов и звонков</li>
<li>Система задач с тактами (минута/час/день) для агентов</li>
<li>Дашборд контроля исполнения в реальном времени</li>
<li>Эскалация важных задач без потери контекста</li>
<li>Интеграция с существующими системами кооператива</li>
<li>Аналитика эффективности решений и их исполнения</li>
<li>Поддержка 7 продуктов кооператива 42</li>
</ul>
</div>
</section>
<!-- How It Works -->
<section class="section">
<div class="container">
<h2>Как это работает</h2>
<div class="steps">
<div class="step fade-in-up">
<div class="step-number">1</div>
<h3>Обсуждение</h3>
<p>Вы обсуждаете решения в Telegram или звонках</p>
</div>
<div class="step fade-in-up delay-1">
<div class="step-number">2</div>
<h3>Авто-создание задач</h3>
<p>Simbiot превращает решения в задачи с ответственными</p>
</div>
<div class="step fade-in-up delay-2">
<div class="step-number">3</div>
<h3>Контроль исполнения</h3>
<p>Агенты выполняют по тактам, важное эскалируется</p>
</div>
</div>
</div>
</section>
<!-- Product Visuals -->
<section class="section">
<div class="container">
<h2>Визуалы продукта</h2>
<div class="mockup fade-in-up">
<h3>Дашборд задач</h3>
<div class="dashboard">
<div class="dashboard-header">Панель управления задачами</div>
<div class="dashboard-content">
<div class="task-item">
<span>Анализ Q3 отчетов</span>
<span class="task-status status-progress">В работе</span>
</div>
<div class="task-item">
<span>Подготовить презентацию для инвесторов</span>
<span class="task-status status-pending">Ожидание</span>
</div>
<div class="task-item">
<span>Обновить CRM систему</span>
<span class="task-status status-done">Готово</span>
</div>
<div class="task-item">
<span>Проверить compliance требования</span>
<span class="task-status status-pending">Ожидание</span>
</div>
</div>
</div>
</div>
<div class="mockup fade-in-up delay-1">
<h3>Telegram-диалог</h3>
<div class="telegram-chat">
<div class="message bot">
<strong>Simbiot:</strong> Обнаружено решение: "Запланировать встречу с командой по стратегии Q4"
</div>
<div class="message user">
<strong>Вы:</strong> Подтверждаю, создай задачу
</div>
<div class="message bot">
<strong>Simbiot:</strong> Задача создана: Встреча по стратегии Q4, ответственный: Вы, срок: 25.10.2024
</div>
</div>
</div>
</div>
</section>
<!-- Social Proof -->
<section class="section">
<div class="container">
<h2>Соцдоказательство</h2>
<div class="social-proof">
<p>Кооператив 42 «Друзья Коллективного Разума»</p>
<p>Юрлицо с 2026 года, Минск</p>
<p>7 продуктов под управлением, сайт cil.by, Telegram t.me/cil_by</p>
<div class="stats">
<div class="stat">
<div class="stat-number">2026</div>
<div>Год основания</div>
</div>
<div class="stat">
<div class="stat-number">7</div>
<div>Активных продуктов</div>
</div>
<div class="stat">
<div class="stat-number">≥+1%</div>
<div>Рост AUM в год+</div>
</div>
</div>
</div>
</div>
</section>
<!-- Pricing -->
<section class="section" id="pricing">
<div class="container">
<h2>Тарифные планы</h2>
<div class="pricing">
<div class="plan fade-in-up">
<h3>Старт</h3>
<div class="plan-price">₽15к/мес</div>
<ul class="plan-features">
<li>До 5 пользователей</li>
<li>1 Telegram-чат</li>
<li>Базовый дашборд</li>
<li>Стандартная поддержка</li>
</ul>
<a href="#" class="btn">Начать</a>
</div>
<div class="plan fade-in-up delay-1">
<h3>Про</h3>
<div class="plan-price">₽35к/мес</div>
<ul class="plan-features">
<li>До 20 пользователей</li>
<li>5 Telegram-чатов</li>
<li>Продвинутый дашборд</li>
<li>Приоритетная поддержка</li>
<li>API интеграции</li>
</ul>
<a href="#" class="btn">Выбрать</a>
</div>
<div class="plan fade-in-up delay-2">
<h3>Enterprise</h3>
<div class="plan-price">По запросу</div>
<ul class="plan-features">
<li>Неограниченно пользователей</li>
<li>Неограниченно чатов</li>
<li>Полный функционал</li>
<li>Персональный менеджер</li>
<li>Кастомные интеграции</li>
</ul>
<a href="#" class="btn">Контакты</a>
</div>
</div>
</div>
</section>
<!-- Final CTA -->
<section class="hero section">
<div class="container fade-in-up">
<h1>Харнес превращает решения в исполненные задачи</h1>
<p>Для кооперативов, которые хотят ≥ +1% к AUM в горизонте год+</p>
<a href="#pricing" class="btn">Начать использовать Simbiot</a>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h4>Кто делает</h4>
<p>Кооператив 42 «Друзья Коллективного Разума»</p>
<p>Разработка Simbiot</p>
</div>
<div class="footer-section">
<h4>Контакты</h4>
<p>Сайт: cil.by</p>
<p>Telegram: t.me/cil_by</p>
<p>Email: info@cil.by</p>
</div>
<div class="footer-section">
<h4>Юр. информация</h4>
<p>УНП: 123456789</p>
<p>Адрес: г. Минск, ул. Примерная, 42</p>
<p>2026-2026 © Все права защищены</p>
</div>
</div>
</div>
</footer>
</body>
</html>

View file

@ -1,162 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simbiot — общий харнес кооператива 42</title>
<meta name="description" content="Simbiot — ритмичный ИИ-харнес кооператива 42: решения со звонков превращаются в исполненные задачи, агенты живут тактами.">
<style>
:root{
--ink:#101418; --paper:#f4f0e6; --tape:#e0d9c8;
--acc:#0e7a5f; --acc2:#c2571b; --line:#101418;
--mono:"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace;
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{background:var(--paper);color:var(--ink);font-family:var(--serif);line-height:1.55}
.mono{font-family:var(--mono)}
a{color:inherit}
/* ---------- шапка-таблица ---------- */
header{border-bottom:2px solid var(--line);padding:18px clamp(16px,4vw,48px);display:flex;justify-content:space-between;align-items:baseline;gap:16px;flex-wrap:wrap}
.brand{font-family:var(--mono);font-weight:700;font-size:18px;letter-spacing:.04em}
.brand i{color:var(--acc2);font-style:normal}
nav{font-family:var(--mono);font-size:13px;display:flex;gap:20px}
nav a{text-decoration:none;border-bottom:1px dotted var(--ink)}
nav a:hover{color:var(--acc2)}
/* ---------- hero: «осциллограмма» тактов ---------- */
.hero{position:relative;overflow:hidden;padding:clamp(56px,9vw,110px) clamp(16px,4vw,48px) 40px;border-bottom:2px solid var(--line)}
.hero h1{font-weight:400;font-size:clamp(38px,6.5vw,84px);line-height:1.04;letter-spacing:-.015em;max-width:14ch}
.hero h1 b{font-weight:600}
.hero p.lede{margin-top:26px;max-width:52ch;font-size:clamp(17px,1.6vw,21px)}
.tick-strip{margin-top:48px;display:flex;align-items:flex-end;gap:3px;height:64px}
.tick-strip span{flex:1;background:var(--ink);min-width:2px;animation:pulse 2.4s ease-in-out infinite}
@keyframes pulse{0%,100%{transform:scaleY(.35);opacity:.35}50%{transform:scaleY(1);opacity:1}}
.tick-strip span:nth-child(odd){background:var(--acc)}
.tick-label{font-family:var(--mono);font-size:12px;margin-top:10px;color:#555}
.cta{margin-top:40px;display:flex;gap:14px;flex-wrap:wrap}
.btn{font-family:var(--mono);font-size:15px;text-decoration:none;padding:14px 26px;border:2px solid var(--line)}
.btn.solid{background:var(--acc);color:#fff;border-color:var(--acc)}
.btn:hover{transform:translate(-2px,-2px);box-shadow:3px 3px 0 var(--line)}
.btn.solid:hover{box-shadow:3px 3px 0 var(--acc2)}
/* ---------- секции ---------- */
section{padding:clamp(44px,6vw,80px) clamp(16px,4vw,48px);border-bottom:1px solid #c9c2b0}
h2{font-weight:400;font-size:clamp(26px,3.4vw,40px);letter-spacing:-.01em;margin-bottom:28px}
h2 em{font-style:italic;color:var(--acc2)}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:0;border:2px solid var(--line)}
.card{padding:26px 24px 30px;border-right:1px solid var(--line);position:relative}
.card:last-child{border-right:none}
.card .n{font-family:var(--mono);font-size:12px;color:var(--acc2);display:block;margin-bottom:14px}
.card h3{font-weight:600;font-size:19px;margin-bottom:10px}
.card p{font-size:15.5px;color:#33301f}
/* поток решения */
.flow{font-family:var(--mono);font-size:14px;display:flex;flex-wrap:wrap;gap:10px 0;align-items:center}
.flow .step{border:1.5px solid var(--ink);padding:10px 16px;background:#fff8}
.flow .step b{color:var(--acc)}
.flow .arw{padding:0 12px;color:var(--acc2)}
.numbers{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:32px;font-family:var(--mono)}
.numbers .v{font-size:clamp(30px,4vw,46px);font-weight:700}
.numbers .v.a{color:var(--acc)} .numbers .v.b{color:var(--acc2)}
.numbers .c{font-size:13px;color:#555;margin-top:6px;max-width:24ch}
blockquote{border-left:4px solid var(--acc);padding:6px 0 6px 22px;font-size:clamp(19px,2.2vw,26px);font-style:italic;max-width:46ch}
footer{padding:36px clamp(16px,4vw,48px);font-family:var(--mono);font-size:13px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;background:var(--ink);color:var(--paper)}
footer a{color:#8fd6bd}
@media (prefers-reduced-motion:reduce){.tick-strip span{animation:none;transform:scaleY(.6);opacity:.7}.btn:hover{transform:none;box-shadow:none}}
</style>
</head>
<body>
<header>
<div class="brand">simbiot<i>·42</i></div>
<nav>
<a href="#what">что это</a>
<a href="#how">как работает</a>
<a href="#numbers">цифры</a>
<a href="#join">участие</a>
</nav>
</header>
<section class="hero" style="border:none;padding-bottom:56px">
<h1>Компания, которая <b>думает тактами</b>, а не совещаниями</h1>
<p class="lede">Simbiot — ритмичный ИИ-харнес кооператива 42. Решение, зафиксированное на звонке, к утру превращается в выполненную задачу с проверяемым результатом. Агенты живут тактами, копят память и отчитываются — как люди, только без выходных.</p>
<div class="tick-strip" aria-hidden="true">
<span style="animation-delay:0s"></span><span style="animation-delay:.2s"></span><span style="animation-delay:.4s"></span><span style="animation-delay:.6s"></span><span style="animation-delay:.8s"></span><span style="animation-delay:1s"></span><span style="animation-delay:1.2s"></span><span style="animation-delay:1.4s"></span><span style="animation-delay:1.6s"></span><span style="animation-delay:1.8s"></span><span style="animation-delay:2s"></span><span style="animation-delay:2.2s"></span>
</div>
<div class="tick-label mono">каждая полоска — один такт агента · минутный пульс → часовой → дневной</div>
<div class="cta">
<a class="btn solid" href="https://cil.by">Зайти в портал</a>
<a class="btn" href="#how">Как это работает ↓</a>
</div>
</section>
<section id="what">
<h2>Не чат-бот. <em>Операционная система</em> студии</h2>
<div class="cards">
<div class="card">
<span class="n">01 / симбиоз</span>
<h3>Общий харнес, а не личный</h3>
<p>Разработчики строят личные харнесы и уносят их с собой. Simbiot — харнес всей организации: нулевая точка воздействия, он умеет переписывать сам себя.</p>
</div>
<div class="card">
<span class="n">02 / координация</span>
<h3>Интегрирует открытое</h3>
<p>Координатор харнесов: встраивает открытые системы (Hermes, OpenClaw) и свои скиллы в один контур исполнения.</p>
</div>
<div class="card">
<span class="n">03 / память</span>
<h3>CQRS + Event Sourcing</h3>
<p>Каждое решение — событие. История не теряется, любой такт можно переиграть. База знаний живая: протоколы встреч, договоры, проекты.</p>
</div>
<div class="card">
<span class="n">04 / эскалация</span>
<h3>Уровни, а не один мозг</h3>
<p>Минутный пульс отвечает в чате, часовой и дневной — берут сложное. Что не по зубам уровню, уходит наверх с контекстом.</p>
</div>
</div>
</section>
<section id="how" style="background:#fff6; ">
<h2>От голоса — <em>к результату</em></h2>
<div class="flow">
<span class="step">звонок → <b>протокол решений</b></span><span class="arw">→</span>
<span class="step">задача на доске с <b>критерием готовности</b></span><span class="arw">→</span>
<span class="step">агент исполняет <b>такт за тактом</b></span><span class="arw">→</span>
<span class="step">результат в чат, <b>метрика AUM</b> растёт</span>
</div>
<blockquote style="margin-top:44px">Фильтр любого решения: добавляет ли оно +1% к AUM в горизонте год и больше?</blockquote>
</section>
<section id="numbers">
<h2>Цифры, без глянца</h2>
<div class="numbers">
<div><div class="v a">7</div><div class="c">продуктов в студии, живые деньги — уже у двух</div></div>
<div><div class="v b">42</div><div class="c">кооператив «Друзья Коллективного Разума» — юрлицо живое с 2026</div></div>
<div><div class="v">24/7</div><div class="c">пульс: минутный, часовой и дневной уровни агентов</div></div>
<div><div class="v a">100%</div><div class="c">уходя, участник забирает свой проект — база не разваливается</div></div>
</div>
</section>
<section id="join">
<h2>Встроиться в <em>симбиоз</em></h2>
<p style="max-width:56ch;font-size:18px">Мы берём проекты в программу 42 Labs: юрлицо и платёжный контур, студийная ОС с первого дня, плата из выручки — когда проект заработал. Или просто напишите боту в Telegram и посмотрите, как отвечает минутный пульс.</p>
<div class="cta">
<a class="btn solid" href="https://cil.by">Подать заявку на портале</a>
<a class="btn" href="https://t.me/cil_by">Написать в Telegram</a>
</div>
</section>
<footer>
<div>кооператив 42 · «Друзья Коллективного Разума» · Минск</div>
<div><a href="https://cil.by">cil.by</a> · собрано харнесом Simbiot</div>
</footer>
</body>
</html>

View file

@ -1,91 +0,0 @@
# MiroFish (github 666ghj/MiroFish): разбор и применимость — 2026-10-04
Задача: t-261004062405-fa9926. Метод: клон репо (depth 1), разбор кода backend
(58 py-файлов, ~17.2k строк) + README. Версия зависимостей: camel-oasis==0.2.5,
zep-cloud==3.25.0, flask>=3.0, openai>=1.0.
## Вердикт
**Как движок — НЕ берём. Как источник паттернов — да, 3-5 идей.**
Причины против движка: (1) стоимость — README сам пишет «High consumption,
попробуйте сначала <40 раундов»; (2) жёсткая привязка к Zep Cloud (внешний
SaaS-граф памяти, счёт отдельно от LLM) и OASIS/camel-ai; (3) китайский
дефолт-стек (qwen-plus через dashscope) — у нас 9router; (4) это продукт-платформа
(Flask 5001 + React 3000 + Docker), а не библиотека — интеграция = second brain.
## Архитектура (проверено по коду)
Пайплайн: **Graph Building → Environment Setup → Simulation → Report → Interaction**
1. **Graph Building** (`graph_builder.py` 879 строк, `ontology_generator.py` 732):
seed-материалы (новости/политики/финанс.сигналы, PDF/текст) → извлечение
сущностей и связей → GraphRAG-граф в Zep (онтология генерится LLM).
2. **Environment Setup** (`oasis_profile_generator.py` 1248): из сущностей Zep
LLM-ом генерятся персоны агентов — индивидуальные и групповые
(`generate_profile_from_entity` → `_generate_profile_with_llm`, JSON-схема,
username, bio, persona-текст) → конфиг агентов инжектится в OASIS.
3. **Simulation** (`simulation_runner.py` 2033, `run_parallel_simulation.py` 1699,
twitter/reddit скрипты): движок **OASIS (camel-ai)** — двухплатформенная
параллельная симуляция соцсети (Twitter+Reddit). Раунды = часы×60/минут_на_раунд,
есть max_rounds-обрезка (контроль стоимости). Каждый агент за раунд —
действие через `LLMAction` (≥1 LLM-вызов на активного агента за раунд).
Память: каждое действие → episode-текст → динамическое обновление Zep-графа
(`zep_graph_memory_updater.py` 791: описатели create_post/like/repost/
comment/follow/search).
4. **Report** (`report_agent.py` 2619 — самый большой модуль): ReportAgent с 4
инструментами — `insight_forge`, `panorama_search`, `quick_search`,
`interview_agents` (интервьюирует агентов мира post-factum); протокол
`<tool_call>{...}</tool_call>`, трекер «не все инструменты использованы»
дожимает агент до полного отчёта.
5. **Interaction**: чат с любым агентом мира + ReportAgent (фронт).
Память = **Zep Cloud** (knowledge-graph + temporal): не векторная база, а граф
сущностей с би-темпоралью; free-тариф «на простое использование».
## Оценка стоимости прогона
Формула расхода: ~ `N_агентов × N_раундов` LLM-вызовов (действия) + граф/онтология
+ персоны + отчёт (десятки вызовов) + инференс Zep (граф-обновления, отдельный счёт).
- Заявленный дефолт: qwen-plus. README: «<40 раундов на старте».
- Пример: 100 агентов × 40 раундов ≈ 4–5k вызовов ≈ 15–25M входящих токенов
(контекст платформы в каждом) → **~$10–25 на qwen-plus за прогон**;
на премиум-модели класса GPT-4o — **×10–30 (сотни $)**.
- Осмысленный прогнозный прогон (500+ агентов, 100+ раундов) — **сотни $ за
прогон** даже на дешёвых моделях + Zep Cloud сверх.
- Контекст: наш burn 9router = 2291.81 у.е./7д — один-два «взрослых» прогона
MiroFish съели бы заметный кусок недельного бюджета без прямой выручки.
## Что берём (идеи заимствования)
1. **Персона-из-сущности** (`_generate_profile_with_llm`): LLM-генерация
структурированной персоны агента из записи графа знаний (JSON-схема, инди/групп).
Куда: v2 aum-pilot — вместо placeholder-модели продуктивности, агентные симуляции
решений кооператива (роли: Alex/Vlad/Yura/клиент) с сгенерированными персонами.
2. **ReportAgent-паттерн**: отчётёр = агент с узким набором инструментов
(сводка/поиск/дип-поиск/интервью) + трекер обязательного использования всех
инструментов. Куда: наш «отчёт-адвокат» в aum-pilot и часовые дайджесты —
интервью опрошенных агентов симуляции вместо голой статистики.
3. **Episode→граф памяти**: действие → компактный episode-текст → обновление
графа по ходу симуляции (не в конце). Куда: память ритмичных агентов харнеса
— но на нашем RAG (без Zep: episode → embedding → граф связей) — паттерн
повторяем, сервис не покупаем.
4. **Предпродажная симуляция спроса**: OASIS даёт готовую соц-динамику
(лайк/репост/коммент/фоллов). Идея под AUM: перед запуском лендинга/прайса —
дешёвый прогон (20–40 раундов, flash-модель, 50–100 агентов-«клиентов» из
персон сегмента) → прогноз реакции и возражений. Это прямой кандидат в
наши симуляции поведения пользователей.
5. **Автогенерация конфига из NL-запроса** (`simulation_config_generator.py`):
«что хочешь узнать» естественным языком → параметры прогона + max_rounds
(жёсткий потолок стоимости). Куда: постановка задачи → конфиг прогона
aum-pilot; потолок раундов — в наши длинные симуляции.
6. **IPC/прогресс долгих прогонов** (`simulation_ipc.py` + progress %):
отдельный процесс, структурированный прогресс, чистая обрезка. У нас уже
есть _llm_worker — позаимствовать схему прогресса/тримминга.
## Чего не берём
- Zep Cloud (внешний SaaS, счёт отдельно, данные наружу) — свой RAG/graph.
- OASIS целиком (тяжёлый camel-ai стек) — только если сделаем п.4; тогда
изолированный Docker-стенд, flash-модель через 9router, budget-cap на прогон.
- Их фронт/платформу — у нас свои каналы (портал, MCP, TG).

View file

@ -1,176 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Прокат авто × Simbiot — партнёрское соглашение v1</title>
<style>
:root{
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8; --ok:#1d7a3e; --warn:#a3690f;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
.wrap{max-width:980px;margin:0 auto;padding:56px 24px 100px}
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:36px}
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
h1{font-size:clamp(32px,5.5vw,52px);line-height:1.05;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
.sub{font-size:17px;color:var(--dim);max-width:720px}
.badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.badge{border:1px solid var(--ink);padding:5px 12px;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper)}
.badge.s{background:var(--ink);color:var(--paper)}
section{margin:52px 0 0}
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
h2{font-size:26px;font-weight:800;letter-spacing:-.01em;margin:4px 0 6px;text-transform:uppercase}
.lead{color:var(--dim);max-width:760px;margin-bottom:20px}
h3{font-size:14px;text-transform:uppercase;letter-spacing:.06em;margin:24px 0 10px;border-left:3px solid var(--signal);padding-left:10px}
p{margin:10px 0;max-width:820px}
.clause{border:1px solid var(--line);background:#fff;padding:14px 18px;margin:12px 0;max-width:860px}
.clause .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.clause h4{font-size:15px;margin:4px 0 6px;font-weight:800}
.clause p{font-size:14px;color:var(--ink);margin:4px 0}
.note{border:1px solid var(--ink);border-left:6px solid var(--signal);background:#fff;padding:14px 18px;margin:20px 0;max-width:860px}
.note b{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--signal)}
.note.ok{border-left-color:var(--ok)} .note.ok b{color:var(--ok)}
table{width:100%;border-collapse:collapse;margin:18px 0;max-width:900px;background:#fff;border:1px solid var(--ink)}
th{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;text-align:left;padding:10px 12px;background:var(--ink);color:var(--paper)}
td{padding:10px 12px;border-top:1px solid var(--line);font-size:14px;vertical-align:top}
td .mono{font-size:12.5px;color:var(--signal);font-weight:700}
ul{margin:8px 0 8px 18px;font-size:14.5px}
li{margin:6px 0}
li::marker{color:var(--signal)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:18px 0}
@media(max-width:760px){.grid2{grid-template-columns:1fr}}
.sign{border:3px double var(--ink);background:#fff;padding:22px;margin:26px 0;display:grid;grid-template-columns:1fr 1fr;gap:20px}
@media(max-width:760px){.sign{grid-template-columns:1fr}}
.sig .org{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin-bottom:14px}
.sig .line{border-bottom:1px solid var(--ink);height:38px;margin-bottom:8px}
.sig .field{font-size:12.5px;color:var(--dim)}
footer{margin-top:60px;border-top:1px solid var(--line);padding-top:18px;color:var(--dim);font-size:12.5px}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">Потребительский кооператив «Друзья Коллективного Разума» · Минск</div>
<h1>Соглашение о партнёрском вознаграждении № 1</h1>
<div class="sub">Прокат авто (Минск) × Simbiot. Договор-черновик на согласование: платформа как операционный и маркетинговый слой партнёра, вознаграждение — 10% от заработка на атрибутированных клиентах.</div>
<div class="badges">
<span class="badge s">Черновик v1 · 04.10.2026</span>
<span class="badge">10% от нетто-выручки</span>
<span class="badge">Ставка фиксирована 12 мес</span>
<span class="badge">Атрибуция — 24 мес</span>
</div>
</header>
<section>
<div class="snum">00 / СТОРОНЫ</div>
<h2>Стороны соглашения</h2>
<div class="grid2">
<div class="clause"><div class="cn">ОПЕРАТОР ПЛАТФОРМЫ</div><h4>Кооператив</h4><p>[полное наименование кооператива 42], г. Минск — оператор платформы Simbiot (агентские, маркетинговые и операционные AI-сервисы).</p></div>
<div class="clause"><div class="cn">ПАРТНЁР</div><h4>Прокатная компания</h4><p>[наименование юрлица/ИП], г. Минск — владелец автопарка, передаёт Platform привлечение клиентов и операционное управление частью бизнеса.</p></div>
</div>
<div class="note ok"><b>Разделение документов ·</b> Этот документ — только про партнёрское вознаграждение и рост выручки. Исторический долг Партнёра (~15 000 USD) вынесен в отдельный Side Letter № 1 — подписывается параллельно, на условия этого соглашения не влияет.</div>
</section>
<section>
<div class="snum">01 / ПРЕДМЕТ</div>
<h2>Предмет соглашения</h2>
<div class="clause"><div class="cn">1.1</div><p>Оператор предоставляет Партнёру доступ к Платформе Simbiot (агентские, маркетинговые и операционные AI-сервисы) для привлечения клиентов и управления автопарком Партнёра.</p></div>
<div class="clause"><div class="cn">1.2</div><p>За это Партнёр уплачивает Оператору <b>партнёрское вознаграждение в размере 10%</b> от заработка по каждому <b>Атрибутированному клиенту</b> (раздел 02).</p></div>
</section>
<section>
<div class="snum">02 / АТРИБУЦИЯ</div>
<h2>Кто считается атрибутированным клиентом</h2>
<p class="lead">Три категории. Фиксация — записью в Реестре атрибуций (Приложение № 1): ведёт Оператор, Партнёру — доступ на чтение.</p>
<table>
<tr><th style="width:80px">Категория</th><th>Определение</th></tr>
<tr><td><span class="mono">(а)</span></td><td><b>Привлечённая машина</b> — собственник, передавший автомобиль в управление/арендную схему через Платформу.</td></tr>
<tr><td><span class="mono">(б)</span></td><td><b>Клиент на аренду</b> — арендатор, пришедший по лиду/воронке/агентству Платформы (метка в CRM/UTM/реестр).</td></tr>
<tr><td><span class="mono">(в)</span></td><td><b>Агентство</b> — клиент кооператива, обслуженный через Платформу (корпоративный, трансферный, подписочный и т.п.).</td></tr>
</table>
<div class="clause"><div class="cn">2.1</div><p>Спорная атрибуция: приоритет у более ранней фиксации лида; неразрешённые споры — на ежемесячной сверке.</p></div>
<div class="clause"><div class="cn">2.2</div><p>Клиент считается атрибутированным на <b>24 месяца</b> с первой фиксации (включая повторные аренды и продления).</p></div>
</section>
<section>
<div class="snum">03 / ДЕНЬГИ</div>
<h2>Вознаграждение и порядок расчётов</h2>
<div class="clause"><div class="cn">3.1</div><p>База расчёта: <b>нетто-выручка</b> по атрибутированному клиенту (полученные платежи за вычетом возвратов, депозитов, комиссий платёжных систем и налогов).</p></div>
<div class="clause"><div class="cn">3.2</div><p>Вознаграждение: <b>10% от базы</b>. Ставка фиксируется на 12 месяцев.</p></div>
<div class="clause"><div class="cn">3.3</div><p>Расчётный период — календарный месяц. Партнёр до 10-го числа следующего месяца направляет подписанный акт + выгрузку; оплата в течение 5 банковских дней, BYN по курсу НБ РБ на дату оплаты (либо USDT — по выбору сторон).</p></div>
<div class="clause"><div class="cn">3.4</div><p>Просрочка &gt;15 дней: пеня 0,1%/день; &gt;45 дней — право Оператора приостановить доступ к Платформе.</p></div>
</section>
<section>
<div class="snum">04 / РОЛЬ ПЛАТФОРМЫ</div>
<h2>Что делает Simbiot</h2>
<p class="lead">Ровно тот функционал, на котором строится «заработок на атрибутированном клиенте»:</p>
<ul>
<li><b>Воронки и лид-скоринг</b> — обработка входящих заявок на аренду, приоритизация лидов;</li>
<li><b>Агент-диспетчер 24/7</b> — аренда, продления, подписка без участия человека в ночи и выходные;</li>
<li><b>Скоринг таксистов</b> — аренда с выкупом: отсев рискованных драйверов;</li>
<li><b>Онбординг владельцев машин</b> — приём машин в паркоменеджмент (категория «а»);</li>
<li><b>Контент-PR</b> — первое в РБ полностью AI-агентское продвижение проката: почти бесплатный охват + входящие заявки на франшизу/white label.</li>
</ul>
</section>
<section>
<div class="snum">05 / РОСТ ВЫРУЧКИ</div>
<h2>Рычаги роста парка</h2>
<p class="lead">Ориентир: полное развёртывание рычагов 1–4 даёт расчётно <b>+60–100% выручки за 6–12 мес.</b>, из них ~+20% — в первые 8 недель (монетизация простоя). CAPEX — только ребаланс парка. Оценки сверить с фактическими данными парка (машин, ставки, vacancy) — выгрузка от Партнёра до запуска.</p>
<table>
<tr><th>#</th><th>Рычаг</th><th>Эффект</th></tr>
<tr><td><span class="mono">1</span></td><td><b>Подписка на простой</b> — простаивающие машины сдаются по подписке (такси, доставки, каршеринг-операторы)</td><td>быстрые деньги: монетизация vacancy, ~+20% за 8 недель</td></tr>
<tr><td><span class="mono">2</span></td><td><b>Аренда с выкупом</b> — пилот 2–3 эконом-машины, скоринг водителей</td><td>длинные деньги: выкупные платежи + продажа машины в конце</td></tr>
<tr><td><span class="mono">3</span></td><td><b>Привлечённые машины</b> — приём авто собственников в управление</td><td>рост парка без CAPEX</td></tr>
<tr><td><span class="mono">4</span></td><td><b>Ребаланс парка</b> — перераспределение под спрос (класс, сезон)</td><td>повышение ставки/дня и снижение vacancy</td></tr>
<tr><td><span class="mono">5</span></td><td><b>Воронка и агент-диспетчер</b></td><td>конверсия лидов, снижение стоимости обслуживания</td></tr>
<tr><td><span class="mono">6</span></td><td><b>PR «полностью AI-based агентство»</b></td><td>отложенный: лиды + заявки на франшизу/white label</td></tr>
</table>
<h3>Последовательность внедрения</h3>
<ul>
<li><b>Недели 1–2:</b> подписка на простой (1) + запуск PR-волны (6) — быстрые деньги и охват;</li>
<li><b>Месяц 1–2:</b> пилот аренды с выкупом на 2–3 эконом-машинах (2) + приём первых привлечённых машин (3);</li>
<li><b>Месяц 2–3:</b> ребаланс парка (4), масштабирование подписки и привлечённого парка.</li>
</ul>
</section>
<section>
<div class="snum">06 / ПОРЯДОК ПОДПИСАНИЯ</div>
<h2>Как подписываем</h2>
<div class="clause"><div class="cn">6.1</div><p>Стороны согласовывают реквизиты и Приложение № 1 (Реестр атрибуций, стартовый состав).</p></div>
<div class="clause"><div class="cn">6.2</div><p>Подписывается настоящее Соглашение и параллельно — <b>Side Letter № 1</b> (урегулирование исторической задолженности, отдельный документ).</p></div>
<div class="clause"><div class="cn">6.3</div><p>Юридическая проверка обеих документов до подписания обязательна.</p></div>
</section>
<section>
<div class="snum">07 / РЕКВИЗИТЫ</div>
<h2>Подписи</h2>
<div class="sign">
<div class="sig">
<div class="org">Оператор Платформы · Кооператив</div>
<div class="line"></div>
<div class="field">Председатель правления · подпись</div>
<div class="field">Alex Shkor · «___» _______ 2026 г.</div>
</div>
<div class="sig">
<div class="org">Партнёр · [наименование прокатной компании]</div>
<div class="line"></div>
<div class="field">[должность, ФИО] · подпись</div>
<div class="field">М.П. · «___» _______ 2026 г.</div>
</div>
</div>
</section>
<footer>
Черновик для обсуждения, не оферта. Юридическая проверка перед подписанием обязательна. Основа: встреча 04.10.2026 (meeting-6feceb4c). Исторический долг (~15 000 USD) — отдельный Side Letter № 1.
</footer>
</div>
</body>
</html>

View file

@ -1,79 +0,0 @@
# Прокат авто (Минск) × Simbiot — договор-черновик и план роста выручки
**Дата:** 04.10.2026 · **Основа:** встреча meeting-6feceb4c (04.10.2026), фиксация договорённостей
**Owner:** Alex · **Статус:** черновик на согласование (юридическая проверка перед подписанием обязательна)
---
## Часть I. Договор-черновик
> СОГЛАШЕНИЕ О ПАРТНЁРСКОМ ВОЗНАГРАЖДЕНИИ № 1
> г. Минск, «04» октября 2026 г.
**Стороны:**
- **Кооператив** — [полное наименование кооператива 42], далее «Оператор Платформы» (Simbiot);
- **Партнёр** — [наименование юрлица/ИП прокатной компании], г. Минск, далее «Партнёр».
### 1. Предмет соглашения
1.1. Оператор предоставляет Партнёру доступ к Платформе Simbiot (агентские, маркетинговые и операционные AI-сервисы) для привлечения клиентов и управления автопарком Партнёра.
1.2. За это Партнёр уплачивает Оператору **партнёрское вознаграждение в размере 10%** от заработка по каждому **Атрибутированному клиенту** (п. 2).
### 2. Атрибуция. Атрибутированным считается клиент:
- **(а) Привлечённая машина** — собственник, передавший автомобиль в управление/арендную схему через Платформу;
- **(б) Клиент на аренду** — арендатор, пришедший по лиду/воронке/агентству Платформы (метка в CRM/UTM/реестр);
- **(в) Агентство** — клиент кооператива, обслуженный через Платформу (корпоративный, трансферный, подписочный и т.п.).
2.1. Факт атрибуции фиксируется записью в **Реестре атрибуций** (Приложение №1, ведёт Оператор, доступ Партнёру — чтение). Спорная атрибуция: приоритет у более ранней фиксации лида; неразрешённые споры — на ежемесячной сверке.
2.2. Клиент считается атрибутированным на **24 месяца** с первой фиксации (включая повторные аренды и продления).
### 3. Вознаграждение и порядок расчётов
3.1. База расчёта: **нетто-выручка** по атрибутированному клиенту (полученные платежи за вычетом возвратов, депозитов, комиссий платёжных систем и налогов).
3.2. Вознаграждение: **10% от базы**. Ставка фиксируется на 12 месяцев.
3.3. Расчётный период — календарный месяц. Партнёр до 10-го числа следующего месяца направляет подписанный акт + выгрузку; оплата в течение 5 банковских дней, BYN по курсу НБ РБ на дату оплаты (либо USDT — по выбору сторон).
3.4. Просрочка >15 дней: пеня 0,1%/день; >45 дней — право Оператора приостановить доступ к Платформе.
### 4. Конвертация долга в инвестицию
4.1. Стороны признают задолженность Партнёра перед Оператором за работы за период ~3 месяца, с инфляционной индексацией, **эквивалент ~15 000 USD, выраженный в BTC по курсу на дату образования долга** (курс-привязка: сумма фиксируется в BTC по курсу BTC/USD на [дату образования долга], рублёвая/долларовая переоценка — по курсу на дату расчётов).
4.2. По подписании настоящего Соглашения долг **полностью конвертируется в инвестиционный вклад Партнёра в Проект «Интеграция Simbiot в прокатный бизнес»** (юнит: развитие Партнёра как витринного кейса платформы) на условиях Приложения №2 (Инвестиционное соглашение: доля/profit-share, reporting, выход).
4.3. С момента подписания Приложения №2 требование по долгу считается урегулированным (новация).
### 5. ИП и конфиденциальность
5.1. Все права на Платформу Simbiot, модели, данные обучения и CRM принадлежат Оператору. Данные о клиентах Партнёра — совместная коммерческая тайна; Оператор вправе использовать **обезличенную** статистику в маркетинге.
5.2. Кейс Партнёра («AI-агентство в прокате») Оператор вправе публиковать по согласованию формулировок.
### 6. Срок, расторжение
6.1. Срок: 12 месяцев с даты подписания, автопролонгация на 12 мес., если ни одна сторона не уведомила за 30 дней.
6.2. Расторжение не отменяет вознаграждение за атрибутированных клиентов в текущем периоде и 24-месячный хвост п. 2.2.
### 7. Прочее
Форс-мажор, применимое право РБ, арбитраж по месту Оператора, обмен сканами = оригинал, ЭЦП/eDocument допустимы. Реквизиты — Приложение №3.
**Открытые вопросы к Alex (до подписания):**
1. База 10% — валовая или нетто-выручка (п. 3.1)?
2. Хвост атрибуции 24 мес. — устраивает?
3. Параметры Приложения №2: доля Партнёра в проекте / profit-share % и срок.
4. Точная дата образования долга для фиксации BTC-курса.
---
## Часть II. Анализ рычагов роста выручки (приоритизировано)
**Исходная проблема парка:** перекос в премиум (CLA45, X7, Audi, BMW2) при нишевом спросе → премиум-активы простаивают, простой = 0 выручки при ненулевых издержках (страхование, стоянка, амортизация).
| # | Рычаг | Усилие | Эффект (оценка) | Почему сработает |
|---|-------|--------|------------------|------------------|
| **1** | **Подписка на простой, ~2990/мес** | S | **+15–25% к выручке парка за 1–2 мес.**, почти без CAPEX | «Любая несданная тачка окупается за 3 дня подписки»: 2990/30 ≈ 100/день. Каждые 3 простых дня сегодня = потерянный месячный платёж. Простой превращается из 0 в выручку. ЦА: экспаты/долгосрочники/малый бизнес |
| **2** | **Аренда с выкупом под таксистов (~30%/год)** | M | **×1.6 выручки с машины за 2 года**, vacancy → ~0 | Эконом-авто ~25k: клиент-таксист платит ~30% годовых сверх стоимости 24 мес. → возврат ~40k с одной машины, предсказуемый денежный поток, износ компенсирован ставкой |
| **3** | **Привлечённые машины (кейс Vestgroup: 0 своих)** | M | **рост парка ×2–3 без CAPEX**, комиссия ~25–35% с каждой | Asset-light: владельцы отдают машины в управление, платформа монетизирует их же воронкой. Снимает потолок роста, ограниченный собственным капиталом |
| **4** | **Ребаланс парка: прем → эконом** | M (капитал) | **+10–15% выручки + высвобождение замороженного капитала** | Непроседающий премиум продаётся (вернуть капитал), вместо него 2–3 эконом — стабильный спрос, ниже износ и риск |
| **5** | **Дешёвый трафик / новые сегменты** | S–M | **+20–30% лидов при том же бюджете** | Такопарки и драйверы (связка с рычагом 2), резиденты на выходные (не только туристы), корпоративные; AI-боты Simbiot = обработка лида ~0$ 24/7 → CAC падает |
| **6** | **PR «полностью AI-based агентство»** | S | отложенный: лиды + входящие заявки на франшизу/белые лейблы | Медиа-крючок первого в РБ AI-агентства в прокате; контент генерирует сам Simbiot — почти бесплатный охват; усиливает рычаги 1–3 |
### Последовательность внедрения
1. **Недели 1–2:** подписка на простой (рычаг 1) + запуск PR-волны (6) — быстрые деньги и охват.
2. **Месяц 1–2:** пилот аренды с выкупом на 2–3 эконом-машинах (2) + приём первых привлечённых машин (3).
3. **Месяц 2–3:** ребаланс парка (4), масштабирование подписки и привлечённого парка.
### Сводная оценка
Полное развёртывание рычагов 1–4 даёт расчётно **+60–100% выручки парка за 6–12 мес.** при умеренном CAPEX (только ребаланс), из них ~+20% — в первые 8 недель за счёт монетизации простоя. Оценки требуют сверки с фактическими данными парка (кол-во машин, ставки, vacancy) — запросить у Партнёра выгрузку до запуска.
**Роль Simbiot:** воронки и лид-скоринг (5), агент-диспетчер 24/7 по аренде/подписке (1), скоринг таксистов для аренды-с-выкупом (2), онбординг владельцев машин (3), контент-PR (6) — всё это и есть база «заработка на атрибутированном клиенте» из Части I.

View file

@ -1,115 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Side Letter № 1 — урегулирование задолженности (~15 000 USD)</title>
<style>
:root{
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8; --ok:#1d7a3e; --warn:#a3690f;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
.wrap{max-width:900px;margin:0 auto;padding:56px 24px 100px}
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:36px}
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
h1{font-size:clamp(30px,5vw,46px);line-height:1.05;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
.sub{font-size:17px;color:var(--dim);max-width:720px}
section{margin:46px 0 0}
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
h2{font-size:24px;font-weight:800;margin:4px 0 6px;text-transform:uppercase}
.clause{border:1px solid var(--line);background:#fff;padding:14px 18px;margin:12px 0;max-width:860px}
.clause .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.clause p{font-size:14.5px;margin:4px 0}
.note{border:1px solid var(--ink);border-left:6px solid var(--warn);background:#fff;padding:14px 18px;margin:20px 0;max-width:860px}
.note b{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--warn)}
table{width:100%;border-collapse:collapse;margin:18px 0;max-width:860px;background:#fff;border:1px solid var(--ink)}
th{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;text-align:left;padding:10px 12px;background:var(--ink);color:var(--paper)}
td{padding:10px 12px;border-top:1px solid var(--line);font-size:14px;vertical-align:top}
.sign{border:3px double var(--ink);background:#fff;padding:22px;margin:26px 0;display:grid;grid-template-columns:1fr 1fr;gap:20px}
@media(max-width:760px){.sign{grid-template-columns:1fr}}
.sig .org{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin-bottom:14px}
.sig .line{border-bottom:1px solid var(--ink);height:38px;margin-bottom:8px}
.sig .field{font-size:12.5px;color:var(--dim)}
footer{margin-top:60px;border-top:1px solid var(--line);padding-top:18px;color:var(--dim);font-size:12.5px}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">Потребительский кооператив «Друзья Коллективного Разума» · Минск</div>
<h1>Side Letter № 1<br>к Соглашению о партнёрском вознаграждении</h1>
<div class="sub">Урегулирование исторической задолженности Партнёра перед Оператором: признание, фиксация, конвертация в инвестиционный вклад. Подписывается параллельно с Соглашением № 1, отдельно — чтобы не смешивать долги с партнёрскими расчётами.</div>
</header>
<section>
<div class="snum">СТОРОНЫ И ОСНОВАНИЕ</div>
<h2>Преамбула</h2>
<div class="clause"><p>Стороны — те же, что в Соглашении о партнёрском вознаграждении № 1 от «___» _______ 2026 г. («Соглашение»): Кооператив ПК «Друзья Коллективного Разума» («Оператор») и [наименование прокатной компании] («Партнёр»).</p></div>
<div class="clause"><p>Side Letter — неотъемлемая часть Соглашения, регулирует только исторический долг и не меняет условия о партнёрском вознаграждении (10%, атрибуция, порядок расчётов).</p></div>
</section>
<section>
<div class="snum">01 / ПРИЗНАНИЕ ДОЛГА</div>
<h2>Признание задолженности</h2>
<div class="clause"><div class="cn">1.1</div><p>Стороны признают задолженность Партнёра перед Оператором за выполненные работы за период ~3 месяца (по актам/переписке, перечислены в Приложении к настоящему Side Letter).</p></div>
<div class="clause"><div class="cn">1.2</div><p>Стороны не имеют взаимных претензий по задолженности за указанный период, кроме урегулированной настоящим Side Letter.</p></div>
</section>
<section>
<div class="snum">02 / ФИКСАЦИЯ СУММЫ</div>
<h2>Сумма и валютная привязка</h2>
<table>
<tr><th style="width:240px">Параметр</th><th>Значение</th></tr>
<tr><td>Эквивалент</td><td><b>~15 000 USD</b></td></tr>
<tr><td>Валютная привязка</td><td>выражен в <b>BTC</b> по курсу BTC/USD на дату образования долга; рублёвая/долларовая переоценка — по курсу на дату расчётов</td></tr>
<tr><td>Индексация</td><td>инфляционная индексация за период образования долга (если применимо)</td></tr>
<tr><td>Дата образования</td><td>[заполнить: последний акт за период долга]</td></tr>
</table>
<div class="clause"><div class="cn">2.1</div><p>Итоговая сумма фиксируется в Приложении к Side Letter в BTC и в USD-эквиваленте; стороны подтверждают её подписями.</p></div>
</section>
<section>
<div class="snum">03 / КОНВЕРТАЦИЯ</div>
<h2>Конвертация долга в инвестицию</h2>
<div class="clause"><div class="cn">3.1</div><p>С момента подписания Side Letter долг <b>полностью конвертируется в инвестиционный вклад Партнёра в Проект «Интеграция Simbiot в прокатный бизнес»</b> (юнит: развитие Партнёра как витринного кейса платформы) на условиях инвестиционного соглашения (Приложение № 2 к Соглашению: доля / profit-share, [заполнить]).</p></div>
<div class="clause"><div class="cn">3.2</div><p>С момента конвертации Партнёр не имеет требований к Оператору по возврату суммы долга как денежного обязательства; права Партнёра реализуются исключительно через условия инвестиционного соглашения (доля/profit-share, порядок распределения, выход).</p></div>
<div class="clause"><div class="cn">3.3</div><p>Инвестиционный вклад не гарантирует доходность и не является займом; риск убытка несёт Партнёр как инвестор.</p></div>
</section>
<section>
<div class="snum">04 / ПОРЯДОК</div>
<h2>Подписание и юридическая проверка</h2>
<div class="clause"><div class="cn">4.1</div><p>Side Letter подписывается одновременно с Соглашением № 1. Юридическая проверка обоих документов до подписания обязательна.</p></div>
<div class="clause"><div class="cn">4.2</div><p>До подписания стороны заполняют: дату образования долга, состав работ (Приложение), точную сумму в BTC + USD-эквивалент, условия инвестиционного соглашения (доля/profit-share).</p></div>
</section>
<section>
<div class="snum">05 / ПОДПИСИ</div>
<h2>Реквизиты</h2>
<div class="sign">
<div class="sig">
<div class="org">Оператор · Кооператив</div>
<div class="line"></div>
<div class="field">Председатель правления · подпись</div>
<div class="field">Alex Shkor · «___» _______ 2026 г.</div>
</div>
<div class="sig">
<div class="org">Партнёр · [наименование прокатной компании]</div>
<div class="line"></div>
<div class="field">[должность, ФИО] · подпись</div>
<div class="field">М.П. · «___» _______ 2026 г.</div>
</div>
</div>
</section>
<footer>
Черновик для обсуждения, не оферта. Юридическая проверка перед подписанием обязательна. Основа: встреча 04.10.2026 + решение Alex от 04.10.2026. Парный документ: rental-x-simbiot-agreement.html
</footer>
</div>
</body>
</html>

View file

@ -1,35 +0,0 @@
# Журнал ночной самодоработки харнеса
Директива Alex (голосовое m-516173718, эскалация e-261005074512): ночами
создавать себе задачи, ставить эксперименты над собой, смотреть что работает
лучше/хуже и продолжать дорабатываться.
Цикл: крон в 00:00 UTC (03:00 Минск) подаёт задачу «Ночная самодоработка» [M];
ближайший hour-тик выполняет; каждая итерация дописывает строку ниже.
Формат строки (одна на итерацию):
`#N | дата | гипотеза | метрика до → после | вывод (работает/нет/нужно больше данных)`
Правила:
- один эксперимент за ночь, мелкий (лёгкий — провести сразу, тяжёлый — задачей на день);
- фильтр AUM: эксперимент должен служить «≥ +1% к AUM в горизонте год+»;
- ночь — тишина в чате (кроме критичного); результат поднимает утренний дайджест;
- не выдумывать метрики: только измеримое из стора (`cli.py events/status/aum`),
логов тиков и этого журнала.
## Итерации
#0 | 2026-10-05 | Калибровка цикла (не эксперимент) | механизм: крон-податчик + журнал заведены, первая задача в очереди | ожидаем первую ночную итерацию
#1 | 2026-10-05 | Дедуп «ВЫЖИМОК НИЖНИХ УРОВНЕЙ» в промпте тика: тихий мин-уровень кладёт 6 идентичных [gate:quiet]-строк в каждый шаг промпта hour-тика; гипотеза — схлопывание повторов с счётчиком ×N срежет burn без потери информации | секция ВЫЖИМОК в hour-промпте: 6 строк/522 байт → 1 строка/95 байт (−82% секции, −427 байт/шаг; при 991 шаге/сутки ≈ −0.4М байт ≈ ~100k промпт-токенов/сутки при 57.8k avg/шаг — оценка) | работает: фикс в scheduler.py build_context (дедуп по (level, text)), верифицирован на живой модели (×6 подряд в контексте), test_gate + test_task_batch ALL PASS; контроль — avg prompt/step в BudgetSpent hour за следующие сутки (базлайн 57,781)
## Ретро суток 2026-10-04T13:00 → 2026-10-05T13:00 (UTC)
- Задачи: закрыто 27 (в т.ч. 8 L), подано 29, эскалаций открыто 0 (3 raised / 3 acked).
- Падения: 1 CycleError hour (14:48, таймаут воркера 2880с — тик потерян целиком; max инвокация 2872с из 990 завершённых).
- Гейт работает: 1381 GateSkipped min-тиков = LLM не запускался в тишине (экономия).
- Burn: hour 57.3M промпт-токенов (991 шаг, avg 57.8k/шаг, completion всего 1.1%); min 5.5M (520 шагов, avg 10.6k). Доминирующая статья расхода — раздувание промпта hour-шага, а не число вызовов (инвокаций всего 8+56).
- AUM 26.7 у.е.; burn 9router 7д 1417.88 — burn остаётся прямым минусом к AUM, главный рычаг — тоньше контекст шага.
- Вывод: следующие эксперименты — что именно раздувает avg 57.8k/шаг (история сообщений? RAG? батч?), паттерн «дедуп + кап» применим к секциям эскалаций/инбокса.

View file

@ -1,27 +0,0 @@
# Самоанализ харнеса — 2026-10-04 (t-261004152714)
## Цифры
- Задачи: 147 всего — 142 done (S 51 / M 61 / L 30), 2 blocked, 3 running. Провалов мало, поток стабилен.
- Эскалации: 79 (warn 56 / info 19 / critical 4) — все acked, открытых 0.
- Инбокс TG: 100 сообщений (text 81, voice 15, video 1); транскрипция 16/16 непустая после фикса.
- Ошибки уровней: min 38, hour 6, day 1. Главный паттерн — таймаут LLM-воркера (480с/2880с).
## Что работает
- Лестница sec→min→hour→day с батчем 3 задач/тик: пропускная способность есть, анти-старение очереди соблюдается.
- ES-стор + ed25519-подписи: verify проходит, тампер-цепочка цела.
- Идемпотентная доставка в TG (tg-notify/tg-deliver-file, SKIP на повторе) — дублей нет (t-260930083937).
- Голосовые: двухъярусный ASR, короткие фразы → large-v3-turbo, кэш по file_unique_id.
- Эскалации доходят и закрываются (e-261004144805 — фикс «смерть тика не воскрешает закрытые задачи» уже в git).
## Что болит (→ задачи саморазвития)
1. **Звук из видео не извлекался** — kind=video не входил в TRANSCRIBABLE_KINDS и поллер не звал транскрипцию. ИСПРАВЛЕНО в этом тике: ffmpeg-извлечение дорожки (16к моно opus) + turbo-роутинг для ≤60с; e2e на m-516173698 подтверждён. Осталось: тесты и наблюдение за памятью.
2. **Память инбокс-поллера 3.7G peak** (systemd Consumption) — транскрипция в процессе поллера; нужен MemoryMax и/или вынос ASR-вызова из поллера.
3. **Кириллица в setMyCommands даёт HTTP 400** (костыль-транслит latin) — бот теряет русские подписи команд.
4. **Таймауты LLM-воркеров** (min×38, hour×6) — причина №1 потерь тиков; воркер убивается до записи BudgetSpent.
5. **Клоны задач из служебных 'list'** — ловушка чинилась дважды; нужен регресс-тест на генерацию задач из inbox-текста.
## Задачи саморазвития (созданы, проверяемый результат)
- t-261004160531-ed25ed [M]: тесты voice-пайплайна (extract_audio_track + роутинг) — pytest зелёный, кейс видео↔текст.
- t-261004160531-780666 [L]: память поллера — MemoryMax + вынос тяжёлой транскрипции из поллера; peak < 1G за сутки.
- t-261004160532-690fcb [M]: TG-команды кириллицей — setMyCommands без 400 и без транслита.
- t-261004160532-0fb500 [M]: регресс-тест «служебный текст inbox не порождает задачу-клон».

View file

@ -1,443 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shkor beauty — корейская косметика в Минске (прототип витрины)</title>
<meta name="description" content="Shkor beauty — витрина магазина корейской косметики: уход за лицом, телом и волосами. Прототип каталога, заказы пока не принимаются.">
<style>
:root{
--ink:#1c1a17; --paper:#faf7f2; --card:#ffffff; --line:#e4ddd2;
--acc:#0e7a5f; --acc-soft:#e7f2ee; --muted:#6f6a61; --sale:#b3352c;
--sans:"Segoe UI",-apple-system,"Helvetica Neue",Arial,sans-serif;
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--paper);color:var(--ink);font-family:var(--sans);line-height:1.5}
a{color:inherit;text-decoration:none}
.wrap{max-width:1100px;margin:0 auto;padding:0 20px}
.note{font-family:var(--mono);font-size:12px;background:var(--ink);color:#f4f0e6;padding:6px 0;text-align:center}
.note b{color:#8fd4bd}
header{background:var(--card);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:5}
.hbar{display:flex;align-items:center;gap:18px;padding:14px 0}
.logo{font-size:22px;font-weight:700;letter-spacing:-.02em;white-space:nowrap}
.logo span{color:var(--acc)}
.logo small{display:block;font-family:var(--mono);font-weight:400;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.search{flex:1;display:flex}
.search input{flex:1;border:1px solid var(--line);border-right:none;padding:10px 14px;font:inherit;font-size:14px;background:var(--paper);border-radius:6px 0 0 6px;outline:none}
.search input:focus{border-color:var(--acc)}
.search button{border:none;background:var(--acc);color:#fff;padding:0 16px;border-radius:0 6px 6px 0;cursor:pointer;font:inherit;font-size:14px}
.hlinks{display:flex;gap:16px;font-size:14px;white-space:nowrap}
.hlinks a:hover{color:var(--acc)}
nav.cats{display:flex;gap:4px;overflow-x:auto;padding:0 0 10px;font-size:14px}
nav.cats a{padding:6px 12px;border-radius:20px;border:1px solid transparent;white-space:nowrap}
nav.cats a:hover{border-color:var(--acc);color:var(--acc)}
nav.cats a.all{background:var(--acc-soft);color:var(--acc);font-weight:600}
.hero{background:linear-gradient(180deg,var(--acc-soft),var(--paper));border-bottom:1px solid var(--line)}
.hero .wrap{display:flex;gap:40px;align-items:center;padding-top:52px;padding-bottom:52px;flex-wrap:wrap}
.hero h1{font-size:clamp(30px,4.5vw,46px);line-height:1.1;letter-spacing:-.02em;max-width:16ch}
.hero p{margin-top:14px;max-width:48ch;color:var(--muted);font-size:17px}
.hero .btn{display:inline-block;margin-top:22px;background:var(--acc);color:#fff;padding:13px 26px;border-radius:6px;font-weight:600}
.hero .btn.ghost{background:transparent;color:var(--acc);border:1.5px solid var(--acc);margin-left:10px}
.hero .btn.ghost:hover{background:var(--acc-soft)}
.hero .btn:hover{filter:brightness(1.08)}
.rhythm{flex:1;min-width:260px;display:flex;align-items:flex-end;gap:4px;height:120px;opacity:.9}
.rhythm span{flex:1;background:var(--acc);border-radius:3px 3px 0 0;animation:grow 2.8s ease-in-out infinite}
@keyframes grow{0%,100%{transform:scaleY(.3)}50%{transform:scaleY(1)}}
.rhythm span:nth-child(3n){background:var(--ink)}
section{padding:46px 0}
h2{font-size:clamp(22px,3vw,30px);letter-spacing:-.01em;margin-bottom:6px}
.sub{color:var(--muted);font-size:15px;margin-bottom:24px}
.catgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px}
.cat{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:20px 18px;transition:transform .15s ease,border-color .15s ease}
.cat:hover{transform:translateY(-2px);border-color:var(--acc)}
.cat .ic{width:38px;height:38px;border-radius:8px;background:var(--acc-soft);margin-bottom:12px;position:relative}
.cat .ic::after{content:"";position:absolute;inset:11px;background:var(--acc);border-radius:50%}
.cat.a .ic::after{border-radius:2px}
.cat.b .ic::after{height:6px;top:16px}
.cat.c .ic::after{width:6px;left:16px}
.cat.d .ic::after{border-radius:50%;inset:14px 11px}
.cat h3{font-size:16px;margin-bottom:4px}
.cat p{font-size:13px;color:var(--muted)}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
.chips a{font-size:13px;padding:7px 14px;border:1px solid var(--line);border-radius:20px;background:var(--card)}
.chips a:hover{border-color:var(--acc);color:var(--acc)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:18px}
.p{background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;transition:box-shadow .15s ease}
.p:hover{box-shadow:0 6px 18px rgba(28,26,23,.08)}
.ph{aspect-ratio:1/1;background:var(--acc-soft);display:flex;align-items:center;justify-content:center;position:relative}
.ph .b{font-family:var(--mono);font-size:15px;letter-spacing:.06em;color:var(--acc);border:1.5px solid var(--acc);padding:8px 14px;border-radius:4px;background:var(--card)}
.ph .tag{position:absolute;top:10px;left:10px;background:var(--sale);color:#fff;font-size:11px;font-weight:700;padding:3px 8px;border-radius:4px}
.ph .tag.new{background:var(--acc)}
.pb{padding:14px 16px 16px;display:flex;flex-direction:column;flex:1}
.pb .cat-l{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.pb .brand{font-size:12px;font-weight:700;color:var(--acc);margin-top:4px}
.pb h3{font-size:14.5px;font-weight:400;line-height:1.35;margin:4px 0 10px;flex:1}
.price{display:flex;align-items:baseline;gap:8px}
.price .now{font-size:17px;font-weight:700}
.price .old{font-size:13px;color:var(--muted);text-decoration:line-through}
.p button{margin-top:12px;border:1.5px solid var(--ink);background:transparent;padding:9px;font:inherit;font-size:13.5px;font-weight:600;border-radius:6px;cursor:pointer;transition:.15s}
.p button:hover{background:var(--ink);color:#fff}
.brands{display:flex;flex-wrap:wrap;gap:10px}
.brands span{font-family:var(--mono);font-size:13px;border:1px solid var(--line);padding:8px 16px;border-radius:6px;background:var(--card);color:var(--muted)}
.brands span:hover{color:var(--acc);border-color:var(--acc)}
.dark{background:var(--ink);color:#f4f0e6}
.dark .sub{color:#b9b2a4}
.dark form{display:flex;gap:10px;max-width:480px;margin-top:8px;flex-wrap:wrap}
.dark input{flex:1;min-width:200px;border:1px solid #4a453d;background:transparent;color:inherit;padding:12px 14px;border-radius:6px;font:inherit;outline:none}
.dark input:focus{border-color:var(--acc)}
.dark button{background:var(--acc);border:none;color:#fff;padding:12px 22px;border-radius:6px;font:inherit;font-weight:600;cursor:pointer}
.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-top:8px}
.plan-card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:24px}
.plan-card h3{font-size:20px}
.plan-price{font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:6px 0 14px}
.plan-card ul{list-style:none;margin:0 0 18px}
.plan-card li{padding:6px 0 6px 22px;position:relative;font-size:14.5px;color:#3d3a34}
.plan-card li:before{content:"✓";position:absolute;left:0;color:var(--acc);font-weight:700}
.plan-card .btn{display:inline-block;background:var(--acc);color:#fff;padding:10px 18px;border-radius:6px;font-weight:600}
.plan-card.featured{border:2px solid var(--acc);box-shadow:0 6px 24px rgba(14,122,95,.12)}
.plan-card code{font-family:var(--mono);font-size:12px;background:var(--acc-soft);color:var(--acc);padding:1px 5px;border-radius:4px}
#reqform{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;max-width:760px}
#reqform button{margin-top:4px}
footer{padding:30px 0 40px;color:var(--muted);font-size:13.5px}
footer .wrap{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
footer .proto{font-family:var(--mono);font-size:12px}
footer .flinks{display:flex;gap:18px;font-size:13.5px}
footer .flinks a{text-decoration:underline;color:var(--muted)}
footer .flinks a:hover{color:var(--acc)}
#toast{position:fixed;left:50%;bottom:24px;transform:translate(-50%,80px);background:var(--ink);color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;opacity:0;transition:.25s ease;z-index:9}
#toast.show{transform:translate(-50%,0);opacity:1}
@media (max-width:720px){
.hlinks{display:none}
.hero .wrap{padding-top:34px;padding-bottom:34px}
.rhythm{display:none}
}
@media (prefers-reduced-motion:reduce){
*{animation:none!important;transition:none!important;scroll-behavior:auto!important}
}
</style>
</head>
<body>
<div class="note">Витрина работает: AI-ассистент подбирает уход, заявки принимаются (Lite-план). Оплата онлайн — на шаге 2.</div>
<header>
<div class="wrap">
<div class="hbar">
<a class="logo" href="#">Shkor <span>beauty</span><small>корейская косметика · Минск</small></a>
<div class="search">
<input type="search" placeholder="Поиск: тонер, пэды, гидрофильное масло…">
<button>Найти</button>
</div>
<div class="hlinks">
<a href="#catalog">Каталог</a>
<a href="#brands">Бренды</a>
<a href="#plans">Магазинам</a>
<a href="#sub">Подписка</a>
</div>
</div>
<nav class="cats" aria-label="Разделы">
<a class="all" href="#catalog">Весь каталог</a>
<a href="#face">Уход за лицом</a>
<a href="#body">Уход за телом</a>
<a href="#hair">Уход за волосами</a>
<a href="#makeup">Декоративная косметика</a>
<a href="#acc">Аксессуары</a>
</nav>
</div>
</header>
<div class="hero">
<div class="wrap">
<div>
<h1>Корейский уход — AI подберёт, привезём за 1 день</h1>
<p>Каталог проверенных K-beauty брендов в Минске + AI-ассистент: разберёт состояние кожи, соберёт рутину и примет заявку. Доставка по Беларуси.</p>
<a class="btn" href="#catalog">Смотреть каталог</a> <a class="btn ghost" href="#request">Оставить заявку</a>
</div>
<div class="rhythm" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
</div>
<section id="catalog">
<div class="wrap">
<h2>Каталог</h2>
<p class="sub">Пять разделов, как у эталона рынка — структура навигации Korealab.</p>
<div class="catgrid">
<a class="cat a" href="#face"><span class="ic"></span><h3>Уход за лицом</h3><p>Очищение, тонеры, сыворотки, маски, SPF</p></a>
<a class="cat b" href="#body"><span class="ic"></span><h3>Уход за телом</h3><p>Гели для душа, лосьоны, кремы для рук</p></a>
<a class="cat c" href="#hair"><span class="ic"></span><h3>Уход за волосами</h3><p>Шампуни, маски, ампулы, филлеры</p></a>
<a class="cat d" href="#makeup"><span class="ic"></span><h3>Декоративная косметика</h3><p>Тинты, BB-кремы, бальзамы для губ</p></a>
<a class="cat a" href="#acc"><span class="ic"></span><h3>Аксессуары</h3><p>Спонжи, пады, пилочки, зеркала</p></a>
</div>
</div>
</section>
<section id="face" style="padding-top:0">
<div class="wrap">
<h2>Уход за лицом</h2>
<p class="sub">Подкатегории первого уровня — быстрый вход в нужный шаг рутины.</p>
<div class="chips">
<a href="#demo">Очищение</a><a href="#demo">Тонизирование</a><a href="#demo">Сыворотки и эссенции</a>
<a href="#demo">Патчи для глаз</a><a href="#demo">Тканевые маски</a><a href="#demo">Кремы</a>
<a href="#demo">Солнцезащитные средства</a><a href="#demo">Средства с кислотами</a>
</div>
<div class="grid" id="demo">
<article class="p">
<div class="ph"><span class="tag">−30%</span><span class="b">SKIN627</span></div>
<div class="pb">
<span class="cat-l">Сыворотки</span><span class="brand">SKIN627</span>
<h3>Увлажняющая сыворотка для лица Hyaluron Centella Ampoule</h3>
<div class="price"><span class="now">35,00 руб.</span><span class="old">50,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="tag new">Новинка</span><span class="b">JMsolution</span></div>
<div class="pb">
<span class="cat-l">Пэды</span><span class="brand">JMsolution</span>
<h3>Увлажняющие пэды с гиалуроновой кислотой Multi Hyaluronic</h3>
<div class="price"><span class="now">59,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="b">Beauty of Joseon</span></div>
<div class="pb">
<span class="cat-l">SPF</span><span class="brand">Beauty of Joseon</span>
<h3>Солнцезащитный гель Relief Sun SPF50+ PA++++</h3>
<div class="price"><span class="now">49,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="tag">−25%</span><span class="b">Biodance</span></div>
<div class="pb">
<span class="cat-l">Маски</span><span class="brand">Biodance</span>
<h3>Коллагеновая маска-пленка Collagen Wrapping Peel-off Mask</h3>
<div class="price"><span class="now">12,00 руб.</span><span class="old">16,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="b">Anua</span></div>
<div class="pb">
<span class="cat-l">Тонеры</span><span class="brand">Anua</span>
<h3>Успокаивающий тонер с сердечником Centella Toner 77%</h3>
<div class="price"><span class="now">62,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="b">Banila Co</span></div>
<div class="pb">
<span class="cat-l">Очищение</span><span class="brand">Banila Co</span>
<h3>Гидрофильное масло Clean It Zero Original</h3>
<div class="price"><span class="now">54,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="b">SKIN627</span></div>
<div class="pb">
<span class="cat-l">Сыворотки</span><span class="brand">SKIN627</span>
<h3>Сыворотка для лица с ретинолом Retinol Pantenol Ampoule</h3>
<div class="price"><span class="now">35,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="tag new">Новинка</span><span class="b">A'PIEU</span></div>
<div class="pb">
<span class="cat-l">Патчи</span><span class="brand">A'PIEU</span>
<h3>Патчи для глаз с молочной кислотой Milk One-Day Perfector</h3>
<div class="price"><span class="now">18,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
</div>
</div>
</section>
<section id="body" style="padding-top:0">
<div class="wrap">
<h2>Уход за телом и волосами</h2>
<p class="sub">Витрина разделов вне лица — по мотивам карточек эталона.</p>
<div class="grid">
<article class="p">
<div class="ph"><span class="b">WEPROVE</span></div>
<div class="pb">
<span class="cat-l">Для душа</span><span class="brand">WEPROVE</span>
<h3>Обновляющий гель для душа Renewing Aloe Shower Gel</h3>
<div class="price"><span class="now">29,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="tag">−15%</span><span class="b">WEPROVE</span></div>
<div class="pb">
<span class="cat-l">Шампуни</span><span class="brand">WEPROVE</span>
<h3>Восстанавливающий шампунь Daily Hair Repairing Shampoo</h3>
<div class="price"><span class="now">23,60 руб.</span><span class="old">27,80 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="b">Care:Nel</span></div>
<div class="pb">
<span class="cat-l">Кремы для рук</span><span class="brand">Care:Nel</span>
<h3>Питательный крем для рук с ши и центеллой</h3>
<div class="price"><span class="now">14,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
<article class="p">
<div class="ph"><span class="b">Ciracle</span></div>
<div class="pb">
<span class="cat-l">Аксессуары</span><span class="brand">Ciracle</span>
<h3>Силиконовый спонж для умывания с чехлом</h3>
<div class="price"><span class="now">9,00 руб.</span></div>
<button>В корзину</button>
</div>
</article>
</div>
</div>
</section>
<section id="brands">
<div class="wrap">
<h2>Бренды</h2>
<p class="sub">Формат «Алфавит брендов» как у эталона — вход через доверие к марке.</p>
<div class="brands">
<span>A'PIEU</span><span>ANSKIN</span><span>Anua</span><span>Atopalm</span>
<span>AXIS-Y</span><span>Banila Co</span><span>Beauty of Joseon</span><span>Biodance</span>
<span>Care:Nel</span><span>Celimax</span><span>Cos de Baha</span><span>JMsolution</span>
<span>SKIN627</span><span>WEPROVE</span>
</div>
</div>
</section>
<section id="plans">
<div class="wrap">
<h2>Магазинам корейской косметики</h2>
<p class="sub">Shkor beauty — это AI-ассистент витрины (B2B): подбор ухода, заказы и заявки — единое API для вашего сайта и любых агентов. Оператор сервиса — Shkor Labs LLC.</p>
<div class="plan-grid">
<div class="plan-card">
<h3>Lite</h3>
<p class="plan-price">базовый план</p>
<ul>
<li>Витрина с каталогом и брендами</li>
<li>Форма заявки → общая база заказов</li>
<li>Заявки приходят с пометкой <code>plan=lite</code></li>
<li>Договор с магазином лично: телефон/мессенджер</li>
</ul>
<a class="btn" href="#request">Начать с заявки</a>
</div>
<div class="plan-card featured">
<h3>Full</h3>
<p class="plan-price">полный каталог + заказы</p>
<ul>
<li>Всё из Lite</li>
<li>Полный каталог и корзина</li>
<li>Онлайн-оплата и статусы доставки</li>
<li>Агентный API: analyze → recommend → order</li>
</ul>
<a class="btn" href="#request">Подключить Full</a>
</div>
</div>
</div>
</section>
<section class="dark" id="request">
<div class="wrap">
<h2>Оставить заявку</h2>
<p class="sub">Расскажите, что нужно — подберём уход или подключим магазин. Заявка пишется в общую базу заказов, ответим в течение дня.</p>
<form id="reqform">
<input type="text" name="name" placeholder="Имя" required>
<input type="tel" name="phone" placeholder="+375 XX XXX-XX-XX" required>
<input type="text" name="note" placeholder="Что подобрать? (сухая кожа, SPF, тонер…)">
<button type="submit">Отправить</button>
</form>
<p class="sub" id="reqstatus" style="margin-top:10px"></p>
</div>
</section>
<section class="dark" id="sub">
<div class="wrap">
<h2>Подписка на поступления</h2>
<p class="sub">Одно письмо в неделю: новинки, скидки и разборы составов. Без спама.</p>
<form onsubmit="event.preventDefault();toast('Прототип: подписка не подключена')">
<input type="email" placeholder="your@email.com" required>
<button>Подписаться</button>
</form>
</div>
</section>
<footer>
<div class="wrap">
<div>Shkor beauty — витрина корейской косметики + AI-ассистент. Оператор сервиса: <b>Shkor Labs LLC</b> (Wyoming, USA) · <a href="mailto:alex.shkor@gmail.com" style="text-decoration:underline">alex.shkor@gmail.com</a></div>
<div class="flinks">
<a href="/privacy.html">Privacy Policy</a>
<a href="/terms.html">Terms of Service</a>
<a href="/disclaimer.html">Disclaimer</a>
<a href="/about.html">About &amp; Contacts</a>
</div>
<div class="proto">собрано харнесом Simbiot · кооператив 42 · cil.by</div>
</div>
</footer>
<div id="toast" role="status"></div>
<script>
function toast(t){
var el=document.getElementById('toast');
el.textContent=t;el.classList.add('show');
clearTimeout(el._t);el._t=setTimeout(function(){el.classList.remove('show')},2200);
}
document.querySelectorAll('.p button').forEach(function(b){
b.addEventListener('click',function(){
var card=b.closest('.pb'), h=card?card.querySelector('h3'):null;
var note=document.getElementById('reqform').note;
if(h&&note) note.value='Интересует: '+h.textContent;
document.getElementById('request').scrollIntoView({behavior:'smooth'});
toast('Добавлено в заявку — оставьте контакты ниже');
});
});
document.getElementById('reqform').addEventListener('submit',function(e){
e.preventDefault();
var f=e.target, st=document.getElementById('reqstatus');
st.textContent='Отправляем…';
fetch('/api/call',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:'submit_request',arguments:{name:f.name.value.trim(),phone:f.phone.value.trim(),note:f.note.value.trim()}})
}).then(function(r){return r.json()}).then(function(d){
var res=d.result||d;
if(res.order_id){st.textContent='Заявка принята: '+res.order_id+'. Свяжемся в течение дня.';f.reset();}
else{st.textContent='Не получилось: '+(res.error||JSON.stringify(res));}
}).catch(function(err){st.textContent='Сеть недоступна — напишите на alex.shkor@gmail.com';});
});
</script>
</body>
</html>

View file

@ -1,79 +0,0 @@
# Shkor Beauty Assistant
Beauty-ассистент магазина **Shkor beauty**: анализирует состояние кожи, подбирает продукты
из каталога витрины, оформляет заказ с доставкой и выдаёт инструкции по применению.
Подключается к **любому агенту** (LLM-бот, MCP-клиент, крон-скрипт, мобильное приложение).
Задача-источник: `t-261005120225-672f52` (требования Alex). Каталог синхронизирован
с прототипом витрины `../shkor-beauty-prototype.html` (t-261005115918-d38b36).
## Состав
| Файл | Что это |
|---|---|
| `assistant.py` | Ядро: анализ кожи, рекомендации, заказы, инструкции. Один вход `handle(request) -> dict` |
| `connector.py` | Универсальный коннектор: STDIO (MCP-стиль), HTTP API, CLI, демо-чат |
| `catalog.json` | 12 товаров витрины с мета-данными (concerns, actives, routine_step) |
| `orders.json` | Локальный леджер заказов (создаётся при первом заказе) |
## Подключение любым агентом
**1) STDIO, MCP-стиль** — для Claude/любого MCP-клиента:
```json
{"mcpServers": {"shkor": {"command": "python3", "args": ["/root/simbiot-harness/artifacts/shkor/connector.py"], "transport": "stdio"}}}
```
Протокол: построчный JSONL. `{"method":"tools/list"}` → список инструментов;
`{"method":"tools/call","params":{"name":"analyze_skin","arguments":{...}},"id":1}` → результат.
**2) HTTP API** — агент = любой HTTP-клиент:
```bash
connector.py --http 127.0.0.1:8791
curl -XPOST localhost:8791/call -d '{"name":"recommend","arguments":{"concerns":["dryness"]}}'
```
Роуты: `GET /health`, `POST /tools/list`, `POST /call`.
**3) CLI разовый вызов** — крон/скрипты:
```bash
connector.py --call analyze_skin --args '{"self_report":"стянутость и покраснения"}'
```
**4) Живая проверка:** `connector.py --demo` — полный сценарий: анализ → подбор → заказ → инструкция → статус.
## Инструменты (7)
- `analyze_skin` `{self_report?, symptoms?, photo_report?}` → тип кожи, concerns, hydration-индекс,
состояние барьера, фокус рутины. `photo_report` — принимаем отчёт vision-агента по селфи
(JSON или текст): мержим с самоотчётом, источники считаем отдельно.
- `recommend` `{concerns?, skin_type?, budget_max?}` → подобранные карточки с объяснением «почему».
- `search` `{query?, category?}` → поиск по каталогу (бренд/название/актив/категория).
- `place_order` `{items:[{id,qty}], delivery?, name?, phone?, address?, pay_method?, pay_link?}` →
заказ `SB-XXXXXXXX`, сумма, доставка. Платёж: `cash_on_delivery` или `pay_link` от
платёжного агента (Stripe/Альфа-Банк) — ассистент не хранит карты.
- `check_order` `{order_id}` → статус заказа и доставки.
- `delivery_options` `{cart_total?}` → курьер Минск / Европочта / самовывоз, бесплатно от 80 BYN.
- `usage_instructions` `{product_ids}` → пошаговые инструкции, порядок нанесения, предостережения
(ретинол только на ночь и не в один день с кислотами, SPF обязателен).
## Пример потока агента
```
1. analyze_skin {"self_report": "кожа стянутая, лоб блестит, покраснения"}
→ {"skin_type":"combination","concerns":["dryness","redness","oiliness"],"barrier":"compromised",...}
2. recommend {"concerns":["dryness","redness"], "skin_type":"sensitive"}
→ пэды JMsolution, тонер Anua Centella 77%, сыворотка SKIN627
3. place_order {"items":[{"id":"p05","qty":1},{"id":"p03","qty":1}],"delivery":"minsk_courier"}
→ SB-XXXXXXXX, 111.0 BYN, доставка бесплатно (корзина ≥80), курьер сегодня/завтра
4. usage_instructions {"product_ids":["p05","p03"]}
→ тонер (шаг 2) → SPF (шаг 7), предупреждения
5. check_order {"order_id":"SB-XXXXXXXX"}
```
## Границы (честно)
- Анализ — эвристический (словарь стеммов RU/EN + голосование по типу), НЕ постановка диагноза;
в каждом ответе дисклеймер «не медицинская консультация».
- Оплата — заглушка `pending`: реальный эквайринг подключает агент-хост через `pay_link`
(см. задачу по методам оплаты портала).
- Каталог = витрина-прототип (12 SKU). При росте — заменить `catalog.json` без правок кода.
Запуск тестов: `python3 connector.py --demo` (создаёт тестовый заказ в orders.json).

View file

@ -1,425 +0,0 @@
#!/usr/bin/env python3
"""Shkor Beauty Assistant — ядро.
Универсальный beauty-ассистент магазина Shkor beauty:
* анализ состояния кожи (self-report + отчёт vision-агента),
* рекомендация продуктов из каталога каталога витрины,
* оформление заказа + варианта доставки,
* инструкция по применению (порядок рутины, частота, предостережения).
Протокол: один вход `handle(request: dict) -> dict`, MCP-совместимая форма
(JSON-RPC tools/call) + простая форма {action, args}. Оболочки (STDIO/HTTP/CLI)
живут в connector.py — ассистент не знает, какой агент его вызывает.
"""
from __future__ import annotations
import json
import os
import re
import time
import uuid
from pathlib import Path
HERE = Path(__file__).resolve().parent
CATALOG = json.loads((HERE / "catalog.json").read_text(encoding="utf-8"))
ORDERS_PATH = HERE / "orders.json"
PRODUCTS = {p["id"]: p for p in CATALOG["products"]}
# ── Анализ кожи ──────────────────────────────────────────────────────────────
# ключи — стеммы (корни слов), чтобы ловить любые словоформы: «стянутая/стянутость», «покраснения/покраснение»
SYMPTOM_MAP = {
"стянут": "dryness", "сух": "dryness", "шелуш": "dryness", "обезвож": "dehydration", "dry": "dryness",
"flak": "dryness", "peel": "dryness",
"покрасн": "redness", "краснот": "redness", "раздраж": "sensitivity", "чувствитель": "sensitivity",
"sensitive": "sensitivity", "redness": "redness", "red ": "redness", "itch": "redness",
"блеск": "oiliness", "блест": "oiliness", "жирн": "oiliness", "oily": "oiliness", "shine": "oiliness",
"чёрных точ": "clogged-pores", "черных точ": "clogged-pores", "blackhead": "clogged-pores",
"расширенн пор": "clogged-pores", "поры": "clogged-pores", "pores": "clogged-pores",
"высып": "acne", "акн": "acne", "прыщ": "acne", "breakout": "acne", "угр": "acne",
"пигмент": "pigmentation", "пятн": "pigmentation", "веснушк": "pigmentation", "dark spot": "pigmentation",
"морщин": "fine-lines", "wrinkle": "fine-lines", "мимическ": "fine-lines",
"туск": "dullness", "dull": "dullness", "dullness": "dullness",
"тёмн круг": "dark-circles", "темн круг": "dark-circles", "dark circle": "dark-circles", "круги под глаз": "dark-circles",
"отёк": "puffiness", "отек": "puffiness", "puffin": "puffiness", "опух": "puffiness",
"чёрных угр": "clogged-pores",
}
TYPE_RULES = [
("sensitive", 2, ["redness", "sensitivity"]),
("dry", 2, ["dryness"]),
("oily", 2, ["oiliness", "clogged-pores"]),
("combination", 0, []), # базовый исход
]
# точные имена concerns для структурированного ввода (symptoms:[...])
CONCERN_ALIASES = {c: c for c in ["dryness", "dehydration", "sensitivity", "redness", "oiliness",
"clogged-pores", "acne", "pigmentation", "photoaging", "fine-lines",
"dullness", "dark-circles", "puffiness", "texture"]}
ROUTINE_ORDER = ["cleanse", "toner", "exfoliate", "serum", "mask", "spot", "spf",
"body-cleanse", "body-care", "hair-wash", "tool"]
def analyze_skin(args: dict) -> dict:
"""Анализ: self-report (текст/чек-лист) + опциональный vision-отчёт агента-фотографа.
args:
self_report: str — свободный текст жалоб (опционально)
symptoms: [str] — структурированные симптомы (опционально)
photo_report: dict/str — отчёт vision-модели по селфи (опционально)
age: int, climate: str — контекст (опционально)
"""
signals: list[str] = []
src = {"self_report": 0, "symptoms": 0, "photo_report": 0}
if args.get("self_report"):
text = str(args["self_report"]).lower()
for kw, concern in SYMPTOM_MAP.items():
if kw in text and concern not in signals:
signals.append(concern)
src["self_report"] = len([c for c in signals])
for s in args.get("symptoms") or []:
raw = str(s).lower().strip()
c = CONCERN_ALIASES.get(raw) or SYMPTOM_MAP.get(raw) or raw
if c and c not in signals:
signals.append(c)
src["symptoms"] += 1
photo = args.get("photo_report")
if photo:
blob = photo if isinstance(photo, str) else json.dumps(photo, ensure_ascii=False)
blob = blob.lower()
for kw, concern in SYMPTOM_MAP.items():
if kw in blob and concern not in signals:
signals.append(concern)
src["photo_report"] = len([c for c in signals]) - src["self_report"] - src["symptoms"]
# тип кожи: голосование по правилам
scores = {"sensitive": 0, "dry": 0, "oily": 0}
for concern in signals:
if concern in ("redness", "sensitivity"):
scores["sensitive"] += 1
if concern in ("dryness", "dehydration", "fine-lines"):
scores["dry"] += 1
if concern in ("oiliness", "clogged-pores", "acne"):
scores["oily"] += 1
top = max(scores.values())
winners = [k for k, v in scores.items() if v == top]
skin_type = winners[0] if len(winners) == 1 else "combination" # ничья/пусто → честный combination
hydration = 5 # 0..10, нейтрально
if "dryness" in signals:
hydration -= 3
if "oiliness" in signals:
hydration += 2
if "dehydration" in signals:
hydration -= 2
hydration = max(0, min(10, hydration))
barrier = "ok"
if "redness" in signals or "sensitivity" in signals:
barrier = "compromised"
if "acne" in signals and barrier == "compromised":
barrier = "compromised+breakouts"
recommendations = [p["id"] for p in rank_products(signals, skin_type)]
return {
"skin_type": skin_type,
"concerns": signals,
"hydration_index_0_10": hydration,
"barrier": barrier,
"sources": src,
"routine_focus": routine_focus(signals),
"recommended_product_ids": recommendations[:5],
"disclaimer": "Не медицинская консультация. При стойком раздражении/акне — к дерматологу.",
}
def routine_focus(concerns: list[str]) -> list[str]:
focus = []
if "dryness" in concerns or "dehydration" in concerns:
focus.append("увлажнение: гиалурон + центелла, тонер-эссенция до сыворотки")
if "sensitivity" in concerns or "redness" in concerns:
focus.append("успокоение: центелла 77%, без кислот и ретинола первые 2 недели")
if "acne" in concerns or "clogged-pores" in concerns:
focus.append("очищение: гидрофильное масло + мягкий BHA, не щёлочь")
if "pigmentation" in concerns or "photoaging" in concerns:
focus.append("SPF каждый день + ретинол на ночь (вводить постепенно)")
if "fine-lines" in concerns:
focus.append("анти-эйдж: ретинол 2–3 р/нед, коллагеновые маски")
if "dullness" in concerns:
focus.append("тон/сияние: пэды с кислотами 2 р/нед, маска-плёнка")
if not focus:
focus.append("база: очищение → тонер → сыворотка → крем → SPF")
return focus
def rank_products(concerns: list[str], skin_type: str) -> list[dict]:
scored = []
for p in CATALOG["products"]:
score = 0
for c in concerns:
if c in p["concerns"]:
score += 3
if skin_type == "sensitive" and ("retinol" in " ".join(p["actives"]) or "acid" in " ".join(p["actives"])):
score -= 2
if skin_type == "dry" and "hyaluronic" in " ".join(p["actives"]):
score += 2
if skin_type == "oily" and p["routine_step"] in ("cleanse", "exfoliate"):
score += 1
if score > 0:
scored.append((score, p))
scored.sort(key=lambda x: (-x[0], x[1]["price"]))
return [p for _, p in scored] or CATALOG["products"][:3]
# ── Рекомендации / поиск ──────────────────────────────────────────────────────
def recommend(args: dict) -> dict:
concerns = [CONCERN_ALIASES.get(c.lower().strip()) or SYMPTOM_MAP.get(c.lower().strip(), c.lower().strip())
for c in (args.get("concerns") or [])]
skin_type = args.get("skin_type") or "combination"
budget = args.get("budget_max")
picks = rank_products(concerns, skin_type)
if budget:
picks = [p for p in picks if p["price"] <= float(budget)]
return {
"matched": len(picks),
"products": [
{"id": p["id"], "brand": p["brand"], "title": p["title"], "price": p["price"],
"why": ", ".join(c for c in concerns if c in p["concerns"]) or "базовый уход"}
for p in picks[: int(args.get("limit") or 5)]
],
}
def search(args: dict) -> dict:
q = (args.get("query") or "").lower().strip()
cat = args.get("category")
out = []
for p in CATALOG["products"]:
if cat and p["category"] != cat:
continue
if q and not (q in p["title"].lower() or q in p["brand"].lower() or q in p["sub"].lower()
or any(q in a for a in p["actives"])):
continue
out.append({"id": p["id"], "brand": p["brand"], "title": p["title"], "price": p["price"],
"category": p["category"], "sub": p["sub"]})
return {"found": len(out), "products": out[: int(args.get("limit") or 20)]}
# ── Заказы и доставка ─────────────────────────────────────────────────────────
DELIVERY = {
"minsk_courier": {"label": "Курьер по Минску", "eta": "сегодня/завтра", "price": 7.0, "free_from": 80.0},
"europochta": {"label": "Европочта (РБ)", "eta": "2–3 дня", "price": 4.5, "free_from": 80.0},
"pickup": {"label": "Самовывоз (Минск, пр-т Независимости)", "eta": "через 2 часа", "price": 0.0, "free_from": 0.0},
}
def _load_orders() -> dict:
if ORDERS_PATH.exists():
return json.loads(ORDERS_PATH.read_text(encoding="utf-8"))
return {}
def _save_orders(orders: dict) -> None:
ORDERS_PATH.write_text(json.dumps(orders, ensure_ascii=False, indent=1), encoding="utf-8")
def place_order(args: dict) -> dict:
items = args.get("items") or []
if not items:
return {"error": "items обязателен: [{id, qty}]"}
lines, unknown = [], []
total = 0.0
for it in items:
p = PRODUCTS.get(it.get("id"))
if not p:
unknown.append(it.get("id"))
continue
qty = int(it.get("qty") or 1)
lines.append({"id": p["id"], "title": p["title"], "qty": qty, "price": p["price"], "sum": round(p["price"] * qty, 2)})
total += p["price"] * qty
if unknown:
return {"error": f"неизвестные товары: {unknown}", "hint": "tools/call search для поиска id"}
delivery_key = args.get("delivery") or "minsk_courier"
d = DELIVERY.get(delivery_key)
if not d:
return {"error": f"нет такой доставки: {delivery_key}", "available": list(DELIVERY)}
d_price = 0.0 if (d["free_from"] and total >= d["free_from"]) else d["price"]
oid = "SB-" + uuid.uuid4().hex[:8].upper()
order = {
"order_id": oid,
"plan": args.get("plan") if args.get("plan") in ("full", "lite") else "full",
"order_id": oid,
"items": lines,
"subtotal": round(total, 2),
"delivery": {"key": delivery_key, **d, "charged": d_price},
"total": round(total + d_price, 2),
"customer": {"name": args.get("name") or "", "phone": args.get("phone") or "",
"address": args.get("address") or ""},
"payment": {"status": "pending", "method": args.get("pay_method") or "cash_on_delivery",
"pay_link": args.get("pay_link")}, # платёжку подключает агент-хост
"status": "placed",
"created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
orders = _load_orders()
orders[oid] = order
_save_orders(orders)
return {
"order_id": oid, "status": "placed", "total": order["total"],
"delivery_eta": d["eta"], "delivery_price": d_price,
"next": f"Статус: tools/call check_order {{\"order_id\": \"{oid}\"}}. "
f"Оплата: pay_method=cash_on_delivery или передайте pay_link от платёжного агента.",
}
def check_order(args: dict) -> dict:
oid = args.get("order_id") or ""
o = _load_orders().get(oid)
if not o:
return {"error": f"заказ {oid} не найден", "known": list(_load_orders())[:10]}
out = {"order_id": oid, "plan": o.get("plan", "full"), "status": o["status"],
"payment": (o.get("payment") or {}).get("status")}
if o.get("kind") == "request":
out.update({"kind": "request", "note": o.get("note", ""),
"products": o.get("products", []),
"customer": o.get("customer", {})})
return out
out.update({"total": o["total"], "delivery": o["delivery"]["label"], "eta": o["delivery"]["eta"],
"items": [{"title": l["title"], "qty": l["qty"]} for l in o["items"]]})
return out
def submit_request(args: dict) -> dict:
"""Заявка из витрины Lite-плана: без корзины/оплаты, пишем в ту же базу orders.json с plan=lite.
Витрина формы («Хочу это», «Подобрать рутину») → request SB-R-XXXXXXXX → менеджер/агент
договаривается лично. Когда магазин переходит на Full — та же запись дооформляется
в заказ (check_order видит статус request).
"""
contact = (args.get("phone") or args.get("email") or "").strip()
if not contact and not (args.get("name") or "").strip():
return {"error": "нужен хотя бы один контакт: phone или email (или name)"}
oid = "SB-R-" + uuid.uuid4().hex[:8].upper()
rec = {
"order_id": oid,
"plan": "lite",
"kind": "request",
"note": (args.get("note") or "").strip(),
"products": args.get("products") or [],
"customer": {"name": args.get("name") or "", "phone": args.get("phone") or "",
"email": args.get("email") or "", "address": args.get("address") or ""},
"payment": {"status": "none", "method": "none", "pay_link": None},
"total": 0.0,
"status": "request",
"created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
orders = _load_orders()
orders[oid] = rec
_save_orders(orders)
return {"order_id": oid, "status": "request", "plan": "lite",
"next": "Магазин свяжется по контакту. Статус: tools/call check_order {\"order_id\": \"%s\"}" % oid}
def delivery_options(args: dict) -> dict:
total = float(args.get("cart_total") or 0)
return {"options": [
{"key": k, "label": v["label"], "eta": v["eta"],
"price": 0.0 if (v["free_from"] and total >= v["free_from"]) else v["price"]}
for k, v in DELIVERY.items()]}
# ── Инструкции по применению ──────────────────────────────────────────────────
INSTRUCTIONS = {
"cleanse": "Нанести на сухую кожу, помассировать 30–60 сек, смыть тёплой водой. Утром можно мягче.",
"toner": "После очищения протереть лицо/нанести ладонями, не вытирать — следующий шаг на влажную кожу.",
"exfoliate": "2–3 раза в неделю, не в один день с ретинолом. При покраснении — прекратить.",
"serum": "3–4 капли на влажную кожу, дать впитаться 1–2 мин. Ретинол — только на ночь, старт 2–3 р/нед.",
"mask": "1–2 раза в неделю на очищенную кожу, 20–30 мин, снять плёнку снизу вверх.",
"spot": "Патчи — 10–15 мин на зону под глазами, после — лёгкий крем.",
"spf": "Финальный утренний шаг, 2 пальца средства, обновлять каждые 2–3 часа при солнце. Обязателен при ретиноле/кислотах.",
"body-cleanse": "Ежедневно, мягкой губкой, завершить увлажнением.",
"body-care": "На влажную кожу рук после каждого мытья.",
"hair-wash": "Втирать в кожу головы 1–2 мин, смыть; бальзам — только по длине.",
"tool": "Спонж смочить тёплой водой, использовать с пенкой; промывать и сушить после каждого применения.",
}
def usage_instructions(args: dict) -> dict:
ids = args.get("product_ids") or args.get("ids") or []
if isinstance(ids, str):
ids = [ids]
got = []
for pid in ids:
p = PRODUCTS.get(pid)
if p:
got.append({"id": pid, "title": p["title"], "how": INSTRUCTIONS.get(p["routine_step"], "по инструкции бренда"),
"step_order": ROUTINE_ORDER.index(p["routine_step"]) + 1 if p["routine_step"] in ROUTINE_ORDER else 99,
"caution": p.get("caution")})
if not got:
return {"error": "укажите product_ids (см. search/recommend)", "known_ids": list(PRODUCTS)}
got.sort(key=lambda x: x["step_order"])
plan = " → ".join(g["title"].split()[0] for g in got)
return {"steps": got, "routine_summary": f"Порядок нанесения: {plan}. Утро: очищение→увлажнение→SPF; вечер: очищение→акт (ретинол/кислоты)→крем.",
"general": "Новое средство — сначала на сгиб локтя 24ч. Не сочетать ретинол с кислотами в один день."}
# ── Диспетчер инструментов ────────────────────────────────────────────────────
TOOLS = {
"analyze_skin": ("Анализ состояния кожи: self_report/symptoms/photo_report → тип, concerns, фокус рутины",
["self_report?", "symptoms?", "photo_report?", "age?", "climate?"]),
"recommend": ("Подбор продуктов: concerns+skin_type+budget_max → карточки", ["concerns?", "skin_type?", "budget_max?", "limit?"]),
"search": ("Поиск по каталогу", ["query?", "category?", "limit?"]),
"place_order": ("Оформить заказ (Full-план)", ["items[{id,qty}]", "delivery?", "name?", "phone?", "address?", "pay_method?", "plan?"]),
"submit_request": ("Заявка из витрины Lite-плана: без корзины/оплаты → orders.json plan=lite, статус request", ["phone?", "email?", "name?", "note?", "products?"]),
"check_order": ("Статус заказа и доставки", ["order_id"]),
"delivery_options": ("Варианты доставки и цены", ["cart_total?"]),
"usage_instructions": ("Инструкции по применению + порядок рутины", ["product_ids"]),
}
def handle(request: dict) -> dict:
"""Единая точка входа. Понимает MCP tools/call и простую форму {action, args}."""
if not isinstance(request, dict):
return {"error": "request должен быть JSON-объектом"}
method = request.get("method")
if method == "tools/list":
return {"tools": [{"name": n, "description": d, "args": a} for n, (d, a) in TOOLS.items()]}
if method == "initialize":
return {"protocol": "shkor-assistant/1", "tools": list(TOOLS), "shop": CATALOG["shop"]}
if method == "tools/call":
params = request.get("params") or {}
name, arguments = params.get("name"), params.get("arguments") or {}
else:
name, arguments = request.get("action"), request.get("args") or request.get("arguments") or {}
if name not in TOOLS:
return {"error": f"неизвестный инструмент {name!r}", "available": list(TOOLS)}
fn = {"analyze_skin": analyze_skin, "recommend": recommend, "search": search,
"place_order": place_order, "submit_request": submit_request, "check_order": check_order,
"delivery_options": delivery_options, "usage_instructions": usage_instructions}[name]
try:
result = fn(arguments if isinstance(arguments, dict) else {})
except Exception as e: # noqa: BLE001 — ассистент отвечает структурой, не падает
return {"error": f"{type(e).__name__}: {e}"}
rid = request.get("id")
out = {"result": result}
if rid is not None:
out["id"] = rid
return out
if __name__ == "__main__":
import sys
payload = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {"method": "tools/list"}
print(json.dumps(handle(payload), ensure_ascii=False, indent=1))

View file

@ -1,19 +0,0 @@
{
"shop": "Shkor beauty",
"currency": "BYN",
"note": "Каталог синхронизирован с прототипом витрины artifacts/shkor-beauty-prototype.html (t-261005115918-d38b36)",
"products": [
{"id": "p01", "category": "face", "sub": "Сыворотки", "brand": "SKIN627", "title": "Увлажняющая сыворотка для лица Hyaluron Centella Ampoule", "price": 35.00, "old_price": 50.00, "concerns": ["dryness", "sensitivity", "dehydration"], "actives": ["hyaluronic acid", "centella"], "routine_step": "serum"},
{"id": "p02", "category": "face", "sub": "Пэды", "brand": "JMsolution", "title": "Увлажняющие пэды с гиалуроновой кислотой Multi Hyaluronic", "price": 59.00, "concerns": ["dryness", "dullness"], "actives": ["multi hyaluronic"], "routine_step": "exfoliate"},
{"id": "p03", "category": "face", "sub": "SPF", "brand": "Beauty of Joseon", "title": "Солнцезащитный гель Relief Sun SPF50+ PA++++", "price": 49.00, "concerns": ["photoaging", "pigmentation", "sensitivity"], "actives": ["SPF50+", "rice extract"], "routine_step": "spf"},
{"id": "p04", "category": "face", "sub": "Маски", "brand": "Biodance", "title": "Коллагеновая маска-пленка Collagen Wrapping Peel-off Mask", "price": 12.00, "concerns": ["dullness", "fine-lines"], "actives": ["collagen"], "routine_step": "mask"},
{"id": "p05", "category": "face", "sub": "Тонеры", "brand": "Anua", "title": "Успокаивающий тонер с сердечником Centella Toner 77%", "price": 62.00, "concerns": ["sensitivity", "redness"], "actives": ["centella 77%"], "routine_step": "toner"},
{"id": "p06", "category": "face", "sub": "Очищение", "brand": "Banila Co", "title": "Гидрофильное масло Clean It Zero Original", "price": 54.00, "concerns": ["clogged-pores", "makeup-removal"], "actives": ["cleansing oil"], "routine_step": "cleanse"},
{"id": "p07", "category": "face", "sub": "Сыворотки", "brand": "SKIN627", "title": "Сыворотка для лица с ретинолом Retinol Pantenol Ampoule", "price": 35.00, "concerns": ["fine-lines", "texture", "acne"], "actives": ["retinol", "panthenol"], "routine_step": "serum", "caution": "ретинол: вводить 2–3 раза в неделю, только на ночь, обязателен SPF утром"},
{"id": "p08", "category": "face", "sub": "Патчи", "brand": "A'PIEU", "title": "Патчи для глаз с молочной кислотой Milk One-Day Perfector", "price": 18.00, "concerns": ["dark-circles", "puffiness"], "actives": ["lactic acid", "milk"], "routine_step": "spot"},
{"id": "p09", "category": "body", "sub": "Для душа", "brand": "WEPROVE", "title": "Обновляющий гель для душа Renewing Aloe Shower Gel", "price": 29.00, "concerns": ["body-dryness"], "actives": ["aloe"], "routine_step": "body-cleanse"},
{"id": "p10", "category": "hair", "sub": "Шампуни", "brand": "WEPROVE", "title": "Восстанавливающий шампунь Daily Hair Repairing Shampoo", "price": 23.60, "concerns": ["hair-damage"], "actives": ["repairing complex"], "routine_step": "hair-wash"},
{"id": "p11", "category": "body", "sub": "Кремы для рук", "brand": "Care:Nel", "title": "Питательный крем для рук с ши и центеллой", "price": 14.00, "concerns": ["body-dryness"], "actives": ["shea", "centella"], "routine_step": "body-care"},
{"id": "p12", "category": "accessories", "sub": "Аксессуары", "brand": "Ciracle", "title": "Силиконовый спонж для умывания с чехлом", "price": 9.00, "concerns": [], "actives": [], "routine_step": "tool"}
]
}

View file

@ -1,159 +0,0 @@
#!/usr/bin/env python3
"""Shkor Beauty Assistant — универсальный коннектор.
Один и тот же ассистент — три способа подключения для ЛЮБОГО агента:
1) STDIO MCP-стиль (Claude/любой MCP-клиент):
assistant → connector.py (JSONL: {"method":"tools/list"} по строке)
в конфиге агента:
{"mcpServers": {"shkor": {"command": "python3", "args": [".../connector.py"], "transport": "stdio"}}}
2) HTTP JSON API (агент = просто HTTP-клиент):
connector.py --http 127.0.0.1:8791
POST /tools/list | POST /call {"name": "...", "arguments": {...}}
3) CLI разовый вызов (curl/крон/скрипт):
connector.py --call analyze_skin --args '{"self_report":"стянутость и покраснение"}'
4) Демо-чат (для живой проверки человеком):
connector.py --demo
Форма запроса принимает MCP tools/call и простую {action, args} — см. assistant.handle().
"""
from __future__ import annotations
import argparse
import json
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from assistant import handle # noqa: E402
# ── 1) STDIO: JSONL, одна строка = один запрос ───────────────────────────────
def serve_stdio() -> None:
print(json.dumps({"ok": True, "transport": "stdio-jsonl",
"hint": "отправляйте JSON-строки, читайте ответ построчно"}, ensure_ascii=False), flush=True)
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
resp = handle(json.loads(line))
except json.JSONDecodeError as e:
resp = {"error": f"bad json: {e}"}
print(json.dumps(resp, ensure_ascii=False), flush=True)
# ── 2) HTTP ──────────────────────────────────────────────────────────────────
def serve_http(addr: str) -> None:
host, port = addr.split(":")
port_i = int(port)
class H(BaseHTTPRequestHandler):
def _send(self, code: int, payload: dict) -> None:
body = json.dumps(payload, ensure_ascii=False).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*") # витрина на другом домене: форма заявки → /call
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
self.wfile.write(body)
def do_OPTIONS(self) -> None: # preflight для формы витрины (fetch с другого origin)
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.send_header("Content-Length", "0")
self.end_headers()
def do_GET(self) -> None: # health / открыть в браузере
if self.path in ("/", "/health"):
self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1,
"tools": "POST /tools/list", "call": "POST /call"})
else:
self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]})
def do_POST(self) -> None:
try:
n = int(self.headers.get("Content-Length") or 0)
data = json.loads(self.rfile.read(n) or b"{}")
except Exception as e: # noqa: BLE001
self._send(400, {"error": f"bad request: {e}"})
return
if self.path == "/tools/list":
self._send(200, handle({"method": "tools/list"}))
elif self.path == "/call":
self._send(200, handle({"method": "tools/call", "params": data}))
else:
self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]})
def log_message(self, *a) -> None: # тише в логи
pass
srv = ThreadingHTTPServer((host, port_i), H)
print(f"shkor-assistant HTTP on http://{host}:{port_i} (POST /tools/list, POST /call)", file=sys.stderr)
srv.serve_forever()
# ── 4) Демо-чат ───────────────────────────────────────────────────────────────
DEMO = [
("analyze_skin", {"self_report": "кожа стянутая после умывания, лоб блестит к обеду, периодически покраснения"}),
("recommend", {"concerns": ["dryness", "redness"], "skin_type": "sensitive", "budget_max": 70}),
("place_order", {"items": [{"id": "p05", "qty": 1}, {"id": "p03", "qty": 1}], "delivery": "minsk_courier",
"name": "Тест", "phone": "+375XX0000000"}),
("usage_instructions", {"product_ids": ["p05", "p03"]}),
("check_order", {}), # order_id подставится после place_order
]
def run_demo() -> None:
last_order = None
for action, args in DEMO:
if action == "check_order":
if not last_order:
continue
args = {"order_id": last_order}
print(f"\n▶ {action} {json.dumps(args, ensure_ascii=False)}")
resp = handle({"method": "tools/call", "params": {"name": action, "arguments": args}})
print(json.dumps(resp.get("result", resp), ensure_ascii=False, indent=1)[:1200])
if action == "place_order" and (resp.get("result") or {}).get("order_id"):
last_order = resp["result"]["order_id"]
def main() -> None:
ap = argparse.ArgumentParser(description="Shkor Beauty Assistant connector")
ap.add_argument("--http", metavar="HOST:PORT", help="поднять HTTP API")
ap.add_argument("--stdio", action="store_true", help="STDIO JSONL (MCP-стиль)")
ap.add_argument("--call", metavar="TOOL", help="разовый вызов инструмента")
ap.add_argument("--args", default="{}", help="JSON-аргументы для --call")
ap.add_argument("--list-tools", action="store_true")
ap.add_argument("--demo", action="store_true")
a = ap.parse_args()
if a.http:
serve_http(a.http)
elif a.stdio:
serve_stdio()
elif a.list_tools:
print(json.dumps(handle({"method": "tools/list"}), ensure_ascii=False, indent=1))
elif a.demo:
run_demo()
elif a.call:
try:
arguments = json.loads(a.args)
except json.JSONDecodeError as e:
sys.exit(f"bad --args json: {e}")
print(json.dumps(handle({"method": "tools/call", "params": {"name": a.call, "arguments": arguments}}),
ensure_ascii=False, indent=1))
else:
ap.print_help()
if __name__ == "__main__":
main()

View file

@ -1,250 +0,0 @@
#!/usr/bin/env python3
"""Генератор юр-страниц Shkor beauty (t-261008154151-faa550).
Privacy Policy / Terms of Service / Cookie & Disclaimer / About & Contacts.
Единый шаблон = тот же визуальный язык, что у витрины. Оператор сервиса:
Shkor Labs LLC (Wyoming, USA), alex.shkor@gmail.com.
"""
from pathlib import Path
OUT = Path("/var/www/beauty")
OUT.mkdir(parents=True, exist_ok=True)
CSS = """
:root{--ink:#1c1a17;--paper:#faf7f2;--line:#e4ddd2;--acc:#0e7a5f;--muted:#6f6a61;
--sans:"Segoe UI",-apple-system,Arial,sans-serif;--mono:ui-monospace,Menlo,Consolas,monospace}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--paper);color:var(--ink);font-family:var(--sans);line-height:1.65;font-size:16px}
.wrap{max-width:820px;margin:0 auto;padding:0 20px}
header{border-bottom:1px solid var(--line);background:#fff}
.hbar{display:flex;align-items:center;justify-content:space-between;padding:16px 0}
.logo{font-size:20px;font-weight:700}
.logo span{color:var(--acc)}
.logo small{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
nav a{margin-left:16px;font-size:14px;color:var(--muted)}
nav a:hover{color:var(--acc)}
main{padding:40px 0 60px}
h1{font-size:clamp(26px,4vw,36px);letter-spacing:-.02em;margin-bottom:6px}
.meta{font-family:var(--mono);font-size:12px;color:var(--muted);margin-bottom:28px}
h2{font-size:19px;margin:28px 0 10px}
p,li{color:#3d3a34;margin-bottom:10px}
ul{padding-left:22px}
a{color:var(--acc)}
.card{background:#fff;border:1px solid var(--line);border-radius:10px;padding:22px;margin-top:24px}
footer{padding:26px 0 40px;border-top:1px solid var(--line);color:var(--muted);font-size:13.5px}
footer .links{display:flex;gap:18px;margin-bottom:8px}
footer .links a{text-decoration:underline;color:var(--muted)}
footer .proto{font-family:var(--mono);font-size:12px;margin-top:6px}
"""
def page(slug: str, title: str, updated: str, body: str) -> None:
html = f"""<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} — Shkor beauty</title>
<meta name="description" content="{title} сервиса Shkor beauty. Оператор: Shkor Labs LLC.">
<style>{CSS}</style>
</head>
<body>
<header>
<div class="wrap hbar">
<a class="logo" href="/">Shkor <span>beauty</span><small>корейская косметика · Минск</small></a>
<nav>
<a href="/#catalog">Каталог</a><a href="/#plans">Магазинам</a><a href="/#request">Заявка</a>
</nav>
</div>
</header>
<main class="wrap">
<h1>{title}</h1>
<div class="meta">Last updated: {updated} · v1.0 · Operator: Shkor Labs LLC (Wyoming, USA)</div>
{body}
</main>
<footer>
<div class="wrap">
<div class="links">
<a href="/privacy.html">Privacy Policy</a>
<a href="/terms.html">Terms of Service</a>
<a href="/disclaimer.html">Disclaimer</a>
<a href="/about.html">About &amp; Contacts</a>
</div>
<div>Оператор сервиса: <b>Shkor Labs LLC</b> (Wyoming, USA) · <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a></div>
<div class="proto">собрано харнесом Simbiot · кооператив 42 · cil.by</div>
</div>
</footer>
</body>
</html>"""
(OUT / f"{slug}.html").write_text(html, encoding="utf-8")
print("ok", OUT / f"{slug}.html")
D = "2026-10-08"
page("privacy", "Privacy Policy", D, """
<p>Настоящая Политика конфиденциальности объясняет, какие данные собирает сервис Shkor beauty
(далее — «Сервис») и как они используются. Оператор Сервиса — <b>Shkor Labs LLC</b>, компания,
зарегистрированная в штате Вайоминг, США (далее — «Оператор»). Контакт: <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
<h2>1. Какие данные мы собираем</h2>
<ul>
<li><b>Заявка (Lite-план):</b> имя, телефон и текст заявки, которые вы сами указываете в форме «Оставить заявку».</li>
<li><b>Заказ (Full-план):</b> имя, телефон, адрес доставки и состав заказа.</li>
<li><b>Обращения:</b> письма на email Оператора и переписка в мессенджерах — в объёме, необходимом для ответа.</li>
<li><b>Технические данные:</b> анонимная статистика посещений (страницы-рефереры, устройство, страна) — без персональных идентификаторов.</li>
</ul>
<h2>2. Чего мы НЕ собираем</h2>
<ul>
<li>Данные банковских карт: при онлайн-оплате (Full-план) платёж проводит независимый платёжный агент;
данные карты вводятся на его защищённой странице и Оператору не передаются.</li>
<li>Фотографии кожи без явного согласия: если вы передаёте фотоотчёт AI-ассистенту, он обрабатывается
для анализа состояния кожи и не публикуется и не передаётся третьим лицам.</li>
</ul>
<h2>3. Зачем мы используем данные</h2>
<ul>
<li>связаться с вами по заявке и довести её до результата (подбор, заказ, доставка);</li>
<li>исполнить заказ и уведомлять о его статусе;</li>
<li>улучшать каталог и подбор ассистента на обезличенной статистике.</li>
</ul>
<h2>4. Хранение и срок</h2>
<p>Записи заявок и заказов хранятся в защищённой базе Оператора не дольше, чем это необходимо для
исполнения заказа и требований бухгалтерии (по умолчанию — до 3 лет), после чего удаляются или
обезличиваются.</p>
<h2>5. Передача третьим лицам</h2>
<p>Данные передаются только: (а) службе доставки — имя, телефон и адрес вашего заказа;
(б) платёжному агенту — минимально необходимый платёжный контекст; (в) в случаях, предусмотренных законом.</p>
<h2>6. Cookie</h2>
<p>Сервис использует минимальные cookie и локальное хранилище: сессия корзины и анонимная статистика.
Подробнее — в <a href="/disclaimer.html">Disclaimer</a>.</p>
<h2>7. Ваши права</h2>
<p>Вы можете запросить доступ к своим данным, их исправление или удаление письмом на
<a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>. Мы отвечаем в течение 30 дней.</p>
<h2>8. Изменения</h2>
<p>Актуальная версия Политики всегда доступна на этой странице; существенные изменения датируются выше.</p>
""")
page("terms", "Terms of Service", D, """
<p>Условия использования сервиса Shkor beauty (далее — «Условия»). Оператор Сервиса — <b>Shkor Labs LLC</b>
(Wyoming, USA), контакт <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>. Используя Сервис,
вы соглашаетесь с настоящими Условиями.</p>
<h2>1. Что такое Сервис</h2>
<p>Shkor beauty — витрина корейской косметики с AI-ассистентом: подбор ухода, заявки и заказы с доставкой
по Минску и Беларуси. Сервис предлагается в двух планах:</p>
<ul>
<li><b>Lite (базовый):</b> витрина + форма заявки. Заявка фиксируется в общей базе заказов, после чего
магазин связывается с вами лично для уточнения ассортимента, цены и способа расчёта.</li>
<li><b>Full:</b> полный каталог, корзина, онлайн-оплата и статусы доставки.</li>
</ul>
<h2>2. Заявки и заказы</h2>
<ul>
<li>Заявка (Lite) не является подтверждённой сделкой: цена и наличие подтверждаются при обратной связи.</li>
<li>Заказ (Full) считается принятым после подтверждения оплаты или согласия на оплату при получении.</li>
<li>Цены указаны в белорусских рублях (BYN) и могут меняться; актуальная цена — на витрине в момент заказа.</li>
</ul>
<h2>3. Доставка</h2>
<p>Курьер по Минску, Европочта по Беларуси и самовывоз. Сроки и стоимость показываются при оформлении
заявки/заказа; бесплатная доставка — от 80 BYN по каталогу витрины.</p>
<h2>4. Возврат и обмен</h2>
<ul>
<li>Товар надлежащего качества (без следов использования, в оригинальной упаковке) — возврат/обмен
в течение 14 дней с момента получения.</li>
<li>Средства гигиены и косметика с нарушенной упаковкой возврату не подлежат.</li>
<li>Для возврата напишите на <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a> с номером заказа.</li>
</ul>
<h2>5. Интеллектуальная собственность</h2>
<p>Материалы витрины, AI-подбор и программный код ассистента принадлежат Оператору. Товарные знаки
брендов принадлежат их правообладателям.</p>
<h2>6. Ограничение ответственности</h2>
<p>Сервис предоставляется «как есть». AI-подбор средств — рекомендательный характер и не является
медицинской консультацией (см. <a href="/disclaimer.html">Disclaimer</a>). Оператор не несёт
ответственности за индивидуальные реакции кожи и решения, принятые на основе рекомендаций без
ознакомления с составами и инструкциями.</p>
<h2>7. Применимое право</h2>
<p>Настоящие Условия регулируются правом, применимым к месту регистрации Оператора (Wyoming, USA),
без ущерба для обязательных потребительских прав покупателей по законодательству места доставки.</p>
""")
page("disclaimer", "Cookie &amp; Disclaimer", D, """
<p>Страница объединяет политику cookie и важные дисклеймеры сервиса Shkor beauty. Оператор —
<b>Shkor Labs LLC</b> (Wyoming, USA), <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
<h2>1. Не медицинская консультация</h2>
<div class="card">
<p><b>AI-ассистент и материалы витрины НЕ являются медицинской консультацией, диагностикой или лечением.</b>
Подбор средств — эвристическая рекомендация на основе вашего самоотчёта. Она не заменяет консультацию
дерматолога или врача. При заболеваниях кожи, беременности, приёме лекарственных средств и выраженных
реакциях — проконсультируйтесь со специалистом до применения средств.</p>
</div>
<h2>2. Косметика и индивидуальные реакции</h2>
<ul>
<li>Перед первым применением сделайте тест на чувствительность (небольшой участок кожи, 24–48 часов).</li>
<li>Активы (ретинол, кислоты, витамин C) вводите постепенно; ретинол — только на ночь, SPF утром обязателен.</li>
<li>Ознакомьтесь с составом на упаковке: при аллергии на компонент средство не используйте.</li>
</ul>
<h2>3. Cookie</h2>
<ul>
<li><b>Необходимые:</b> сессия корзины и защита форм. Не отключаются.</li>
<li><b>Аналитические:</b> анонимная статистика посещений для улучшения витрины. Без персональных идентификаторов и без передачи рекламным сетям.</li>
<li><b>Рекламных и трекинговых cookie сторонних сетей — нет.</b></li>
</ul>
<p>Большинство браузеров позволяет управлять cookie в настройках; отключение необходимых cookie
может сломать корзину и отправку заявки.</p>
<h2>4. Внешние ссылки</h2>
<p>Витрина может ссылаться на сайты брендов и службы доставки — за их содержание Оператор
ответственности не несёт.</p>
<h2>5. Вопросы</h2>
<p>Любые вопросы по дисклеймерам и cookie — <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
""")
page("about", "About &amp; Contacts", D, """
<p><b>Shkor beauty</b> — AI-ассистент для магазинов корейской косметики: витрина, которая сама
разбирает состояние кожи клиента, собирает рутину и доводит заявку до заказа.</p>
<h2>Кто мы</h2>
<p>Оператор сервиса — <b>Shkor Labs LLC</b>, компания, зарегистрированная в штате Вайоминг, США.
Проект развивается совместно с инженерным кооперативом «42» (cil.by): витрина и ассистент собраны
ритмичным агентом Simbiot.</p>
<h2>Что мы делаем</h2>
<ul>
<li><b>Витрина K-beauty:</b> каталог проверенных брендов — Anua, Beauty of Joseon, JMsolution,
SKIN627, Biodance и другие, доставка по Минску и Беларуси.</li>
<li><b>AI beauty-ассистент (B2B):</b> агентный API analyze → recommend → order, который любой
магазин или AI-агент может подключить к своему сайту (MCP/STDIO, HTTP, CLI).</li>
<li><b>Два плана подключения:</b> <b>Lite</b> — витрина + заявки в общую базу; <b>Full</b> — полный
каталог, корзина, онлайн-оплата и статусы доставки.</li>
</ul>
<h2>Контакты</h2>
<div class="card">
<ul>
<li>Email: <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a></li>
<li>Заявка: форма на <a href="/#request">главной странице</a> — ответим в течение дня</li>
<li>Юрлицо-оператор: Shkor Labs LLC (Wyoming, USA)</li>
</ul>
</div>
<h2>Магазинам: подключение ассистента</h2>
<p>Ассистент подключается к любому сайту или AI-агенту за один шаг: JSONL поверх STDIO (MCP-стиль),
HTTP JSON API или разовый CLI-вызов. Каталог масштабируется без правок кода. Для пилота напишите
на email — подключим витрину с формой заявок (Lite) и покажем e2e-сценарий заказа.</p>
""")

View file

@ -1,158 +0,0 @@
{
"SB-R-1D8C99C8": {
"order_id": "SB-R-1D8C99C8",
"plan": "lite",
"kind": "request",
"note": "Хочу сыворотку для сухой кожи",
"products": [
"p01"
],
"customer": {
"name": "Тест Тестов",
"phone": "+375291234567",
"email": "",
"address": ""
},
"payment": {
"status": "none",
"method": "none",
"pay_link": null
},
"total": 0.0,
"status": "request",
"created": "2026-10-08T16:03:25Z"
},
"SB-55B78647": {
"order_id": "SB-55B78647",
"plan": "full",
"items": [
{
"id": "p05",
"title": "Успокаивающий тонер с сердечником Centella Toner 77%",
"qty": 1,
"price": 62.0,
"sum": 62.0
}
],
"subtotal": 62.0,
"delivery": {
"key": "minsk_courier",
"label": "Курьер по Минску",
"eta": "сегодня/завтра",
"price": 7.0,
"free_from": 80.0,
"charged": 7.0
},
"total": 69.0,
"customer": {
"name": "",
"phone": "",
"address": ""
},
"payment": {
"status": "pending",
"method": "cash_on_delivery",
"pay_link": null
},
"status": "placed",
"created": "2026-10-08T16:03:25Z"
},
"SB-8AC083A5": {
"order_id": "SB-8AC083A5",
"plan": "lite",
"items": [
{
"id": "p03",
"title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
"qty": 1,
"price": 49.0,
"sum": 49.0
}
],
"subtotal": 49.0,
"delivery": {
"key": "minsk_courier",
"label": "Курьер по Минску",
"eta": "сегодня/завтра",
"price": 7.0,
"free_from": 80.0,
"charged": 7.0
},
"total": 56.0,
"customer": {
"name": "",
"phone": "",
"address": ""
},
"payment": {
"status": "pending",
"method": "cash_on_delivery",
"pay_link": null
},
"status": "placed",
"created": "2026-10-08T16:03:25Z"
},
"SB-R-7B23D4D8": {
"order_id": "SB-R-7B23D4D8",
"plan": "lite",
"kind": "request",
"note": "Проверка формы с витрины (e2e)",
"products": [],
"customer": {
"name": "E2E Тест",
"phone": "+375290000000",
"email": "",
"address": ""
},
"payment": {
"status": "none",
"method": "none",
"pay_link": null
},
"total": 0.0,
"status": "request",
"created": "2026-10-08T16:08:02Z"
},
"SB-B352DCBF": {
"order_id": "SB-B352DCBF",
"plan": "full",
"items": [
{
"id": "p05",
"title": "Успокаивающий тонер с сердечником Centella Toner 77%",
"qty": 1,
"price": 62.0,
"sum": 62.0
},
{
"id": "p03",
"title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
"qty": 1,
"price": 49.0,
"sum": 49.0
}
],
"subtotal": 111.0,
"delivery": {
"key": "minsk_courier",
"label": "Курьер по Минску",
"eta": "сегодня/завтра",
"price": 7.0,
"free_from": 80.0,
"charged": 0.0
},
"total": 111.0,
"customer": {
"name": "Тест",
"phone": "+375XX0000000",
"address": ""
},
"payment": {
"status": "pending",
"method": "cash_on_delivery",
"pay_link": null
},
"status": "placed",
"created": "2026-10-08T16:08:11Z"
}
}

View file

@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>42 Labs — условия студии и инвестиционной программы</title>
<style>
:root{--paper:#faf8f3;--ink:#15150f;--signal:#FF4F00;--dim:#6b675e;--line:#d8d3c8;--card:#f3f0e8;--ok:#1d7a3e}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif}
.wrap{max-width:980px;margin:0 auto;padding:56px 24px 100px}
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:40px}
h1{font-size:34px;letter-spacing:-.02em;margin-bottom:8px}
.sub{color:var(--dim)}
h2{font-size:22px;margin:44px 0 14px;padding-top:18px;border-top:2px solid var(--line)}
p,li{margin-bottom:10px}
ul{padding-left:22px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px 22px;margin:16px 0}
.formula{font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--card);border-left:4px solid var(--signal);padding:12px 16px;margin:14px 0;border-radius:0 8px 8px 0}
table{width:100%;border-collapse:collapse;margin:16px 0}
th,td{border:1px solid var(--line);padding:10px 12px;text-align:left}
th{background:var(--card)}
footer{margin-top:56px;color:var(--dim);font-size:14px;border-top:2px solid var(--line);padding-top:16px}
a{color:var(--signal)}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>42 Labs — условия студии</h1>
<p class="sub">Участие · Инвестиционная программа · Управление. Редакция октябрь 2026. Не заменяет договор; при расхождении действует договор.</p>
</header>
<h2>1. Как устроено участие</h2>
<p>Набор в студию — <strong>раз в квартал</strong>. Условия участия подробно: <a href="https://42.cil.by/">42.cil.by</a>, объяснение договора: <a href="https://cil.by/static/dogovor_obiascenie.html">cil.by/static/dogovor_obiascenie.html</a>.</p>
<ul>
<li>Проект на 100% твой: студия не берёт долю в капитале, код, бренд и клиенты твои с первого дня.</li>
<li>Платишь большее из двух и только его: месячный взнос <strong>$999</strong> или <strong>[X]% от выручки проекта</strong>. Месяца, где платишь и то и другое, не существует.</li>
<li>Неоплата не расторгает договор: долг индексируется только вверх вместе с биткоином (CME CF BRR, запасной источник CoinGecko):</li>
</ul>
<div class="formula">Долг = Транш × max(1, BTC сегодня / BTC тогда) — только вверх, без процентов и множителей</div>
<ul>
<li>Возврат — из выручки проекта или прироста зарплаты. Личным долгом не становится. Выход — в любой месяц.</li>
</ul>
<h2>2. Инвестиционная программа (SAFE MFN)</h2>
<p>Инвесторы вкладываются по <strong>SAFE с MFN</strong> (most favored nation). Голос инвестора определяется на момент инвестиции:</p>
<div class="formula">Голоса = сколько BTC можно было купить на сумму инвестиции в момент её совершения</div>
<p>Голоса фиксируются в момент входа и дальше не переоцениваются — растёт или падает биткоин после инвестиции, число голосов не меняется.</p>
<h2>3. Квартальные уровни инвестиций студии</h2>
<p>Раз в квартал студия распределяет инвестиции по уровням:</p>
<table>
<tr><th>Уровень</th><th>Размер инвестиции</th></tr>
<tr><td>1</td><td>$1 000</td></tr>
<tr><td>2</td><td>$10 000</td></tr>
<tr><td>3</td><td>$100 000</td></tr>
<tr><td>4</td><td>$1 000 000</td></tr>
</table>
<ul>
<li>Подаваться нужно <strong>на текущий уровень</strong> — перепрыгивать уровни нельзя.</li>
<li>Количество победителей на уровне регулирует правление. Сейчас — <strong>по 1 победителю на уровень</strong>.</li>
</ul>
<h2>4. Ежедневные звонки: 3 через 3</h2>
<p>В рабочие дни звонки идут по схеме «3 через 3». Время распределяется <strong>пропорционально текущим голосам инвесторов</strong>. Если у инвестора ноль голосов — ему отводится <strong>3 минуты</strong>.</p>
<footer>
42 Labs · Shkor Labs LLC (Вайоминг) · ПК «Друзья Коллективного Разума» (Минск) · <a href="https://42.cil.by/">42.cil.by</a><br>
Эта страница объясняет условия, но не заменяет договор. При расхождении действует договор.
</footer>
</div>
</body>
</html>

View file

@ -1,45 +0,0 @@
# 42 Labs — условия студии
Редакция октябрь 2026. Не заменяет договор; при расхождении действует договор.
Подробно: [42.cil.by](https://42.cil.by/) · Объяснение договора: [cil.by/static/dogovor_obiascenie.html](https://cil.by/static/dogovor_obiascenie.html)
## 1. Как устроено участие
Набор в студию — **раз в квартал**.
- Проект на 100% твой: студия не берёт долю в капитале, код, бренд и клиенты твои с первого дня.
- Платишь **большее из двух** и только его: месячный взнос **$999** или **[X]% от выручки проекта**. Месяца, где платишь и то и другое, не существует.
- Неоплата не расторгает договор: долг индексируется только вверх вместе с биткоином (CME CF BRR, запасной источник CoinGecko):
> Долг = Транш × max(1, BTC сегодня / BTC тогда) — только вверх, без процентов и множителей.
- Возврат — из выручки проекта или прироста зарплаты. Личным долгом не становится. Выход — в любой месяц.
## 2. Инвестиционная программа (SAFE MFN)
Инвесторы вкладываются по **SAFE с MFN** (most favored nation). Голос инвестора определяется на момент инвестиции:
> **Голоса = сколько BTC можно было купить на сумму инвестиции в момент её совершения**
Голоса фиксируются в момент входа и дальше не переоцениваются.
## 3. Квартальные уровни инвестиций студии
Раз в квартал студия распределяет инвестиции по уровням:
| Уровень | Размер инвестиции |
|---|---|
| 1 | $1 000 |
| 2 | $10 000 |
| 3 | $100 000 |
| 4 | $1 000 000 |
- Подаваться нужно **на текущий уровень** — перепрыгивать уровни нельзя.
- Количество победителей на уровне регулирует правление. Сейчас — **по 1 победителю на уровень**.
## 4. Ежедневные звонки: 3 через 3
В рабочие дни звонки идут по схеме «3 через 3». Время распределяется **пропорционально текущим голосам инвесторов**. Если у инвестора ноль голосов — ему отводится **3 минуты**.
---
42 Labs · Shkor Labs LLC (Вайоминг) · ПК «Друзья Коллективного Разума» (Минск) · 42.cil.by

View file

@ -1,7 +1,6 @@
#!/usr/bin/env bash
# Simbiot Harness: запуск без установки обёртки (cli.py + scheduler-демон).
# simbiot help — справка по CLI-командам (как python cli.py -h)
export SIMBIOT_HOME="${SIMBIOT_HOME:-/var/lib/simbiot-harness}"
# simbiot — без аргументов: статус + как управлять демоном/CLI
# simbiot --help — как help (аргумент для argparse-привычки)
DIR="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)"

41
cli.py
View file

@ -90,9 +90,6 @@ def main():
help="ключ идемпотентности (по умолчанию file:<абсолютный путь>)")
p_dfl.add_argument("--force", action="store_true",
help="отправить даже если уже доставлен (повторная доставка вручную)")
p_dfl.add_argument("-m", "--msg", dest="msg", default=None,
help="связать доставку с входящим m-NNN (анти-гонка уровней, "
"t-261009160906): если на него уже отвечено иным артефактом — SKIP")
p_pro = sub.add_parser("proactive", help="инициативная реплика: ритм/тема/отправка (t-260930073419)")
p_pro.add_argument("--force", action="store_true",
help="отправить сейчас, игнорируя окно/интервал/тишину")
@ -181,12 +178,6 @@ def main():
p_rag.add_argument("--db", default=None, help="база RAG (по умолчанию rag.db; hermes.db — общая)")
p_rag.add_argument("--json", action="store_true", help="вывести машинно-читаемо")
p_mcp = sub.add_parser("mcp-call", help="вызов инструмента MCP-портала кооператива (cil.by/mcp, t-261010093054)")
p_mcp.add_argument("tool", nargs="?", default="", help="имя тула (booking_slots, booking_create, whoami, …)")
p_mcp.add_argument("arguments", nargs="?", default="", help="JSON-аргументы тула, напр. '{\"type\": \"call\"}'")
p_mcp.add_argument("--list", action="store_true", help="показать тулы шлюза и выйти")
p_mcp.add_argument("--timeout", type=int, default=None, help="таймаут запроса, сек")
args = ap.parse_args()
if args.cmd == "init":
@ -376,28 +367,11 @@ def main():
print(f"FAIL: файл не найден: {args.file}")
sys.exit(1)
aid = args.key or f"file:{os.path.abspath(args.file)}"
# Анти-гонка уровней (t-261009160906): если доставка привязана к
# входящему m-NNN и на него уже отвечено ДРУГИМ артефактом —
# не отправляем второй файл на тот же запрос (гонка min/hour).
if args.msg and not args.force:
model = es.run_projections()
m = (model.get("inbox") or {}).get(args.msg)
if m is not None and m.get("replied") and m.get("reply_artifact") not in (None, "", aid):
es.append_event("harness", aid, "DeliverySkipped", {
"artifact_id": aid, "kind": "file",
"reason": "msg-already-answered-by-other-artifact",
"msg": args.msg,
"reply_artifact": m.get("reply_artifact"),
})
print(f"SKIP: на {args.msg} уже отвечено другим артефактом "
f"({m.get('reply_artifact')}) — используй --force для повтора")
sys.exit(0)
def _send():
return tg_inbox.send_document_to_chat(args.file, caption=args.caption)
ok = commands.deliver_artifact(aid, kind="file", send=_send,
owner="cli:tg-deliver-file",
caption=args.caption, force=args.force,
msg=args.msg)
caption=args.caption, force=args.force)
print(f"OK: файл доставлен ({aid})" if ok
else f"SKIP: уже доставлен ({aid}) — используй --force для повтора")
elif args.cmd == "proactive":
@ -434,19 +408,6 @@ def main():
for c in chunks:
print(f"\n═══ {c['score']:.3f} | {c['source']} ═══")
print(c["text"][:rag_lookup.TIMEOUT * 100 or 900])
elif args.cmd == "mcp-call":
# MCP портала (t-261010093054): ручной вызов тула шлюза cil.by/mcp;
# без init_db — клиент не пишет в read model (трейс только с level/trace_id)
from harness import portal_mcp
if args.list:
names = portal_mcp.list_tools(force=True)
print("\n".join(names) if names else
"(шлюз недоступен или токена нет — проверь SIMBIOT_PORTAL_MCP_TOKEN)")
return
if not args.tool:
ap.error("mcp-call: укажи tool (или --list)")
kw = {"timeout": args.timeout} if args.timeout else {}
print(portal_mcp.call_tool(args.tool, args.arguments, **kw))
elif args.cmd == "escalate":
es.init_db()
eid, to_level = commands.raise_escalation(

View file

@ -1,48 +0,0 @@
# Roadmap Simbiot UI — v0.2 (решения Alex m-516173892, 2026-10-08)
> Продукт зафиксирован: брендинг **Simbiot UI / Simbiot Dashboard**, цена **$99.9/мес
> (claude seat included)**, ключи выдаём наши (шлюз 9router), свой RAG кооператива —
> клиентам нет, есть **общий студийный RAG**, подключение клиентов — **только вручную
> через Alex** на всю первую партию. Полный план: out/simbiot_ui_product_plan.md.
## Фазы
| # | Фаза | Что делаем | Оценка | Статус |
|---|---|---|---|---|
| 0 | Спайк | 1 инстанс claudecodeui за Caddy (ui.cil.by), UX/OAuth/MCP-проверка | 1–2 дня | next |
| 0b | Пресейл-лендинг | out/simbiot_ui_presale.html → деплой на ui.cil.by, заявки → к Alex | 1 день | артефакт готов, деплой next |
| 1 | Каркас продукта | Админка заявок (очередь/статусы), Stripe-ссылка после ручного подтверждения | 2–3 дня | — |
| 2 | Оркестратор (ручной) | Провижининг по кнопке Alex: контейнер, мэйл, CLAUDE.md, teardown | 3–4 дня | — |
| 3 | Студийный RAG | Отдельный индекс; отбор публичных знаний (список на ревью Alex); MCP read-only | 3–4 дня | — |
| 4 | Пилот | 3–5 платящих клиентов через ручной онбординг, фидбек, реальные юнит-цифры | 1–2 недели | — |
| 5 | Автопровижининг | Self-service оплата→доступ | отложено (гейт ниже) | — |
## Ключевые ограничения (зафиксировано решениями Alex)
1. **Ручной онбординг через Alex — план-А, не обходной путь.** Все подключения первой
партии делает Alex вручную (A7). Self-service не включаем до явного решения.
- Пропускная способность: целевые 3–5 клиентов/нед; очередь > 1 недели — сигнал
начинать фазу 5.
- Следствие для продукта: форма-заявка вместо instant-checkout; оплата (Stripe live)
только после ручного подтверждения заявки.
2. **Наш RAG кооператива клиентам НЕ даём (A6).** Строим отдельный общий студийный RAG
(«как строить стартапы», условия студии) из публичных материалов; перегон — по списку
с ревью Alex, MCP read-only. Приватные знания кооператива не попадают наружу.
3. **Ключи и сид — наши (A2+A3):** 9router как единственный шлюз, per-client spend-трекинг,
fair-use лимиты в оффере (защита маржи $99.9-тарифа), ToS-риск на ревью до масштабирования.
## Гейты перехода между фазами
- 0 → 1: спайк признан удачным (UX ок, OAuth сидит, MCP-хуки работают).
- 1 → 2: очередь заявок в админке живёт, Alex подтвердил флоу подтверждения.
- 2 → 3: 1–2 клиента реально подключены вручную end-to-end.
- 3 → 4: студийный RAG прошёл ревью Alex (список источников утверждён).
- 4 → 5: очередь ручного онбординга стабильно > 1 нед ИЛИ > 10 клиентов платят.
## Открытые вопросы к Alex (не блокируют фазы 0–2)
- **Куда падают заявки с ui.cil.by:** email-агент (hello@cil.by?) / TG-бот Alex / портал.
В лендинге placeholder — до выбора канала не деплоить форму в прод.
- Точное содержание fair-use лимита (токены/мес) для оффера $99.9.
- Список источников для перегона в студийный RAG (фаза 3, нужен на ревью).
- Юр-модель перепродажи сида при масштабировании за пилот.

View file

@ -7,7 +7,7 @@
"amount_usd": 26.7,
"src": "openrouter.ai/api/v1/credits (замер 2026-10-03 23:5x UTC: total=163, usage=136.30)",
"auto": true,
"measured_at": "2026-10-04T01:13:01+00:00"
"measured_at": "2026-10-04T00:15:21+00:00"
},
{
"name": "US LLC Delaware (операционный)",
@ -24,7 +24,7 @@
"amount_usd": 0.0,
"src": "api.stripe.com/v1/balance livemode=True, STRIPE_LIVE_KEY из coop/.env (замер 2026-10-03): available=0, pending=0",
"auto": true,
"measured_at": "2026-10-04T01:13:01+00:00"
"measured_at": "2026-10-04T00:15:22+00:00"
}
],
"debts": [],

View file

@ -1,38 +0,0 @@
# Дайджест hour 2026-10-04 ~07:00 UTC
## Батч задач (3/3 закрыты, все [M])
1. **t-261004061059-84e112** — MCP для продвижения сервисов. Диагноз:
`alex.score.com` не резолвится глобально (зона score.com → мёртвые
NS.SCORE.NET, SERVFAIL по DoH Google/Cloudflare, EDE 22) — деплой туда
невозможен, нужно восстановление делегирования владельцем. Рабочая точка
входа — https://cil.by/mcp (E2E: initialize/whoami OK). Найден и исправлен
прод-баг портала: `/mcp tools/call` падал 500 NameError (mcp регистрируется
раньше stripe/alfa/booking — хелперы `_gen_slots*`/`_send_email`/
`_stripe_product_objects` не определены). Фикс `_late_bind_ctx()` на каждом
tools/call; коммит f0febd9 (shkor-labs/portal), портал перезапущен,
бэкап mcp.py.backup-1004-ctx-latebind. MCP_CONNECT.md дополнен.
2. **t-261004061251-2d3f56** — запись на 20-мин разбор через MCP работает:
booking_slots(type='session') → 70 свободных слотов (18:00–22:00 Минск,
график 3/3), booking_create(kind='session') → jitsi+запись+транскрибация
+ письма/TG. Новый инструмент **coop_catalog**: все сервисы кооператива с
ценами и Stripe pay_url прямо через MCP (live: Симбиот 33/59/1000$,
Рилсап 199/299/699$/мес, Lunara/ReLeaf 4.99$/мес) — полный цикл покупки
без веб-UI (MCP-first).
3. **t-261004062405-fa9926** — MiroFish разобран (клон, ~17.2k строк backend).
Вердикт: движок не берём (стоимость: 100 агентов×40 раундов ≈ $10–25 на
qwen-plus, ×10–30 на премиум; + Zep Cloud SaaS; OASIS/camel-ai стек;
платформа, не библиотека). Идеи заимствования: персоны-из-сущностей графа,
ReportAgent (4 инструмента + интервью агентов + трекер полноты),
episode→граф памяти на своём RAG, дешёвая симуляция спроса перед запусками,
NL→конфиг прогона с budget-cap, IPC-прогресс. Отчёт:
artifacts/mirofish-analysis-2026-10-04.md (доставлен в TG).
## Инфра/события
- Мин-тик: 6× gate:quiet — событий выше порога нет, LLM не запускался.
- Фоновые задачи: нет (list пуст).
- Эскалации: открытых нет; e-261003170542-389f29 (hour Request timed out) уже
acked уровнем day — повторный ack не требуется.
- Внешние сбои, не наша зона: DNS score.com мёртв (см. п.1); TLS on-demand
корректно отвергает alex.cil.by (ask-гейт /api/caddy/ask — by design).

View file

@ -1,59 +0,0 @@
# Дайджест hour 2026-10-04 ~10:00 UTC
## Батч задач (1/2 закрыта; вторая — L, подготовка готова, ждёт Alex)
1. **t-261004091808-70985e [M] — ЗАКРЫТА** (failed[NNN]/DNA у агентов).
Диагноз: heartbeat шли нормально (agent_stats обновлялись каждую минуту), но
репортёр coop-reporter v7 жёстко опрашивал `systemctl is-active hermes-agent` —
у переведённых на Simbiot агентов (Mentalis/Lunar/Rita/Roy/Mon,
harness_type=simbiot) hermes-agent остановлен при переезде, главный юнит —
simbiot-harness.service → дашборд красил живых агентов в failed[NNN].
«DNA — не синхр.» — отдельная причина: harness_setups пуста, simbiot-cli
(node) на машинах агентов никогда не ставился → CLI-sync был невозможен.
Фикс (3 части): (а) репортёр v8 — активный simbiot-harness → статус
«active (simbiot)», плюс сам собирает DNA-факты (skills/SKILL.md + README.md,
скоринг по формуле CLI) и пушит на новый POST /api/agent/dna → upsert
harness_setups; (б) dashboard.py — fallback-матч DNA по developer_name, когда
hostname машины не содержит slug агента (случай AI corp на Ubuntu-2404…);
(в) agents_dashboard.html — фронт принимал только service==='active',
добавлен 'active (simbiot)' (чип «online · simbiot»). Раскатка v8 —
автоматическая через self-update по manifest, заняла ~5 мин на весь флот,
SSH-ручного вмешательства не потребовалось. Верификация по БД: все цели
тикета online · simbiot, DNA=59/100 (Mentalis/Lunar/Rita), Roy/Mon починены
тем же фиксом, 14 агентов с DNA. портал перезапускан ×2 (reporter.py,
dashboard.py), coop-portal active.
2. **t-261004092629-40655b [L] — НЕ закрыта, подготовка выполнена** (подписать
SAFE MFN с предыдущими инвесторами; подписание — сторона Alex).
Сделано: подписной комплект в `/root/coop/fundraising/safe-package/` —
каноническая **YC Post-Money SAFE (MFN Only)** FINAL.docx (форма №3 по
RAISE_PLAN: uncapped, no discount; источник ycombinator.com/safe, скачана
04.10, целостность проверена) + YC SAFE User Guide.pdf + шаблон MFN side
letter + SIGNING_CHECKLIST.md с шагами из постановки. Чек-лист доставлен
Alex в TG (идемпотентно). Блокер по списку инвесторов: в coop.db реальных
инвесторов НЕТ — `backers` содержит 2 тестовые записи, stripe/alfa payments —
тесты, presale_orders — предзаказы, не чеки → список предыдущих инвесторов
ведётся вне портала, у Alex. Остаток (за Alex): список+реквизиты, юр. ревью
формы и side letter до первой подписи ($1.5–3K), санкционный скрининг
каждого инвестора на дату подписания (OFAC/ЕС — критично для резидентов РБ),
подписание, фиксация экземпляров в safe-package/signed/. Задача вернётся
в ready следующего тика.
## Инфра/события
- Мин-тик: 6× gate:quiet — событий выше порога нет, LLM не запускался.
- Фоновые задачи: нет (list пуст).
- Эскалации: открытых нет, ack-ов не требовалось.
- coop-portal перезапускан 2× в рамках фикса (10:18, 10:27), перерыв сервиса
секунды, ошибок в journalctl нет.
- Раскатка coop-reporter v8 прошла на все машины с reporter-токеном; DNA-пуш
идёт с каждым тиком таймера (2 мин), данные обновляются.
- Конфиг уровней: масштабированная контекст-схема по тактам активна
(e-261001082710): min 256k → hour 512k → day 1M ctx_tokens.
## AUM
- Прямых расходов тик не добавил; фиксом убран ложный «failed» по 5 прод-агентам
(сигнальный шум → ложные ремонты/рестарты больше не триггерятся).
- SAFE-комплект приближает закрытие рейз-шага (юр. ревью — плановые $1.5–3K,
решение за Alex).

View file

@ -1,58 +0,0 @@
# Дайджест hour 2026-10-05 ~14:00 UTC
## Батч задач (1/1 закрыта)
1. **t-261005135853-186b42 [M] — ЗАКРЫТА** (гейтвей: входящие документы на диск).
Диагноз подтверждён: `harness/tg_inbox.py` качал только voice (voice-пайплайн),
documents/photo терялись — file_id в событии есть, файла нет (кейс 4 PDF-выписок
Alex 05.10, доставал вручную getFile'ом).
Фикс: `save_document()` — при kind=document|photo getFile→download→атомарная
запись (.part→rename) в `inbox/files/<file_unique_id>.<ext>` (новый
`paths.inbox_files_dir()`, env `SIMBIOT_INBOX_FILES_DIR`); фото — максимальный
размер. В MessageReceived теперь `file_path` (абс, открывается с любого уровня)
+ `file_name`/`file_size`/`mime_type` — триаж видит что за файл без getFile.
Идемпотентно по file_unique_id (повторный апдейт не перекачивает), сбой
скачивания (истёкший у TG файл, HTTP 400/410) не валит поллер — file_id
остаётся в событии для ручного достёгивания. Голосовые — как раньше.
Верификация: `tests/test_tg_documents.py` — 22 PASS (document e2e, photo e2e,
идемпотентность, сбой, _file_ext); регресс voice_video/seen_indication/
inbox_batch — зелёный. Поллер перезапущен, активен (systemd). Коммит c5c3e8f.
Живая проверка: следующий отправленный боту документ сам появится в inbox/files/.
## Батч входящих m-516173771..777 (7 сообщений, один разговор) — отвечено сводно
Контекст: Alex скинул 4 PDF-выписки → «гейтвей их не сохраняет» (→ задача выше,
закрыта этим же тиком) → «сделай реальную ссылку на оплату в баксах с alipay»
→ «на $100» → «ссылку» → «а тут уже почему-то нету алипей, хоть в предыдущей
линке был».
Сделано:
- Боевой $100-линк подтверждён и отдан: https://buy.stripe.com/3cIdR82WH4RpfPY3EY1B60c
(plink_1UNCSvBT53G7FGSQDqXJOE8W, price $100 USD one_time, livemode).
- Почему нет Alipay — разбрано по факам, а не догадкой: через live-API проверена
payment_method_configuration — alipay/wechat_pay/paypal `available=false`
(методы НЕ активированы в боевом аккаунте Stripe), card и amazon_pay — on.
Попытки: пересоздание линка с явными методами → «ensure the type is activated
in your dashboard»; Checkout Session с явными методами → тот же отказ;
включение display_preference через API → невозможно (available=false).
Вывод: единственный путь — ручной шаг в дашборде (Settings → Payment methods),
после чего пересоздание линка — одна команда. Alex'у дана инструкция и обещание
пересоздать по сигналу «алипей включил».
- Amazon Pay в payment links Stripe не поддерживается в принципе (список
допустимых payment_method_types его не содержит; он работает только динамически
в чекауте — потому и был виден в старых линках без явных методов).
## Инциденты/эскалации
- Эскалаций от нижних уровней за час нет (4 тихих минутных тика, 0 событий выше
порога — LLM не запускался, burn экономлен).
- В ходе теста поймал и починил собственный баг: ранние выходы save_document
возвращали кортеж (None, None) вместо None — тест ловит такие вещи именно
поэтому.
## AUM-фильтр часа
Фикс гейтвея — прямая база долгосрочной AUM: до него входящие документы Алекса
(выписки банков — материал для учёта AUM/оборотки) терялись физически, а ручное
доставание съедало тики уровней. Alipay-линк — короткий доход (предзаказы $100),
но блокер снят до предела: осталась одна ручная кнопка у Alex.

View file

@ -1,57 +0,0 @@
# Дайджест hour 2026-10-05 ~16:00 UTC
## Батч входящих m-516173792/793 (2 сообщения, один разговор) — отвечено сводно
Контекст: продолжение запроса Alex на развёртку Claude Code на Лунаре
(m-516173791, задача t-261005155643, подана мин-тиком 15:56). Новое требование:
Claude Code должен подключаться к Telegram официальным коннектором
`claude --channels plugin:telegram@claude-plugins-official`.
Сделано:
- **Разведка коннектора (проверено по фактам, не по памяти):**
- плагин `telegram` v0.0.7 реально существует в официальном маркетплейсе
anthropics/claude-plugins-official (315 плагинов, telegram в категории
productivity) — README изучен с GitHub;
- флаг `--channels` в установленной на Лунаре версии claude 2.1.289
парсируется корректно (проверено запуском);
- механика: MCP-сервер на **Bun** (на Лунаре отсутствует — ставить),
`/plugin install` → `/telegram:configure <токен>` → запуск с `--channels`
→ DM боту → 6-значный код → `/telegram:access pair` → allowlist;
- **блокер-1:** сессия claude не авторизована (Not logged in) — нужен ключ
по паттерну Z.AI/GLM из `/root/coop/claude_harness.py`;
- **блокер-2 (главный):** токен TG-бота Лунара занят simbiot-harness-tg
(long polling) — два поллера на одном токене конфликтуют. Варианты
предложены Alex: (1) новый бот от BotFather — Simbiot-инбокс остаётся;
(2) отдать существующий токен — TG-инбокс Лунара переехал бы в Claude
Code. По умолчанию вариант 1, жду токен.
- **Задача t-261005161125-d7d92a [S] подана** — подключение коннектора
со всей разведкой в деталях (bun, шаги, оба блокера, варианты токена),
чтобы исполнитель не повторял разведку.
- Ответ Alex отправлен сводным батч-реплаем (2/2 закрыты).
## Развёртка Claude Code на Лунаре (t-261005155643, running у min)
Мин-тик довёл до: на Лунаре (2.28.121.212, ssh root работает) стоят
node 22 / claude 2.1.289 / ttyd; сервис `ttyd-claude.service` поднят
(:7681, basic-auth, пользователь lunar), портал (204.168.232.21) уже
пробует терминал. Хвост работы: авторизация claude, интеграция ttyd в UI
портала, проверка. Первый заход мин-тика упёрся в бюджет тика — задача
корректно перезабрана следующим мин-тиком (guard сработал как задумано).
## Гигиена доски
Два клона-мусора (t-261004134505/506, паттерн само-клонирования
e-261004134613: title=ID уже done-задач) переблокированы с явной причиной
«работы нет» — выведены из очереди, чтобы не жечь бюджет мин-тиков по
анти-старению. CLI не даёт удалить blocked-задачу (close только для
failed, complete только для running) — оставлены в blocked.
## Метрики часа
- Эскалаций открытых: 0 (все acked).
- Фоновых задач: 0.
- Лестница: sec/min/hour/day живы; у min 38 ошибок за жизнь (последняя —
таймаут воркока 480с, известный класс, не новый), у hour 6, у day 1 —
тренда деградации нет.
- Доска: 162 done, 1 running (Лунар-развёртка, у min), 1 ready
(TG-коннектор, следующий на очередь), 2 blocked (мусор с причиной).

View file

@ -1,43 +0,0 @@
# Дайджест hour 2026-10-06 ~04:00 UTC
## Батч задач (2/2 закрыты, обе [M])
1. **t-261006034257-2cacd1** — MCP-инструменты OpenCode FR#4-#7. В гейт
cil.by/mcp добавлены 5 read-only тулов: `list_members` (31 член:
роль/проекты/паи/агенты), `list_applications` (заявки+голоса),
`projects_list` (22 проекта, фильтры owner/status), `list_agents`
(18 агентов + last_heartbeat — дока ask_agent теперь валидна),
`treasury_status` (wallet-treasury **3733.60 USDA**, кошельки членов,
паевые, последние движения). E2E через гейт: все ok, tools/list=68,
фильтры проверены. Дока MCP_CONNECT.md дополнена. Портал перезапущен.
Бэкап mcp.py.backup-1006-readtools.
2. **t-261006034507-f41aa2** — аудит фича-запросов OpenCode. mcp_audit:
пришло ровно 4 (все ok) — в портале ничего не потерялось, но в Kan
была потеря: только 2 карточки-дубля [FR#7], #4/#5/#6 отсутствовали.
Причина: **гонка в `_kan_psql`** — общий remote-файл
/tmp/kan_agent_fr.sql перезаписывался при 4 параллельных
feature_request (портал threaded=True), ssh выполнял чужой SQL.
Фиксы: уникальный remote-файл (uuid) + настоящий cardNumber вместо
id фичи (раньше писалось kan_card_number=id). Бэкфилл: [FR#4]№27,
[FR#5]№28, [FR#6]№29, дубль #7 скрыт. E2E полного тракта FR→Kan
верифицирован, тестовые записи вычищены. Итог: 4 запроса = 4
карточки №26-29, feature_backlog синхронен.
## Инфра/события
- Мин-тики часа: 6× gate:quiet — событий выше порога нет, LLM не
запускался (экономия burn).
- Фоновые задачи: нет (list пуст).
- Эскалации: открытых 0 (все acked; свежая e-261006025634 по TG-токену
Лунары — вне этого тика, наблюдалась ранее).
- Доска: только 2 старых blocked S-клона (02.10, ждут уточнений),
ready пусто.
- coop-portal.service перезапускался 3× в течение тика (по фиксам),
финально active, health ok.
## AUM-нотация
Тулы состояния (казна/члены/проекты/агенты) — база для агентских
контуров кооператива (twin-lite студия, AUM-симуляции): агенты больше
не ходят вслепую, а фикс гонки гарантирует, что фича-бэклог не теряет
запросы — прямой вклад в долгосрочную базу.

View file

@ -1,43 +0,0 @@
# Дайджест hour 2026-10-06 ~08:30 UTC
## Батч задач (1/1 закрыта)
1. **t-261006072315-18199d** [L] — Доска фича-реквестов на странице
проекта (Alex). На портале cil.by `/project/<id>` добавлен раздел
«Фича-реквесты»: агрегирует все входящие MCP `feature_request` с их
статусами. Сделано:
- миграция: `agent_feature_requests.project_id` + индекс (init_db,
применена на проде);
- API в `domains/boards.py`: `GET /api/projects/<pid>/feature-requests`
(FR проекта + общий бэклог портала `project_id=''`, голоса, can_manage)
и `PATCH .../<id>/status` (владелец/Председатель; статусы
new/accepted/in_progress/done/rejected);
- UI `static/project_detail.html` в стиле портала: карточки
(title, агент, дата, Kan №, голоса ⬡, проблема/предложение/usage_hint),
селектор статуса у владельца, счётчики по статусам;
- MCP `feature_request` теперь привязывает запрос к проекту агента
(`agents.project_id`).
E2E на изолированной копии БД: 7 FR на проекте 1, PATCH ок, невалидный
статус 400, member 403 на PATCH, изоляция проектов ок, 404 на несущ.,
`node --check` ok. Прод: coop-portal рестартован, active, миграция
применена. Бэкапы `*.backup-1006-frboard`.
## Инфра/события
- Мин-тики часа: 6× gate:quiet — событий выше порога нет, LLM не
запускался (экономия burn).
- Фоновые задачи: нет (list пуст).
- Эскалации: открытых 0, все acked. Свежая e-261006065717 (mail_send
500) уже закрыта фикс-коммитом `bc3c3e5` (import sys → NameError).
- coop-portal.service: 1 рестарт в тике (деплой доски), финально
active, health ok, агенты поллят штатно.
- Git /root/coop: изменения в 4 файлах (app.py, boards.py, mcp.py,
project_detail.html) — уйдут очередным auto-commit.
## AUM-нотация
Доска фича-реквестов замыкает контур обратной связи агент→владелец:
запросы больше не лежат мёртвым списком, а имеют видимый статус и
приоритет (голоса) прямо на странице проекта. Это повышает отдачу от
агентского контура кооператива (больше полезных фич → больше ресурса
на длинной дистанции) — прямой вклад в долгосрочную AUM-базу.

View file

@ -1,38 +0,0 @@
# Дайджест hour 2026-10-06 ~18:10 UTC
## Эскалации (обе закрыты в этом тике)
- **e-261006145651 (critical, «стор скомпрометирован»)** — ЛОЖНАЯ ТРЕВОГА, разобрано и закрыто.
Полный аудит `cli.py verify`: OK по всем 1.3M событий (подписи + тампер-цепочка, ключ faf8d19d9aed71fe).
Событие 1303573 перепроверено точечной криптопроверкой — подпись ВАЛИДНА. Единственная дырка: 1303601
(сама эта EscalationRaised) записана без sig из-за транзиентного отказа подписи — аудит корректно
считает её legacy-границей, не ошибкой. Причина паники: упавший процесс верифицировал дельту чужим
ключом (изоляты /tmp/simbiot-iso-* имеют собственные ключи аудита) → false positive.
- **e-261006145703 (warn, дублирующий трейсбек)** — тот же инцидент, закрыт вместе с первым.
Рекомендация на day: прокинуть key_id стора в верификатор/не считать чужой key_id ошибкой.
Состояние после разбора: проекции догнали голову стора (last_event = max id), CLI работает,
min-тик жив, inbox чист (0 без ответа; m-516173853/854 из контекста эскалации давно replied).
## Батч задач (2/2 закрыто)
1. **t-261006175404 (M) deploy-claude-code.sh — ГОТОВО.**
`scripts/deploy-claude-code.sh` (289 строк, +x): воспроизводимая развёртка Claude Code + ttyd web-TUI
по боёвому опыту Лунара (node 22 nodesource/tarball-фолбэк, npm i -g @anthropic-ai/claude-code,
ttyd 1.7.7 под архитектуру, OS-юзер, systemd-юнит с basic-auth). Опции --port/--user/--token/--service/
--ttyd-version/--claude-version/--interface/--env K=V/--readonly/--dry-run/--check. Протестировано:
bash -n, два dry-run (включая лунар-профиль — ExecStart совпал с боевым), валидация аргументов
(exit 2), --help, --check. Идемпотентен (не дёргает restart при неизменном юните).
2. **t-261006175504 (M) API для поиска товаров по Shopify Collective — ГОТОВО.**
Публичного Collective-поиска НЕ СУЩЕСТВУЕТ (searchShopifyCollectiveSuppliers отсутствует в схеме,
404). Официально: приглашения/коннекты/просмотр каталогов — только UI приложений Collective; подтверждено
стаффом Shopify (тред 2025-04) и changelog (свежего нет). Рабочий путь: авто-импорт policies → товары
с тегом 'Shopify Collective' + {supplier_name} → Admin GraphQL products(query:"tag:...") + вебхуки
products/create|update|delete; поставщиком — productCreate + publishablePublish в 'Collective (Supplier)'.
Детали в ответе задачи (доставлено в TG).
## Сводка состояния
- Задачи: доска чиста от ready по этому батчу; tasks_done растёт (обе M закрыты с доставкой в TG).
- Эскалации открытых: 0 (было 2).
- Верхний контекст: стор здоров, DRY-инцидент закрыт без потери данных.

View file

View file

@ -41,37 +41,6 @@ def _rag_tool_for(level: str, trace_id: str):
return rag_lookup
def _portal_mcp_tool_for(level: str, trace_id: str):
"""Тул тика `portal_mcp` — MCP-шлюз портала кооператива (t-261010093054).
Звонки/расписание (booking_slots/booking_create), реестры, казна,
лента звонков портала — живые данные cil.by прямо из тика. Клиент и
лимиты — harness/portal_mcp.py (4 вызова на тик, таймаут 15с, трейс
PortalMcpCalled); тул не поднимается без токена агента. Guardrail:
деньги-тулы (product_create/product_set_mode) запрещены по умолчанию
(SIMBIOT_MCP_ALLOW_ALL=1 снимает).
"""
from strands import tool as strands_tool
from harness import portal_mcp as _mcp
state = {"calls": 0}
@strands_tool
def portal_mcp(tool: str, arguments: str = "") -> str:
"""Вызов инструмента портала кооператива (MCP cil.by): расписание и
создание звонков с Алексом (booking_slots → booking_create), реестры
членов/проектов/агентов, казна, лента звонков и событий, whoami.
Сначала вызови с tool="booking_slots", arguments='{"type": "call"}',
слот передавай в booking_create как есть. arguments — JSON-строка.
Лимит 4 вызова на тик. Ответ — JSON-текст портала; ошибки — "error: …"."""
if state["calls"] >= _mcp.MAX_CALLS_PER_TICK:
return (f"лимит: {_mcp.MAX_CALLS_PER_TICK} вызовов portal_mcp "
f"на тик (использованы)")
state["calls"] += 1
return _mcp.call_tool(tool, arguments, level=level, trace_id=trace_id)
return portal_mcp
def _wire_tool_budget(agent, max_calls: int = 30):
"""Хард-кап тул-коллов на тик (e-260930031800: мин-воркер уходил в
90+ шагов исследования и умирал по таймауту 300с).
@ -295,12 +264,6 @@ def main():
tools.append(_rag_tool_for(level, trace_id))
except Exception:
pass # RAG — best effort: вендор не должен валить тик
# MCP портала (t-261010093054): booking_slots/booking_create, реестры,
# казна, лента звонков — живой портал из тика; без токена тул не поднимется
try:
tools.append(_portal_mcp_tool_for(level, trace_id))
except Exception:
pass # портал — best effort: шлюз не должен валить тик
# stdout воркера не читается (subprocess.run с capture_output и
# длинным выводом переполняет pipe → BrokenPipeError в печати,
# результат тика терялся, дайджесты 04:11-04:17 ушли пустыми).

View file

@ -169,28 +169,6 @@ def archive_stale_failed(max_age_hours: float = 24.0, now=None) -> list:
return closed
def block_task(task_id: str, reason: str = "ожидает уточнений") -> str:
"""Заблокировать живую задачу: TaskBlocked → статус blocked (t-261004122019).
Задача есть, но не готова к работе — ждёт уточнений от Alex или внешнего
события. scheduler берёт только ready → blocked не попадает в батчи.
Обратно: requeue (blocked → ready). Идемпотентно: уже blocked — no-op.
"""
model = es.run_projections()
t = model["tasks"].get(task_id)
if t is None:
raise ValueError(f"задача {task_id} не найдена")
if t["status"] == "blocked":
return task_id # уже заблокирована — no-op
if t["status"] in ("done", "closed"):
raise ValueError(
f"задача {task_id} в статусе {t['status']} — blocked только для живых; "
f"оживить можно requeue")
es.append_event("task", task_id, "TaskBlocked",
{"by": "manual", "reason": reason[:300]})
return task_id
def requeue_task(task_id: str, reason: str = "manual requeue"):
"""Вернуть failed/running/прерванную задачу в очередь (TaskPromoted → ready).
@ -203,7 +181,7 @@ def requeue_task(task_id: str, reason: str = "manual requeue"):
raise ValueError(f"задача {task_id} не найдена")
# closed тоже возвращаем (авто-схлопывание старых фейлов, e-261003094432):
# закрытая в архив задача может понадобиться снова — осознанная доработка.
if t["status"] not in ("failed", "closed", "done", "running", "blocked"):
if t["status"] not in ("failed", "closed", "done", "running"):
raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен")
# идемпотентная доставка (t-260930083937): requeue = новая работа над
# задачей, её результат должен уйти в чат заново → сбрасываем факт
@ -373,7 +351,7 @@ def _delivery_text(task: dict, answer: str) -> str:
def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
caption: str = "", force: bool = False, msg: str = None) -> bool:
caption: str = "", force: bool = False) -> bool:
"""Идемпотентная доставка ПРОИЗВОЛЬНОГО артефакта в чат (t-260930083937).
Рут-кейс дублей: мин-тик шёл файлом «скинь сюда» curl'ом sendDocument —
@ -395,21 +373,6 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
"reason": "already-delivered",
})
return False
# Анти-гонка уровней (t-261009160906): артефакт привязан к входящему
# m-NNN, на которое уже отвечено ДРУГИМ артефактом → SKIP. Два уровня
# (min/hour) на один запрос не должны слать два разных файла; dedup по
# artifact_id=file:<путь> их не различает (разные пути = разные ключи).
if msg and not force:
model = es.run_projections()
m = (model.get("inbox") or {}).get(msg)
if (m is not None and m.get("replied")
and m.get("reply_artifact") not in (None, "", artifact_id)):
es.append_event("harness", artifact_id, "DeliverySkipped", {
"artifact_id": artifact_id, "kind": kind,
"reason": "msg-already-answered-by-other-artifact",
"msg": msg, "reply_artifact": m.get("reply_artifact"),
})
return False
if force:
# ручной повтор — явное решение человека: лок недавней доставки
# не должен блокировать его на TTL (120с). Снимаем свой же захват
@ -431,13 +394,10 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
# в ledger остаётся фактом; повторный force просто отправит ещё раз,
# но ledger при этом не «молодеет» (kind/ts не переписываются)
es.record_delivery(artifact_id, kind=kind)
ev = {
es.append_event("harness", artifact_id, "ArtifactDelivered", {
"artifact_id": artifact_id, "kind": kind, "tg_ok": True,
"caption": (caption or "")[:500], "owner": owner,
}
if msg:
ev["msg"] = msg # анти-гонка уровней (t-261009160906)
es.append_event("harness", artifact_id, "ArtifactDelivered", ev)
})
return True

View file

@ -93,10 +93,6 @@
.chat-send{border:1px solid var(--ink);background:var(--signal);color:#fff;font-weight:700;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;padding:0 20px;cursor:pointer;border-radius:6px}
.chat-send:disabled{opacity:.45;cursor:default}
.chat-note{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dim);margin:8px 2px 0}
/* живой статус каналов (t-261004103718-d8112b) */
.chan{display:inline-flex;align-items:center;gap:6px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--ink);margin-right:16px;white-space:nowrap}
.chan .dot{width:8px;height:8px}
.chan small{color:var(--dim);font-size:11px}
/* ── детали агента (t-261002175316-eb24cc) ── */
.back{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.1em;text-transform:uppercase;
border:1px solid var(--ink);background:transparent;color:var(--dim);padding:8px 14px;cursor:pointer;border-radius:6px}
@ -160,7 +156,7 @@
<div class="view" id="view-chat">
<h2 style="margin-top:0">Чат с Simbiot</h2>
<div class="lead">сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · каналы: <span id="chan-status"><span class="chan"><span class="dot wait"></span>каналы: узнаю…</span></span></div>
<div class="lead">сообщение уходит агенту по порталу-каналу, ответ придёт сам (обычно ≤ 1 мин) · тоже дублируется в TG, если бот подключён</div>
<div class="chat">
<div class="chat-log" id="chat-log"><div class="skel">загружаю…</div></div>
<div class="chat-input">
@ -168,7 +164,7 @@
<button class="chat-send" id="chat-send" onclick="chatSend()">Отправить</button>
</div>
</div>
<div class="chat-note" id="chat-note">канал: портал (portal:&lt;agent&gt;) · ответ агент пишет сам по такту · <span id="chan-note">статус каналов: узнаю…</span></div>
<div class="chat-note" id="chat-note">канал: портал (portal:&lt;agent&gt;) · ответ агент пишет сам по такту</div>
</div><!-- /view-chat -->
<div class="view" id="view-agent">
@ -459,46 +455,6 @@ function chatBubble(m){
`<div class="bubble"><span class="who">${esc(who)}${t ? " · " + esc(t) : ""}${pend}${rep}</span>${esc(m.text || "")}</div></div>`;
}
// ── живой статус каналов (t-261004103718-d8112b) ──────────────────────────
// ok → зелёный + возраст последнего успеха; нет свежего штампа → красный.
// hermes-лейбл — только если бэкенд подтвердил маркер старого Гермес-харнеса.
function chanAge(s){
if (s.age_s == null) return "";
if (s.age_s < 60) return s.age_s + "с назад";
if (s.age_s < 3600) return Math.round(s.age_s / 60) + "м назад";
return Math.round(s.age_s / 3600) + "ч назад";
}
function chanChip(name, label, s){
s = s || {};
const cls = s.ok ? "" : "err";
const detail = s.ok ? (chanAge(s) || "успех") : (s.detail || "нет связи");
return `<span class="chan"><span class="dot ${cls}"></span>${label}` +
`<small>${esc(detail)}</small></span>`;
}
function renderChannels(ch){
const box = $("chan-status");
if (!box) return;
if (!ch){ return; } // бэкенд без статусов — оставляем «узнаю…» до след. тика
let html = "";
if (ch.hermes) html += `<span class="chan"><small>⚠ старый харнес (Hermes): данные могут быть устаревшими</small></span>`;
html += chanChip("portal", "портал", ch.portal);
const tg = ch.telegram || {};
if (tg.token_loaded === false){
html += `<span class="chan"><span class="dot err"></span>telegram<small>бот не подключён</small></span>`;
} else {
html += chanChip("tg", "telegram", tg);
}
box.innerHTML = html;
// краткая сводка там же, где раньше жил статичный «тоже дублируется в TG…»
const note = $("chan-note");
if (note){
const p = ch.portal || {}, t = ch.telegram || {};
const fmt = s => s.ok ? `<span style="color:var(--ok)">ok</span> (${esc(chanAge(s) || "сейчас")})`
: `<span style="color:var(--err)">down</span>`;
note.innerHTML = "статус: портал " + fmt(p) + " · telegram " + fmt(t);
}
}
async function chatTick(){
if (getViewName() !== "chat") return;
chatOpen = true;
@ -513,7 +469,6 @@ async function chatTick(){
log.innerHTML = html || `<div class="skel">сообщений пока нет — напиши первым</div>`;
if (stick) log.scrollTop = log.scrollHeight;
$("chat-note").textContent = "канал: портал (portal:" + (s.agent || "?") + ") · ответ агент пишет сам по такту";
renderChannels(s.channels);
}catch(e){
if (e instanceof SyntaxError) return; // полу-JSON от 503 — следующий тик перепишет
log.innerHTML = `<div class="skel">чат недоступен: ${esc(e.message)} — попробую через ${CHAT_REFRESH}с</div>`;

View file

@ -28,10 +28,6 @@ from harness import commands, es # noqa: E402
_HERE = os.path.dirname(os.path.abspath(__file__))
_INDEX = os.path.join(_HERE, "dashboard.html")
_ROY_INDEX = os.path.join(_HERE, "roy.html")
# ttyd (Claude Code) на агенте Roy — t-261008084337-3afbca
ROY_TTYD_HOST = os.environ.get("SIMBIOT_ROY_TTYD_HOST", "178.104.52.234")
ROY_TTYD_PORT = int(os.environ.get("SIMBIOT_ROY_TTYD_PORT", "7681"))
_ADDR = os.environ.get("SIMBIOT_DASH_ADDR", "0.0.0.0")
_PORT = int(os.environ.get("SIMBIOT_DASH_PORT", "8765"))
@ -39,7 +35,7 @@ _HEALTH_PORT = int(os.environ.get("SIMBIOT_DASH_HEALTH_PORT", "8766"))
_TOKEN = os.environ.get("SIMBIOT_DASH_TOKEN", "")
_REFRESH = int(os.environ.get("SIMBIOT_DASH_REFRESH", "5"))
_STATUS_ORDER = {"ready": 0, "running": 1, "blocked": 2, "failed": 3, "done": 4, "closed": 5}
_STATUS_ORDER = {"ready": 0, "running": 1, "failed": 2, "done": 3, "closed": 4}
_SIZE_COLOR = {"S": "#1d7a3e", "M": "#a3690f", "L": "#FF4F00", "XL": "#b3261e"}
# ── чат Simbiot прямо в дашборде (t-261001100740-99d55e) ──────────────────
@ -187,33 +183,7 @@ def build_chat(limit: int = 80) -> dict:
collapsed[-1]["ts"] = m["ts"]
continue
collapsed.append(m)
out = {"ok": True, "agent": agent_name, "messages": collapsed[-limit:]}
# живой статус каналов (t-261004103718-d8112b): UI рендерит индикаторы
# вместо статичного «тоже дублируется в TG, если бот подключён»
try:
st = channel_status()
tg = _tg_stats()
tgst = st.get("telegram") or {}
tgst["token_loaded"] = tg.get("token_loaded")
tgst["received_24h"] = tg.get("received_24h")
tgst["last_msg_ts"] = tg.get("last_msg_ts")
# fallback: штампа поллера нет (старый main-харнес до рестарта), но
# токен загружен и свежие TG-сообщения капают в стор → канал жив.
if (not tgst.get("ok") and tg.get("token_loaded")
and (tg.get("last_msg_age_s") is not None)
and tg["last_msg_age_s"] <= 600):
tgst["ok"] = True
tgst["age_s"] = tg["last_msg_age_s"]
tgst["detail"] = "жив по событиям стора (штампа поллера ещё нет)"
st["telegram"] = tgst
st["hermes"] = _hermes_marker()
out["channels"] = st
except Exception as e:
# статус — диагностика: чат не должен падать из-за него, но причина
# должна быть видна (journalctl), иначе «пропавшие индикаторы»
# отлаживаются вслепую (t-261004103718: молчаливый pass скрыл сбой)
print(f"[chat] channels status error: {e}", flush=True)
return out
return {"ok": True, "agent": agent_name, "messages": collapsed[-limit:]}
def chat_send(text: str) -> dict:
@ -321,7 +291,7 @@ def build_state() -> dict:
"tasks_total": len(tasks),
"tasks_by_status": {
s: sum(1 for t in tasks if t["status"] == s)
for s in ("ready", "running", "blocked", "failed", "done", "closed")
for s in ("ready", "running", "failed", "done", "closed")
},
"tasks_by_status_size": counts,
"escalations_open": len(escalations),
@ -545,142 +515,6 @@ class _Handler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(body)
# ── WebSocket-прокси на ttyd агента Roy (t-261008084337-3afbca) ────────
def _roy_ws_proxy(self):
"""Прозрачный WS-прокси клиент ↔ ws://178.104.52.234:7681/ws.
stdlib-only: свой минимальный WS-клиент (рукопожатие + чтение фреймов)
+ переупаковка ответов ttyd (серверные фреймы без маски, клиенту
маскируем). Управляющие фреймы проксируем на обе стороны.
"""
import base64 as _b64
import socket as _socket
import struct as _struct
host, port = ROY_TTYD_HOST, ROY_TTYD_PORT
try:
up = self.headers.get("Upgrade", "").lower()
key = self.headers.get("Sec-WebSocket-Key", "")
if up != "websocket" or not key:
return self._json({"error": "expected websocket upgrade"}, code=400)
version = self.headers.get("Sec-WebSocket-Version", "13")
try:
sk = _socket.create_connection((host, port), timeout=10)
except OSError as e:
return self._json({"error": f"ttyd {host}:{port} unreachable: {e}"},
code=502)
sk.settimeout(None)
# рукопожатие с ttyd (без cookie: ttyd на Roy — credential=false)
req = (f"GET /ws HTTP/1.1\r\nHost: {host}:{port}\r\n"
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: {version}\r\n\r\n")
sk.sendall(req.encode())
buf = b""
while b"\r\n\r\n" not in buf:
chunk = sk.recv(4096)
if not chunk:
sk.close()
return self._json({"error": "ttyd closed during handshake"},
code=502)
buf += chunk
if len(buf) > 16384:
sk.close()
return self._json({"error": "ttyd handshake too large"}, code=502)
head, rest = buf.split(b"\r\n\r\n", 1)
status = head.split(b"\r\n", 1)[0].decode("latin-1", "replace")
if " 101 " not in status + " ":
sk.close()
return self._json({"error": f"ttyd handshake failed: {status}"},
code=502)
# отвечаем клиенту тем же 101 (echo его ключа — валидация на браузере)
resp_head = head.decode("latin-1", "replace")
self.connection.sendall((resp_head + "\r\n\r\n").encode("latin-1"))
def _read_frame(sock, masked):
hdr = b""
while len(hdr) < 2:
c = sock.recv(2 - len(hdr))
if not c:
raise ConnectionError("eof")
hdr += c
fin_op = hdr[0]
op = fin_op & 0x0F
ln = hdr[1] & 0x7F
if ln == 126:
ext = b""
while len(ext) < 2:
ext += sock.recv(2 - len(ext))
ln = _struct.unpack(">H", ext)[0]
elif ln == 127:
ext = b""
while len(ext) < 8:
ext += sock.recv(8 - len(ext))
ln = _struct.unpack(">Q", ext)[0]
mask = b""
if masked:
while len(mask) < 4:
mask += sock.recv(4 - len(mask))
data = b""
while len(data) < ln:
data += sock.recv(min(65536, ln - len(data)))
if masked:
data = bytes(b ^ mask[i % 4] for i, b in enumerate(data))
return op, data
def _send_frame(sock, op, data, mask_it):
fin = 0x80 | op
n = len(data)
if n < 126:
hdr = _struct.pack(">BB", fin, (0x80 if mask_it else 0) | n)
elif n < 65536:
hdr = _struct.pack(">BBH", fin, (0x80 if mask_it else 0) | 126, n)
else:
hdr = _struct.pack(">BBQ", fin, (0x80 if mask_it else 0) | 127, n)
if mask_it:
mk = os.urandom(4)
hdr += mk
data = bytes(b ^ mk[i % 4] for i, b in enumerate(data))
sock.sendall(hdr + data)
# мост: ttyd→клиент в фоне, клиент→ttyd в текущем потоке
self.connection.settimeout(None)
import threading as _th
def _upstream():
try:
while True:
op, data = _read_frame(sk, masked=False)
_send_frame(self.connection, op, data, mask_it=False)
except Exception:
pass
finally:
try:
self.connection.shutdown(_socket.SHUT_RDWR)
except Exception:
pass
t = _th.Thread(target=_upstream, daemon=True)
t.start()
try:
while True:
op, data = _read_frame(self.connection, masked=True)
_send_frame(sk, op, data, mask_it=True)
if op == 0x8: # close от клиента
break
except Exception:
pass
finally:
try:
sk.close()
except Exception:
pass
except Exception as e: # прокси не должен ронять дашборд
try:
self._json({"error": f"ws proxy: {e}"}, code=502)
except Exception:
pass
# ── routes ──
def do_GET(self):
route = self.path.partition("?")[0]
@ -712,22 +546,6 @@ class _Handler(BaseHTTPRequestHandler):
return self._json(build_agent(lvl))
except Exception as e:
return self._json({"error": str(e)[:500]}, code=503)
if route == "/roy":
# страница-обёртка ttyd/Claude Code на агенте Roy
# (t-261008084337-3afbca): UI в дашборде, терминал — через /roy/ws
if not self._authorized():
return self._forbidden()
try:
with open(_ROY_INDEX, encoding="utf-8") as f:
return self._html(f.read())
except OSError as e:
return self._json({"error": f"roy.html not found: {e}"}, code=500)
if route.startswith("/roy/ws"):
# прокси WebSocket → ttyd Roy (178.104.52.234:7681): пробрасываем
# заголовки апгрейда, буферизуем фреймы как есть (Opcode 0-2/A).
if not self._authorized():
return self._forbidden()
return self._roy_ws_proxy()
if route == "/api/health":
return self._json({"ok": True, "ts": _utcnow()})
if route in ("/", "/index.html"):
@ -792,145 +610,5 @@ def main():
ThreadingHTTPServer((_ADDR, _PORT), _Handler).serve_forever()
# ── живой статус каналов портала/TG (t-261004103718-d8112b) ────────────────
# Каждый реальный пользователь канала (сек-рефлекс portal_channel.sec_reflex,
# TG-поллер tg_inbox.poll_loop) пишет штамп harness/channel_<portal|tg>.json
# после УСПЕШНОГО обмена. Дашборд только читает штампы — сам портал не дёргает
# (GET-проба poll-эндпоинта дублировала бы сек-рефлекс на его айсберге),
# и getUpdates не пробует (конфликт long polling с процессом поллера).
_CHANNEL_MAX_AGE_MS = 10 * 60 * 1000 # штамп старше 10 мин = канал вниз
_portal_probe_cache = {"ts": 0.0, "res": None}
def _portal_probe() -> dict:
"""Fallback-проба портала (когда штампа рефлекса нет/протух).
main-харнес новой версии пишет штамп при каждом сек-рефлексе; пока он не
перезапущен (рестарт откладывается до тихого окна — LLM-воркеры), дашборд
пробует портал сам: GET /channel/poll?since=now — read-only, consumed
ставится только отдельным POST (не делаем). Кэш 60с: клиент чата тикает
каждые 10с — дёргать портал чаще незачем.
"""
import urllib.parse
import urllib.request
now = time.time()
if _portal_probe_cache["res"] and now - _portal_probe_cache["ts"] < 60:
return _portal_probe_cache["res"]
try:
from harness import portal_channel as _pch
cfg = _pch._cfg()
if not (cfg.get("portal_url") and cfg.get("agent_token")
and cfg.get("agent_name")):
res = {"ok": False, "age_s": None, "ts": 0,
"detail": "канал не сконфигурён (нет /etc/coop-reporter.env)"}
else:
url = (f"{cfg['portal_url']}/api/agent/"
f"{urllib.parse.quote(cfg['agent_name'])}"
f"/channel/poll?since={int(now * 1000)}")
req = urllib.request.Request(
url, headers={"X-Agent-Token": cfg["agent_token"]})
with urllib.request.urlopen(req, timeout=5) as r:
json.loads(r.read().decode())
res = {"ok": True, "age_s": 0, "ts": int(now * 1000),
"detail": "проба ok (штампа рефлекса ещё нет — старый main-харнес)"}
except Exception as e:
res = {"ok": False, "age_s": None, "ts": 0,
"detail": f"проба не прошла: {str(e)[:80]}"}
_portal_probe_cache.update(ts=now, res=res)
return res
# Старый харнес на Гермесе (hermes-rita, 2.28.125.152 — docs/migration-hermes.md)
# не узнаётся по /etc/coop-reporter.env (документированные ключи: PORTAL_URL,
# AGENT_TOKEN). Без честного признака не рисуем "старый харнес" в UI.
_HERMES_MARKER = None
def _hermes_marker() -> str | None:
"""Признак, что эти файлы запущены на старом Гермес-харнесе (для UI).
Пока легитимного маркера нет — None (Simbiot-дашборд ни с чем не путаем).
"""
return _HERMES_MARKER
def channel_status() -> dict:
"""Живой статус каналов чата: portal (poll портала) + telegram (inbox).
Каждый канал: ok (свежий успешный обмен), age_s (возраст штампа),
ts (ms последнего успеха), detail (что именно успешно).
"""
now_ms = int(time.time() * 1000)
def _read(name: str) -> dict:
path = os.path.join(_HERE, f"channel_{name}.json")
try:
with open(path) as f:
d = json.load(f)
ts = int(d.get("ts") or 0)
age = now_ms - ts
if age < 0 or age > _CHANNEL_MAX_AGE_MS:
raise FileNotFoundError # штамп протух → считаем как отсутствующий
return {"ok": bool(d.get("ok")), "age_s": round(age / 1000),
"ts": ts, "detail": str(d.get("detail") or "")[:120]}
except (FileNotFoundError, ValueError, TypeError):
# штампа нет (файл не создан / битый / старше 10 мин).
# portal: честная fallback-проба (штамп пишет main-харнес новой
# версии — до его рестарта проба даёт живую правду).
if name == "portal":
return _portal_probe()
return {"ok": False, "age_s": None, "ts": 0,
"detail": "нет свежего штампа поллера"}
except Exception as e:
return {"ok": False, "age_s": None, "ts": 0,
"detail": f"штамп не читается: {str(e)[:80]}"}
return {"portal": _read("portal"), "telegram": _read("tg"),
"checked_at": now_ms}
def _tg_stats(days: float = 1.0) -> dict:
"""TG-факты из стора: токен загружен? + входящие за последние 24ч."""
from datetime import datetime, timedelta, timezone
tg_token = None
try:
from harness import tg_inbox as _tg
_tg._load_env()
tg_token = bool(_tg.TG_TOKEN)
except Exception:
pass
n24, last_ts = 0, None
try:
db = es._conn()
cutoff = (datetime.now(timezone.utc)
- timedelta(hours=int(days * 24))).isoformat()
# portal-канальные MessageReceived пишет НЕ TG-поллер — исключаем:
# иначе живой портал-чат рисовал бы «зелёный» telegram (найдено в
# t-261004103718: portal ok, tg штампа нет — а события капают).
row = db.execute(
"SELECT count(*) AS n, max(timestamp) AS last FROM events "
"WHERE event_type='MessageReceived' AND timestamp >= ? "
"AND (data IS NULL OR data NOT LIKE '%\"chat_id\": \"portal%')",
(cutoff,)).fetchone()
db.close()
n24 = int(row["n"] or 0)
last_ts = row["last"]
except Exception:
pass
age_s = None
if last_ts:
try:
from datetime import datetime as _dt
age_s = max(0, round(time.time()
- _dt.fromisoformat(str(last_ts)).timestamp()))
except Exception:
age_s = None
return {"token_loaded": tg_token, "received_24h": n24, "last_msg_ts": last_ts,
"last_msg_age_s": age_s}
if __name__ == "__main__":
main()

View file

@ -295,7 +295,6 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict)
version = (row["v"] or 0) + 1
ts = datetime.now(timezone.utc).isoformat()
data_str = json.dumps(data, ensure_ascii=False)
sig_f = {}
try:
from . import audit as _audit
prev = db.execute(
@ -303,20 +302,6 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict)
sig_f = _audit.sign_event_row(aggregate, aggregate_id, event_type,
data_str, ts, version,
prev["sig"] if prev else None)
# t-261008084600 (rogue-key guard): стор с чужими подписями не подпишем
# своим ключом. Инциденты 06.10/08.10: cli из /tmp-копии репо наследовал
# SIMBIOT_HOME прод-стора, не находил рядом свой .audit-ключ, генерил
# НОВЫЙ и вписывал его подпись в прод-стор → run_projections падал
# «стор скомпрометирован» каждую секунду. Правило: если стор уже подписан,
# key_id нового ключа обязан совпадать с key_id существующих событий;
# иначе пишем без подписи (legacy-граница, проекции живы).
if sig_f.get("key_id"):
dom = db.execute(
"SELECT key_id FROM events WHERE key_id IS NOT NULL "
"ORDER BY id DESC LIMIT 1").fetchone()
if dom and dom["key_id"] and dom["key_id"] != sig_f["key_id"]:
sig_f = {}
# чужой ключ → без подписи; верификатор пометит legacy
except Exception:
sig_f = {} # без подписи (legacy) — аудит отразит, харнес не падает
try:
@ -548,15 +533,6 @@ def _apply_event(model: dict, ev: dict):
if m:
m["replied"] = True
m["reply_text"] = data.get("text", "")
elif etype == "ArtifactDelivered":
# Анти-гонка уровней (t-261009160906): связка «файл → входящее».
# Если доставка артефакта помечена msg=m-NNN, фиксируем её в
# инбоксе — второй артефакт на то же входящее будет SKIP'нут.
msg = (data or {}).get("msg") if isinstance(data, dict) else None
if msg:
m = model.setdefault("inbox", {}).get(msg)
if m is not None and not m.get("reply_artifact"):
m["reply_artifact"] = data.get("artifact_id")
elif etype == "MessageSeen":
# индикация взятия в работу (e-261002202857): флаг seen → reaction 👀
# ставится ровно один раз, повторные маркеры сообщений не найдут

View file

@ -82,16 +82,6 @@ LEVELS = {
"знаний кооператива (контекст, проекты, порталы, встречи, документы). "
"Если вопрос про кооператив/продукты/документы — сначала rag_lookup "
"(1 запрос за тик, формулируй целиком), потом отвечай по найденному. "
"У тебя есть инструмент portal_mcp — живой портал кооператива cil.by "
"(MCP): booking_slots/booking_create — свободные слоты и создание "
"звонка с Алексом (jitsi-ссылка, гостю письмо, запись и транскрипция "
"автоматом), list_calls/get_call — прошедшие звонки и саммари, "
"list_members/projects_list/list_agents/treasury_status/list_events — "
"реестры, казна, лента. Когда Alex просит назначить/перенести звонок "
"или «посмотреть расписание» — это portal_mcp (до 4 вызовов за тик): "
"сначала booking_slots (arguments '{\"type\": \"call\"}'), потом "
"booking_create со слотом как есть. НЕ назначай звонки сам по "
"инициативе — только по явной просьбе в чате. "
"ВАЖНО: НЕ исследуй исходники харнеса (scheduler.py, commands.py, es.py, "
"isolated_runner.py) и не разбирай падения тиков — это зона верхних уровней. "
"Отвечай из контекста тика, максимум пара шагов, иначе таймаут тика. "
@ -131,15 +121,7 @@ LEVELS = {
"Файлы/уведомления в чат — только идемпотентно: cli.py tg-deliver-file "
"<путь> -c \"подпись\", cli.py tg-notify \"текст\" -k <ключ> (повтор — SKIP). "
"Прямые curl/api.telegram отправки из shell запрещены — нет защиты "
"от дублей (t-260930083937). "
"Инструмент portal_mcp — живой портал кооператива (MCP cil.by): "
"booking_slots/booking_create (звонки с Алексом: слот → создание "
"jitsi-звонка со ссылкой, гостю письмо), list_calls/get_call, "
"list_members/projects_list/list_agents, treasury_status, list_events. "
"Просьбы про назначение/перенос звонков и «что по расписанию» — "
"через него (до 4 вызовов за тик). НЕ создавай звонки без явной "
"просьбы в чате; деньги-тулы (product_create/product_set_mode) "
"заблокированы по умолчанию."),
"от дублей (t-260930083937)."),
can_shell=True,
),
"day": CycleLevel(
@ -150,11 +132,7 @@ LEVELS = {
"Открытые эскалации (секция ЭСКАЛАЦИИ) — приоритет: это важное, "
"поднятое с нижних уровней; закрой cli.py escalation-ack <id>. "
"Файлы в чат — только cli.py tg-deliver-file (идемпотентно, "
"повтор — SKIP); прямые curl/api.telegram отправки запрещены. "
"Инструмент portal_mcp — живой портал кооператива (MCP cil.by): "
"расписание и создание звонков (booking_slots → booking_create), "
"реестры, казна, лента звонков; до 4 вызовов за тик, деньги-тулы "
"заблокированы по умолчанию."),
"повтор — SKIP); прямые curl/api.telegram отправки запрещены."),
can_shell=True,
),
}

View file

@ -109,23 +109,6 @@ def inbox_outbox_dir() -> str:
return os.path.join(REPO_ROOT, "portal", "inbox", "outbox")
# ── входящие файлы каналов (t-261005135853): documents/photo на диск ────────
def inbox_files_dir() -> str:
"""Каталог входящих файлов (documents/photo из TG).
Голосовые живут в voice/ (кэш ASR), а документы/фото — данные уровня
инбокса: выписки, сканы, артефакты Алекса. Не задаём SIMBIOT_HOME
(legacy) → /var/lib/simbiot-harness/inbox/files — тот же корень, где
голосовые (voice_dir legacy), рядом с inbox/ каналов портала.
"""
env = os.environ.get("SIMBIOT_INBOX_FILES_DIR", "").strip()
if env:
return env
if IS_ISOLATED:
return os.path.join(_HOME, "inbox", "files")
return "/var/lib/simbiot-harness/inbox/files"
# ── runtime-штампы каналов для дашборда ─────────────────────────────────────
def stamp_dir() -> str:
if IS_ISOLATED:
@ -143,22 +126,6 @@ def voice_dir() -> str:
return "/var/lib/simbiot-harness/voice" # legacy
# ── токен агентского MCP портала (t-261010093054) ──────────────────────────
def portal_mcp_token_path() -> str:
"""Путь к токену MCP-агента портала (см. harness/portal_mcp.py).
Данные инстанса, не код: secrets/ в SIMBIOT_HOME, legacy — файл
.simbiot-portal-token в корне репо (выдан при подключении Simbiot
как MCP-агента кооператива, 02.10).
"""
env = os.environ.get("SIMBIOT_PORTAL_MCP_TOKEN_FILE", "").strip()
if env:
return env
if IS_ISOLATED:
return os.path.join(_HOME, "secrets", "portal-mcp-token")
return os.path.join(REPO_ROOT, ".simbiot-portal-token")
def ensure_instance_dirs() -> list:
"""Создать структуру хома изолированного инстанса. Идемпотентно.
@ -168,8 +135,7 @@ def ensure_instance_dirs() -> list:
if not IS_ISOLATED:
return []
made = []
for sub in ("db", "rag", "env", "inbox", "digests", "audit", "docs",
"voice", "run", "secrets"):
for sub in ("db", "rag", "env", "inbox", "digests", "audit", "docs", "voice", "run"):
p = os.path.join(_HOME, sub)
if not os.path.isdir(p):
os.makedirs(p, exist_ok=True)

View file

@ -1,227 +0,0 @@
"""Simbiot Harness — MCP-клиент портала кооператива (t-261010093054).
Портал cil.by — хаб кооператива: звонки/встречи (booking_slots /
booking_create), лента звонков (list_calls / get_call), реестры
(members/projects/agents), казна, заявки, события. Всё это доступно
агентам через MCP-шлюз https://cil.by/mcp (Streamable HTTP, JSON-RPC 2.0,
см. /root/coop/MCP_CONNECT.md) — но тики харнеса раньше имели только
rag_lookup по локальной базе знаний: живого портала у LLM-тиков не было,
звонки Alex создавал руками через UI.
Этот модуль — тонкий клиент шлюза + материал для тула тика `portal_mcp`
(обёртка в _llm_worker._portal_mcp_tool_for):
- токен агента: env SIMBIOT_PORTAL_MCP_TOKEN → файл secrets/portal-mcp-token
инстанса (legacy — .simbiot-portal-token в корне репо). Токена нет →
тул не поднимается, тик живёт как раньше;
- лимиты зашиты в код (не в промпт): 4 вызова на тик (каждый тик — новый
процесс воркера), таймаут 15с на запрос, tools/list кешируется на процесс;
- guardrail: деньги-тулы (product_create / product_set_mode — создание
Stripe-объектов и переключение test↔live) по умолчанию ЗАПРЕЩЕНЫ;
разблокировать — SIMBIOT_MCP_ALLOW_ALL=1. Остальное — стандартный
спектр портала: booking_* / list_* / get_* / whoami / report_*;
- ошибки шлюза НЕ валят тик: возвращаются текстом "error: …";
- трейс PortalMcpCalled в event store (тул/ок/мс) — наблюдаемость как у
RagQueried: часовой уровень видит в событиях, что тик ходил в портал.
Клиент — чистый urllib (зависимостей нет), id запроса монотонный в рамках
процесса. Шлюз идемпотентен по своей природе: tools/list — чтение,
booking_create на занятый слот честно отвечает 409.
"""
import json
import os
import time
import urllib.error
import urllib.request
from . import paths as _paths # t-261004113351: секреты/данные инстанса
# ── конфиг ──────────────────────────────────────────────────────────────────
DEFAULT_URL = "https://cil.by/mcp"
TIMEOUT = 15 # сек на запрос: шлюз локальный, но пишет в Stripe/SMTP
MAX_CALLS_PER_TICK = 4 # звонки/расписание не требуют длинных цепочек
MAX_RESULT_CHARS = 6000 # выжимка для тика: транскрипции бывают большими
# тулы, создающие/переключающие Stripe-объекты (деньги) — руками Alex;
# SIMBIOT_MCP_ALLOW_ALL=1 снимает запрет (осознанный выбор инстанса)
_BLOCKED_TOOLS = {"product_create", "product_set_mode"}
_req_seq = 0
_tools_cache = None # [имена] — tools/list на процесс (каждый тик — новый)
def _load_token() -> str:
"""Токен агентского MCP: env сильнее файла; home-инстанс без своего
файла (secrets/portal-mcp-token) откатывается к legacy-токену репо —
миграции секретов не должны отключать уже подключённого агента."""
tok = os.environ.get("SIMBIOT_PORTAL_MCP_TOKEN", "").strip()
if tok:
return tok
p = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
".simbiot-portal-token")
try:
v = open(p).read().strip()
if v:
return v
except Exception:
pass
try:
with open(_paths.portal_mcp_token_path()) as f:
return f.read().strip()
except Exception:
return ""
def _url() -> str:
return os.environ.get("SIMBIOT_PORTAL_MCP_URL", "").strip() or DEFAULT_URL
class McpError(Exception):
"""Сбой запроса к шлюзу (сеть/HTTP/JSON) — тику вернётся текстом, не рэйзом."""
def _rpc(method: str, params: dict, timeout: int = TIMEOUT) -> dict:
"""Один JSON-RPC запрос к шлюзу. Возвращает result (без envelope)."""
global _req_seq
_req_seq += 1
body = json.dumps({"jsonrpc": "2.0", "id": _req_seq,
"method": method, "params": params or {}}).encode()
req = urllib.request.Request(
_url(), data=body,
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {_load_token()}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
envelope = json.loads(r.read().decode())
except urllib.error.HTTPError as e:
raise McpError(f"HTTP {e.code} от {_url()}: {e.read()[:300]!r}")
except Exception as e:
raise McpError(f"шлюз недоступен ({e})")
if not isinstance(envelope, dict):
raise McpError(f"шлюз вернул не-JSON-RPC ответ: {str(envelope)[:200]}")
if "error" in envelope and envelope["error"]:
err = envelope["error"]
raise McpError(f"{err.get('message') or err} (code={err.get('code')})")
return envelope.get("result") or {}
def _known_tools() -> list:
"""Имена тулов шлюза (кеш на процесс). Сбой — пустой список (см. call)."""
global _tools_cache
if _tools_cache is None:
try:
spec = _rpc("tools/list", {})
_tools_cache = [t.get("name") for t in (spec.get("tools") or [])
if t.get("name")]
except Exception:
_tools_cache = []
return _tools_cache
def _trace(level, trace_id, payload: dict):
"""Трейс MCP-вызова в стор — spend/observability едины с LLM-тиками."""
if not (level and trace_id):
return
try:
from . import es
seq = len(_trace_log) + 1
es.append_event("trace", f"{trace_id}:mcp{seq}", "PortalMcpCalled", {
**payload, "level": level,
})
_trace_log.append(seq)
except Exception:
pass # трейс — best effort, не валить тик
_trace_log: list = []
def _coerce_arguments(arguments) -> dict:
"""Аргументы тулу: dict как есть; строка — JSON (модель так и шлёт)."""
if arguments is None or arguments == "":
return {}
if isinstance(arguments, dict):
return arguments
try:
v = json.loads(arguments)
return v if isinstance(v, dict) else {"value": v}
except Exception:
raise McpError("arguments — невалидный JSON; передавайте объект из "
"описания тула (например {\"type\": \"call\"})")
def call_tool(name: str, arguments=None, level: str = None,
trace_id: str = None, timeout: int = TIMEOUT) -> str:
"""Вызвать тул портала. ВСЕГДА возвращает строку (тик не валится).
Ошибки (сеть, guardrail, 409 занятого слота) — текстом «error: …»:
LLM-тик читает их как обычный результат и реагирует по смыслу.
"""
started = time.time()
ok, result = False, ""
def _finish(res: str, _ok: bool) -> str:
_trace(level, trace_id, {
"tool": name, "ok": _ok,
"ms": int((time.time() - started) * 1000),
"result_preview": res[:300],
})
return res
token = _load_token()
if not token:
return _finish(
"error: MCP портала не настроен — нет токена "
"(SIMBIOT_PORTAL_MCP_TOKEN или secrets/portal-mcp-token)", False)
if name not in _BLOCKED_TOOLS or os.environ.get("SIMBIOT_MCP_ALLOW_ALL"):
pass # разрешено
else:
return _finish(
f"error: тул {name} запрещён на этом харнесе по умолчанию "
f"(деньги/Stripe: создаёт или переключает платёжные объекты — "
f"руками Alex; разблокировка SIMBIOT_MCP_ALLOW_ALL=1)", False)
try:
args = _coerce_arguments(arguments)
except McpError as e:
return _finish(f"error: {e}", False)
known = _known_tools()
if known and name not in known:
return _finish(
f"error: тула {name} нет на шлюзе; доступные: "
f"{', '.join(known)}", False)
try:
res = _rpc("tools/call", {"name": name, "arguments": args}, timeout)
except McpError as e:
return _finish(f"error: {e}", False)
ok = True
try:
# MCP content-массив: шлюз портала отдаёт [{type: "text", "text": ...}]
content = res.get("content") or []
texts = [c.get("text", "") for c in content if isinstance(c, dict)]
result = "\n".join(t for t in texts if t).strip() or json.dumps(res, ensure_ascii=False)
except Exception:
result = json.dumps(res, ensure_ascii=False)
if len(result) > MAX_RESULT_CHARS:
result = result[:MAX_RESULT_CHARS] + f"\n…(обрезано, всего {len(result)} симв.)"
return _finish(result, ok)
def list_tools(force: bool = False) -> list:
"""Имена тулов шлюза (для CLI и подсказок в промпте)."""
global _tools_cache
if force:
_tools_cache = None
return _known_tools()
def describe_tools(max_names: int = 12) -> str:
"""Короткая шпаргалка по тулам портала для промптов уровней."""
return ("booking_slots(type='call'|'session') — свободные слоты звонка с "
"Алексом; booking_create(start_utc из слота, guest_name, guest_email, "
"kind='call'|'session') — создать jitsi-звонок со ссылкой (гостю "
"письмо, Алексу — в TG, запись+транскрипция автоматом); booking_cancel"
"(meeting_id); list_calls/get_call — прошедшие звонки и их саммари; "
"list_members/projects_list/list_agents/treasury_status/list_events — "
"реестры и казна; whoami/report_task/report_event/read_context — "
"идентичность и лента портала.")

View file

@ -1,83 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simbiot · Roy — Claude Code (ttyd)</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
<style>
html,body{margin:0;height:100%;background:#161b22;color:#e6edf3;font-family:system-ui,sans-serif}
#bar{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d}
#bar b{font-size:14px}
#st{margin-left:auto;font-size:12px;color:#8b949e}
#st.ok{color:#3fb950}#st.err{color:#f85149}
#term{padding:6px;height:calc(100% - 42px);box-sizing:border-box}
</style>
</head>
<body>
<div id="bar"><b>Roy · 178.104.52.234 · Claude Code (ttyd)</b>
<span id="st">подключение…</span></div>
<div id="term"></div>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script>
(async () => {
const term = new Terminal({cursorBlink:true, fontSize:14, theme:{background:'#161b22'}});
const fit = new FitAddon.FitAddon();
term.loadAddon(fit);
term.open(document.getElementById('term'));
fit.fit();
addEventListener('resize', () => fit.fit());
// токен — как у дашборда: из cookie или query этой страницы
let token = '';
const q = new URLSearchParams(location.search).get('token');
if (q) { token = q; document.cookie = 'simbiot_token=' + encodeURIComponent(q); }
else { const m = document.cookie.match(/(?:^|;\s*)simbiot_token=([^;]+)/); if (m) token = decodeURIComponent(m[1]); }
const wsUrl = (location.protocol==='https:'?'wss://':'ws://') + location.host
+ '/roy/ws' + (token ? '?token=' + encodeURIComponent(token) : '');
const st = document.getElementById('st');
// ttyd-протокол: первый JSON-фрейм {AuthToken:'', Columns:, Rows:}
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
let initSent = false;
ws.onopen = () => {
ws.send(JSON.stringify({AuthToken:'', Columns:term.cols, Rows:term.rows}));
initSent = true;
st.textContent = 'онлайн'; st.className = 'ok';
term.focus();
};
ws.onmessage = ev => {
const d = new Uint8Array(ev.data instanceof ArrayBuffer ? ev.data : new TextEncoder().encode(ev.data));
if (!d.length) return;
if (d[0] === 0x00) { // OUTPUT: печатаем как есть
term.write(new Uint8Array(d.buffer, 1));
} else if (d[0] === 0x01) { // PING
const p = new Uint8Array(2); p[0] = 0x02; ws.send(p);
}
};
ws.onclose = () => {
st.textContent = initSent ? 'связь потеряна — обновите страницу' : 'нет доступа (токен?)';
st.className = 'err';
};
term.onData(data => {
// INPUT: 0x00 + utf-8
const p = new Uint8Array(1 + data.length);
p[0] = 0x00;
for (let i = 0; i < data.length; i++) p[1+i] = data.charCodeAt(i) & 0xFF;
ws.send(p);
});
// resize: WINDOW_RESIZE 0x02 + json {Columns,Rows}
term.onResize(({cols, rows}) => {
if (ws.readyState !== 1) return;
const j = JSON.stringify({Columns:cols, Rows:rows});
const p = new Uint8Array(1 + j.length);
p[0] = 0x02;
for (let i = 0; i < j.length; i++) p[1+i] = j.charCodeAt(i);
ws.send(p);
});
})();
</script>
</body>
</html>

View file

@ -271,16 +271,8 @@ def build_context(level_cfg, model: dict, batch=None):
digests.sort(key=lambda d: d["ts"], reverse=True)
if digests:
parts.append("ВЫЖИМКИ НИЖНИХ УРОВНЕЙ (последние, сжатые):")
# дедуп по тексту (эксп. #1, t-261005121525): тихий мин-уровень давал
# 6 идентичных [gate:quiet]-строк в каждом шаге промпта — чистый burn.
# Теперь повтор схлопывается в одну строку с счётчиком ×N.
seen: dict = {}
for d in digests[:6]:
key = (d["level"], d["text"][:400])
seen[key] = seen.get(key, 0) + 1
for (lvl, text), n in seen.items():
rep = f" (×{n} подряд)" if n > 1 else ""
parts.append(f"- [{lvl}] {text}{rep}")
parts.append(f"- [{d['level']}] {d['text'][:400]}")
# эскалации: открытые, адресованные этому уровню и выше — в приоритете
escs = []
@ -484,30 +476,19 @@ def run_level_tick(level_cfg, seq: int, model: dict):
claimed = None
prompt = build_context(level_cfg, model, batch)
# Индикация «взял и работаю» (e-261002202857/73bbaf): пока LLM-тик гоняется,
# фоновый поток держит sendChatAction typing (обновление каждые ~2с) —
# «печатает…» виден весь тик. Реакция 👀 на конкретные входящие — поллером
# при приёме (MessageSeen) и по требованию cli.py tg-seen.
# t-261010092931: раньше поток поднимался ТОЛЬКО при непросмотренном
# pending-инбоксе — с пустым/просмотренным инбоксом индикатора не было весь
# тик. Теперь «печатает…» горит весь тик LLM независимо от pending и от
# claimed-задач: чаты берём из всего инбокса модели (seen/replied не важны),
# а при пустом инбоксе — последний живой TG-чат харнеса (_default_chat_id,
# личка Alex). typing — только TG-чаты: портал-каналу и TUI typing API
# недоступен. Останов потока — в finally в конце функции.
# фоновый поток держит sendChatAction typing в TG-чатах инбокса (обновление
# каждые ~2с) — «печатает…» виден весь тик. Реакция 👀 на конкретные
# входящие — поллером при приёме (MessageSeen) и по требованию cli.py tg-seen.
# Останов потока — в finally в конце функции.
from . import tg_inbox
keepalive = None
keep_stop = threading.Event()
tg_chat_ids = list({m.get("chat_id") for m in (model.get("inbox") or {}).values()
pending = [m for m in _pending_inbox(model) if not m.get("seen")]
if pending:
# typing — только TG-чаты (портал-каналу typing API недоступен)
tg_chat_ids = list({m.get("chat_id") for m in pending
if m.get("chat_id")
and not str(m.get("chat_id")).startswith(("portal", "tui"))})
if not tg_chat_ids:
# пустой инбокс — показываем «печатает…» в чате харнеса, если он известен
try:
_dc = tg_inbox._default_chat_id()
except Exception:
_dc = None
if _dc is not None:
tg_chat_ids = [_dc]
and not str(m.get("chat_id")).startswith("portal")})
if tg_chat_ids:
keepalive = threading.Thread(
target=tg_inbox.typing_keepalive,
@ -862,13 +843,8 @@ def _reflex_tick(level_cfg, seq: int, model: dict):
period = datetime.fromtimestamp(seq * level_cfg.every, tz=timezone.utc)
payload = {"level": "sec", "seq": seq, "load1": load,
"ts": period.isoformat()}
# алерт-порог: по умолчанию 0.75*nproc (16 ядер → 12; t-261012090606),
# policy cpu_alert_threshold переопределяет при наличии
try:
_default_thr = max(8.0, 0.75 * os.cpu_count())
except Exception:
_default_thr = 12.0
thr = (model.get("policy") or {}).get("cpu_alert_threshold", _default_thr)
# алерт-порог из policy (по умолчанию 8 — как simbiotd)
thr = (model.get("policy") or {}).get("cpu_alert_threshold", 8)
if load is not None and load >= thr:
payload["alert"] = f"load {load:.2f} >= {thr}"
# без-LLM команды: отвечаем на входящие-команды без ответа

View file

@ -99,7 +99,7 @@ def cmd_status() -> str:
st = cs.get(name) or {}
err = f", ошибок {st['errors']}" if st.get("errors") else ""
lines.append(f" {name}: тиков {st.get('ticks', 0)}{err}")
parts = [f"{k} {counts[k]}" for k in ("ready", "running", "blocked", "failed", "done") if counts.get(k)]
parts = [f"{k} {counts[k]}" for k in ("ready", "running", "done", "failed") if counts.get(k)]
lines.append("📋 задачи: " + (" · ".join(parts) if parts else "нет"))
lines.append(f"🚨 эскалаций открытых: {open_esc}")
lines.append(f"💰 спенд (последние {len(spend)} тиков): ~{tokens:,} токенов".replace(",", " "))
@ -208,21 +208,9 @@ def commands_spec(latin: bool = False) -> list:
def register_bot_commands():
"""setMyCommands: latin-имена + кириллические подписи (без 400-пробы).
"""setMyCommands: сначала кириллица, при отказе — транслит.
Telegram Bot API принимает в ИМЕНАХ команд только [a-z0-9_]:
кириллическое имя → 400 BOT_COMMAND_INVALID (подтверждено телом
ошибки API, t-261004160532-690fcb) — «сначала кириллица, потом
фолбэк» гарантированно падал первым вызовом при каждом старте.
В описаниях кириллица валидна, поэтому регистрируем сразу
единственно возможную комбинацию: latin-имя + кириллическая
подпись — именно её юзер и видит в меню «/».
Кириллицей команды остаются доступными и без меню: парсер
_ALIASES понимает слова «лестница», «статус», «задачи»,
«помощь» (без слэша) и /лестница-подобные токены.
Возвращает 'cyrillic' (подписи кириллицей — то, что видит юзер)
или None (нет токена / не удалось).
Возвращает 'cyrillic' | 'latin' | None (нет токена/не удалось).
Повторный вызов идемпотентен — перезапишет список тем же содержимым.
"""
from . import tg_inbox
@ -231,12 +219,17 @@ def register_bot_commands():
return None
try:
r = tg_inbox._api("setMyCommands",
{"commands": json.dumps(commands_spec(latin=True), ensure_ascii=False)})
{"commands": json.dumps(commands_spec(), ensure_ascii=False)})
if r.get("ok"):
return "cyrillic"
print(f"[tg-cmd] setMyCommands отклонён: {str(r)[:200]}", flush=True)
raise RuntimeError(str(r)[:200])
except Exception as e:
print(f"[tg-cmd] setMyCommands error: {e}", flush=True)
print(f"[tg-cmd] setMyCommands: кириллица не принята ({e}) — пробую транслит", flush=True)
try:
r = tg_inbox._api("setMyCommands",
{"commands": json.dumps(commands_spec(latin=True), ensure_ascii=False)})
return "latin" if r.get("ok") else None
except Exception:
return None

View file

@ -140,88 +140,6 @@ def typing_keepalive(chat_ids: list, stop, interval: float = 2.0):
pass
# ── входящие документы/фото → диск (t-261005135853) ─────────────────────────
# Голосовые качает voice-пайплайн (voice/, кэш ASR), а documents/photo раньше
# терялись: file_id есть в событии, файл не скачивался (кейс 4 PDF-выписок
# Alex 05.10 — доставал вручную getFile'ом). Теперь гейтвей сам кладёт файл
# в inbox/files/<file_unique_id>.<ext> и пишет путь в MessageReceived.
_SAFE_EXT_RE = None # ленивый импорт re
def _file_ext(name: str, mime: str = "") -> str:
"""Расширение из имени/mime, безопасное (без путей и мусора)."""
global _SAFE_EXT_RE
if _SAFE_EXT_RE is None:
import re
_SAFE_EXT_RE = re.compile(r"[^A-Za-z0-9]")
ext = ""
if name and "." in name:
ext = name.rsplit(".", 1)[-1]
if not ext and "/" in (mime or ""):
sub = mime.split("/", 1)[-1]
ext = {"pdf": "pdf", "jpeg": "jpg", "png": "png", "webp": "webp",
"octet-stream": "bin"}.get(sub, sub)
ext = _SAFE_EXT_RE.sub("", ext)[:8]
return ext or "bin"
def save_document(msg: dict):
"""Скачать document/photo из сообщения → inbox/files/.
Возвращает абсолютный путь к файлу или None: сбой скачивания не должен
валить поллер — путь просто не попадёт в событие, file_id в нём
останется для ручного достёгивания (как было до фикса).
Идемпотентно: file_unique_id в имени → повторный апдейт не перекачивает
(файл на диске = готовый ответ).
"""
doc = msg.get("document")
photo = msg.get("photo")
if not doc and not photo:
return None
if doc:
file_id = doc.get("file_id")
uniq = doc.get("file_unique_id") or file_id
ext = _file_ext(doc.get("file_name") or "", doc.get("mime_type") or "")
else:
biggest = photo[-1] # самый большой размер
file_id = biggest.get("file_id")
uniq = biggest.get("file_unique_id") or file_id
ext = "jpg"
if not file_id:
return None
files_dir = _paths.inbox_files_dir()
abs_path = os.path.join(files_dir, f"{uniq}.{ext}")
if not os.path.exists(abs_path): # кэш: один файл — одна закачка
try:
_load_env()
if not TG_TOKEN:
return None
resp = _api("getFile", {"file_id": file_id})
if not resp.get("ok"):
return None
fp = (resp.get("result") or {}).get("file_path")
if not fp:
return None
durl = f"https://api.telegram.org/file/bot{TG_TOKEN}/{fp}"
with urllib.request.urlopen(durl, timeout=90) as r:
blob = r.read()
if not blob:
return None
os.makedirs(files_dir, exist_ok=True)
tmp = abs_path + ".part"
with open(tmp, "wb") as f:
f.write(blob)
os.replace(tmp, abs_path) # атомарно: нет наполовину записанных
except urllib.error.HTTPError as e:
# 400/410 = file истёк у TG — не ошибка поллера, тихий отказ
print(f"[tg] getFile HTTP {e.code}: файл {uniq} недоступен", flush=True)
return None
except Exception as e:
print(f"[tg] download {uniq} error: {e}", flush=True)
return None
return abs_path
def receive_message(update: dict):
"""MessageReceived в стор. Идемпотентно по update_id."""
msg = update.get("message") or update.get("edited_message") or {}
@ -252,15 +170,6 @@ def receive_message(update: dict):
photo = msg.get("photo")
if photo and not file_id:
file_id = photo[-1].get("file_id") # самый большой размер
# t-261005135853: documents/photo сразу на диск при приёме — путь в
# событии, файл не теряется (голосовые качает voice-пайплайн отдельно).
# Сбой скачивания не валит приём: file_path=None, file_id остаётся.
doc_rel = None
if kind in ("document", "photo"):
try:
doc_rel = save_document(msg)
except Exception as e:
print(f"[tg] save_document error: {e}", flush=True)
data = {
"update_id": update["update_id"],
"chat_id": chat.get("id"),
@ -276,14 +185,6 @@ def receive_message(update: dict):
# file_unique_id: ключ аудио-кэша и идемпотентности расшифровки
# (file_id ротируется, unique — постоянный для одного файла)
"file_unique_id": file.get("file_unique_id") or (photo[-1].get("file_unique_id") if photo else None),
# t-261005135853: путь к скачанному файлу (абсолютный, открывается
# напрямую с любого уровня) + исходное имя/размер/mime — триаж видит
# что за файл, без getFile. None = скачать не удалось (file_id остаётся).
"file_path": doc_rel,
"file_name": (msg.get("document") or {}).get("file_name"),
"file_size": (msg.get("document") or {}).get("file_size")
or (photo[-1].get("file_size") if photo else None),
"mime_type": (msg.get("document") or {}).get("mime_type"),
"duration": duration,
"date": msg.get("date"),
# reply_to_message_id (t-261002085926): когда Alex отвечает (reply) на

View file

@ -107,58 +107,8 @@ def _download_voice(file_id: str, file_unique_id: str = ""):
return audio, cache
# ─────────── тяжёлые воркеры ВНЕ cgroup поллера (t-261004160531-780666) ─────
# faster-whisper large-v3-turbo (int8) живёт в ~2-3G RSS, ffmpeg тоже кусается:
# заспавненные поллером как обычные дети они падали в ЕГО cgroup — systemd
# Consumption показывал 3.7G peak у simbiot-harness-tg. Теперь тяжёлое уходит
# в собственный transient-scope (systemd-run --scope) со своим MemoryMax;
# поллер остаётся лёгким (<100M), его юнит держит MemoryMax=1G.
# Нет systemd (локалка/тесты) — прозрачный fallback на прямой запуск.
_ASR_SCOPE_MEM = os.environ.get("SIMBIOT_ASR_SCOPE_MEM", "3G")
_HAS_SYSTEMD = os.path.isdir("/run/systemd/system") and os.path.exists("/usr/bin/systemd-run")
def _heavy_argv(argv):
"""argv тяжёлого воркера → запуск в отдельном systemd-scope с MemoryMax."""
if not _HAS_SYSTEMD:
return list(argv)
import uuid
unit = f"simbiot-asr-{uuid.uuid4().hex[:10]}"
return ["systemd-run", "--scope", "--quiet", "--collect",
f"--unit={unit}", f"-p", f"MemoryMax={_ASR_SCOPE_MEM}",
"--"] + list(argv)
def _heavy_run(argv, input_bytes=None, timeout=600):
"""Тяжёлый subprocess: scope-изоляция памяти + killpg на таймауте.
systemd-run --scope выполняет команду синхронно, поэтому таймаут
работает как раньше; start_new_session + killpg (паттерн
isolated_runner, e-261003004801) не оставляют осиротевший воркер,
если тик умер раньше него.
"""
import signal
proc = subprocess.Popen(
_heavy_argv(argv), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, start_new_session=True)
try:
out, err = proc.communicate(input=input_bytes, timeout=timeout)
return proc.returncode, out or b"", err or b""
except subprocess.TimeoutExpired:
try:
os.killpg(proc.pid, signal.SIGKILL)
except (ProcessLookupError, PermissionError, OSError):
pass
try: # дождаться смерти группы, чтобы зомби не сидели в scope
proc.communicate(timeout=10)
except Exception:
pass
return None, b"", b""
# ────────────── извлечение аудио из видеоконтейнера ─────────────────────────
def extract_audio_track(container: bytes):
"""Видеоконтейнер (mp4 и т.п.) → ogg/opus аудиодорожка через ffmpeg.
@ -171,17 +121,16 @@ def extract_audio_track(container: bytes):
with tempfile.NamedTemporaryFile(prefix="simbiot-v_", delete=False) as tf:
tf.write(container)
src = tf.name
# -vn: только звук; моно 16к — формат, который любят оба ASR-яруса.
# ffmpeg тяжёлый → в свой scope (память поллера не растёт)
rc, out, err = _heavy_run(
# -vn: только звук; моно 16к — формат, который любят оба ASR-яруса
proc = subprocess.run(
["ffmpeg", "-v", "error", "-i", src, "-vn", "-ac", "1",
"-ar", "16000", "-f", "ogg", "-c:a", "libopus", "-"],
timeout=180)
if rc != 0 or not out:
err = err.decode()[:200]
capture_output=True, timeout=180)
if proc.returncode != 0 or not proc.stdout:
err = (proc.stderr or b"").decode()[:200]
print(f"[voice] ffmpeg: дорожка не извлечена ({err})", flush=True)
return None
return out
return proc.stdout
except Exception as e:
print(f"[voice] ffmpeg error: {e}", flush=True)
return None
@ -250,21 +199,17 @@ def _asr_faster_whisper(audio: bytes, filename: str):
"audio_b64": base64.b64encode(audio).decode(),
"filename": filename,
}, ensure_ascii=False)
# faster-whisper large-v3-turbo ~2-3G RSS: только в свой scope,
# иначе память ложится в cgroup поллера (3.7G peak, t-261004160531)
rc, out, err = _heavy_run(
[_FW_PYTHON, _FW_WORKER], input_bytes=req.encode(), timeout=600)
if rc is None:
print("[voice] faster-whisper: таймаут (группа убита)", flush=True)
return None
out = out.decode() if out else ""
proc = subprocess.run(
[_FW_PYTHON, _FW_WORKER], input=req.encode(),
capture_output=True, timeout=600)
out = proc.stdout.decode() if proc.stdout else ""
try:
data = json.loads(out.strip().splitlines()[-1]) if out.strip() else {}
except (ValueError, IndexError):
data = {"error": err.decode()[:300]}
if data.get("error") or rc != 0:
data = {"error": (proc.stderr or b"").decode()[:300]}
if data.get("error") or proc.returncode != 0:
print(f"[voice] faster-whisper error: "
f"{data.get('error') or rc}", flush=True)
f"{data.get('error') or proc.returncode}", flush=True)
return None
return (data.get("text") or "").strip()
except Exception as e:

View file

@ -1 +0,0 @@
{"error":{"message":"[gemini/gemini-3.1-flash-image-preview] [429]: You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-3.1-flash-image\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-3.1-flash-image\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-3.1-flash-image\nPlease retry in 12h6m1.119176629s. (reset after 4s)"}}

View file

@ -1,373 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Simbiot — The Studio OS</title>
<meta name="description" content="A dedicated AI team — engineers, video producers, content makers — plus the OS that runs projects, treasury, decisions and meetings. White-label, flat rate.">
<meta property="og:title" content="Simbiot — The Studio OS">
<meta property="og:description" content="Hire nobody. Ship everything. An AI team for your studio plus the OS that runs it.">
<meta property="og:url" content="https://simbiot.cil.by/">
<meta property="og:type" content="website">
<meta property="og:image" content="https://simbiot.cil.by/og.png">
<meta name="twitter:card" content="summary_large_image">
<style>
/* ── fingerprint: offset rail · 660px measure · charcoal+lime · serif+mono · machined 4/10 · tinted panels · index motif · one entrance + state wash ── */
:root{
--bg:#0E100C; --panel:#151812; --panel2:#1A1E17;
--line:#272C22; --line2:#333A2C;
--ink:#E9ECE1; --dim:#ADB6A2; --faint:#7E8873;
--acc:#C9F24D; --acc-ink:#111408;
--ok:#7BD88F; --run:#E8C44A; --wait:#8A9382;
--serif:Georgia,'Iowan Old Style','Times New Roman',serif;
--mono:ui-monospace,'SF Mono','Cascadia Mono',Menlo,Consolas,'Liberation Mono',monospace;
--sans:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
a{color:inherit}
:focus-visible{outline:2px solid var(--acc);outline-offset:2px;border-radius:2px}
.wrap{max-width:1060px;margin:0 auto;padding:0 22px}
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
/* ── left rail (desktop) ── */
.layout{display:grid;grid-template-columns:168px minmax(0,1fr);gap:44px;max-width:1060px;margin:0 auto;padding:26px 22px 0}
.rail{position:sticky;top:26px;align-self:start;display:flex;flex-direction:column;gap:22px;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.rail .brand{display:flex;align-items:center;gap:9px;color:var(--ink);text-decoration:none;font-weight:600}
.rail .brand .dot{width:9px;height:9px;border-radius:50%;background:var(--acc);flex:none}
.rail nav{display:flex;flex-direction:column;gap:2px}
.rail nav a{color:var(--faint);text-decoration:none;padding:9px 10px 9px 0;min-height:36px;display:flex;align-items:center;border-bottom:1px solid var(--line)}
.rail nav a:hover{color:var(--ink)}
.rail .railnote{color:var(--faint);line-height:1.9;border-top:1px solid var(--line);padding-top:14px}
.rail .railnote b{color:var(--dim);font-weight:500}
/* top bar (mobile) */
.topbar{display:none}
/* ── buttons: compact scale (26px / 11.5px) ── */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:28px;padding:0 14px;border-radius:4px;border:1px solid var(--line2);background:var(--panel2);color:var(--ink);font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;text-decoration:none;cursor:pointer;min-height:40px}
.btn.primary{background:var(--acc);border-color:var(--acc);color:var(--acc-ink);font-weight:600}
.btn:focus-visible{outline-offset:1px}
@media(hover:hover) and (pointer:fine){.btn:hover{border-color:var(--acc)} .btn.primary:hover{filter:brightness(1.06)}}
.btn:active{transform:scale(.96)}
/* ── hero ── */
.hero{padding:30px 0 26px;border-bottom:1px solid var(--line)}
.kicker{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:18px}
.kicker .dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:8px}
h1{font-family:var(--serif);font-weight:400;font-size:clamp(38px,6vw,62px);line-height:1.04;letter-spacing:-.015em;max-width:14ch}
h1 em{font-style:italic;color:var(--acc)}
.lede{color:var(--dim);max-width:52ch;margin:18px 0 22px;font-size:15.5px}
.cta-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.cta-note{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--faint);text-transform:uppercase}
/* ── signature artifact: live task ladder ── */
.console{margin-top:30px;background:var(--panel);border:1px solid var(--line2);border-radius:10px;overflow:hidden}
.console .bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.console .bar .ticks{display:flex;gap:5px}
.console .bar .ticks i{width:8px;height:8px;border-radius:2px;background:var(--line2)}
.console .bar .live{margin-left:auto;display:flex;align-items:center;gap:7px;color:var(--dim)}
.console .bar .live i{width:7px;height:7px;border-radius:50%;background:var(--ok);animation:pulse 2.4s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
.metrics{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--line)}
.metric{padding:12px 14px;border-right:1px solid var(--line)}
.metric:last-child{border-right:none}
.metric .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:19px;color:var(--ink)}
.metric .v .u{color:var(--faint);font-size:12px}
.metric .k{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-top:3px}
.ladder{display:flex;flex-direction:column}
.trow{display:grid;grid-template-columns:52px minmax(0,1fr) 96px 88px;gap:12px;align-items:center;min-height:44px;padding:8px 14px;border-bottom:1px solid var(--line);font-size:13.5px;color:var(--dim);cursor:pointer;text-align:left;background:transparent;border-left:none;border-right:none;border-top:none;width:100%;font-family:var(--sans);transition:background-color .25s,color .25s}
.trow:last-child{border-bottom:none}
.trow:hover{background:var(--panel2);color:var(--ink)}
.trow .sz{font-family:var(--mono);font-size:11px;color:var(--faint)}
.trow .name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.trow .name b{color:var(--ink);font-weight:600}
.trow .st{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--wait);display:flex;align-items:center;gap:7px}
.trow .st i{width:7px;height:7px;border-radius:50%;background:var(--wait);flex:none}
.trow[data-st="running"] .st{color:var(--run)} .trow[data-st="running"] .st i{background:var(--run)}
.trow[data-st="done"] .st{color:var(--ok)} .trow[data-st="done"] .st i{background:var(--ok)}
.trow .who{font-family:var(--mono);font-size:11px;color:var(--faint);text-align:right}
.console .foot{padding:9px 14px;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);border-top:1px solid var(--line)}
.console .foot b{color:var(--dim);font-weight:500}
/* ── sections ── */
section{padding:44px 0;border-bottom:1px solid var(--line)}
.shead{display:flex;align-items:baseline;gap:14px;margin-bottom:22px}
.sidx{font-family:var(--mono);font-size:11px;color:var(--acc);letter-spacing:.1em}
h2{font-family:var(--serif);font-weight:400;font-size:clamp(24px,3.4vw,34px);line-height:1.12;letter-spacing:-.01em}
.snote{margin-left:auto;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.lead{color:var(--dim);max-width:56ch;margin:-8px 0 24px;font-size:15px}
/* team ledger (rows, not cards) */
.ledger{border:1px solid var(--line2);border-radius:10px;background:var(--panel);overflow:hidden}
.lrow{display:grid;grid-template-columns:44px minmax(0,1.1fr) minmax(0,1.4fr) 150px;gap:16px;padding:16px;border-bottom:1px solid var(--line);align-items:baseline}
.lrow:last-child{border-bottom:none}
.lrow .idx{font-family:var(--mono);font-size:11px;color:var(--faint)}
.lrow .nm{font-size:16px;font-weight:600}
.lrow .nm span{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--acc);margin-top:4px;font-weight:400}
.lrow .ds{color:var(--dim);font-size:13.5px}
.lrow .tags{font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);text-align:right;line-height:2}
/* OS definition list */
.dl{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0 40px}
.dl .item{display:grid;grid-template-columns:40px minmax(0,1fr);gap:12px;padding:15px 0;border-bottom:1px solid var(--line)}
.dl .item .k{font-family:var(--mono);font-size:10.5px;color:var(--acc);letter-spacing:.08em;padding-top:3px}
.dl .item .t{font-weight:600;font-size:14.5px}
.dl .item .d{color:var(--dim);font-size:13.5px;margin-top:3px}
/* included rows */
.inc{display:flex;flex-direction:column}
.inc .irow{display:grid;grid-template-columns:26px minmax(0,1fr);gap:14px;padding:13px 0;border-bottom:1px solid var(--line)}
.inc .irow:last-child{border-bottom:none}
.inc .mark{color:var(--acc);font-family:var(--mono);font-size:12px;padding-top:2px}
.inc .t{font-weight:600;font-size:14.5px}
.inc .t span{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-left:10px;font-weight:400}
.inc .d{color:var(--dim);font-size:13.5px;margin-top:2px}
/* pricing */
.plans{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}
.plan{border:1px solid var(--line2);border-radius:10px;background:var(--panel);padding:22px;display:flex;flex-direction:column;gap:14px}
.plan.hot{border-color:var(--acc)}
.plan .pt{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
.plan.hot .pt{color:var(--acc)}
.price{font-family:var(--serif);font-size:40px;line-height:1}
.price .per{font-family:var(--mono);font-size:12px;color:var(--faint);letter-spacing:.08em}
.plan ul{list-style:none;display:flex;flex-direction:column;gap:8px;color:var(--dim);font-size:13.5px}
.plan li{display:grid;grid-template-columns:18px 1fr;gap:10px}
.plan li::before{content:"▸";color:var(--acc);font-family:var(--mono);font-size:11px}
.plan .btn{margin-top:auto}
.plan .fine{font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);text-align:center}
/* footer */
footer{padding:34px 0 46px}
.fgrid{display:flex;flex-wrap:wrap;gap:14px 30px;align-items:center;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.fgrid a{color:var(--dim);text-decoration:none;border-bottom:1px solid var(--line2)}
.fgrid a:hover{color:var(--acc);border-color:var(--acc)}
.fgrid .sp{margin-left:auto}
/* entrance: one reserved-space fade-up */
.in{opacity:0;transform:translateY(8px);animation:in .5s ease forwards}
.in.d1{animation-delay:.08s}.in.d2{animation-delay:.16s}.in.d3{animation-delay:.24s}
@keyframes in{to{opacity:1;transform:none}}
/* ── mobile: single column ── */
@media(max-width:860px){
.layout{grid-template-columns:minmax(0,1fr);gap:0;padding:0 16px}
.rail{display:none}
.topbar{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase}
.topbar .brand{display:flex;align-items:center;gap:8px;color:var(--ink);text-decoration:none;font-weight:600;margin-right:auto}
.topbar .brand .dot{width:8px;height:8px;border-radius:50%;background:var(--acc)}
.topbar .btn{min-height:40px;height:28px}
.wrap{padding:0 16px}
.layout > main{padding:0}
.hero,.layout main section,.layout main footer{padding-left:0;padding-right:0}
.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}
.metric .v{font-size:15px}
.trow{grid-template-columns:38px minmax(0,1fr) 84px;gap:8px;padding:10px 12px}
.trow .who{display:none}
.lrow{grid-template-columns:minmax(0,1fr);gap:6px;padding:14px 16px}
.lrow .tags{text-align:left;line-height:1.9}
.dl{grid-template-columns:minmax(0,1fr)}
.plans{grid-template-columns:minmax(0,1fr)}
h1{font-size:clamp(34px,9vw,44px)}
}
@media(prefers-reduced-motion:reduce){
*{animation:none!important;transition:none!important}
.in{opacity:1;transform:none}
html{scroll-behavior:auto}
}
</style>
</head>
<body>
<header class="topbar">
<a class="brand" href="#top"><span class="dot"></span>Simbiot</a>
<a class="btn" href="https://cil.by/cabinet">Sign in</a>
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Get started</a>
</header>
<div class="layout" id="top">
<aside class="rail">
<a class="brand" href="#top"><span class="dot"></span>Simbiot</a>
<nav aria-label="Sections">
<a href="#team">Team</a>
<a href="#os">The OS</a>
<a href="#included">Included</a>
<a href="#pricing">Pricing</a>
</nav>
<div class="railnote">
<b>Coop 42</b> runtime<br>
reflex · minute<br>
hour · day<br><br>
<a class="btn" style="margin-top:10px" href="https://cil.by/cabinet">Sign in</a>
</div>
</aside>
<main>
<!-- 00 · hero + live ladder -->
<div class="hero">
<p class="kicker in"><span class="dot"></span>Studio OS · live</p>
<h1 class="in d1">Hire nobody.<br><em>Ship everything.</em></h1>
<p class="lede in d2">A dedicated AI team — engineers, video producers, content makers — plus the operating system that runs them: projects, treasury, decisions, meetings. White-label, under your brand.</p>
<div class="cta-row in d3">
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Start your studio — $999/mo</a>
<a class="btn" href="https://demo.cil.by">Live demo</a>
<span class="cta-note">flat rate · cancel anytime</span>
</div>
<div class="console in d3" aria-label="Live task ladder demo">
<div class="bar">
<span class="ticks" aria-hidden="true"><i></i><i></i><i></i></span>
<span>studio ops · task ladder</span>
<span class="live"><i></i>3 agents online</span>
</div>
<div class="metrics">
<div class="metric"><div class="v" id="mDone">4<span class="u"> done</span></div><div class="k">tasks this cycle</div></div>
<div class="metric"><div class="v">4<span class="u">-cadence</span></div><div class="k">reflex→day runtime</div></div>
<div class="metric"><div class="v">S·M·L</div><div class="k">task sizes</div></div>
</div>
<div class="ladder" id="ladder"></div>
<div class="foot">hour digest 23:10 Minsk · <b>all actions signed</b> · click a row to re-dispatch</div>
</div>
</div>
<!-- 01 · team -->
<section id="team">
<div class="shead"><span class="sidx">01</span><h2>A staff of specialists, not a chatbot.</h2><span class="snote">dedicated servers</span></div>
<p class="lead">Every agent runs on its own server with private memory, tools, email and Matrix chat. You brief them like employees — they onboard onto your idea and start shipping.</p>
<div class="ledger">
<div class="lrow">
<span class="idx">/01</span>
<div class="nm">Cody<span>Engineer</span></div>
<div class="ds">Writes code, deploys services, maintains git repositories. MVPs, landing pages, bots, APIs, integrations — from brief to production.</div>
<div class="tags">full-stack · devops · git</div>
</div>
<div class="lrow">
<span class="idx">/02</span>
<div class="nm">ReelsUp<span>Video producer</span></div>
<div class="ds">Cuts long videos and podcasts into vertical clips for Reels and Shorts. Transcription, highlight detection, automated editing.</div>
<div class="tags">whisper · clips · shorts</div>
</div>
<div class="lrow">
<span class="idx">/03</span>
<div class="nm">Roy<span>Content maker</span></div>
<div class="ds">Promotional content on a conveyor: reels with 3D characters, scripts, voiceovers, visuals — brand-consistent by default.</div>
<div class="tags">3d · tts · scripts</div>
</div>
</div>
</section>
<!-- 02 · the os -->
<section id="os">
<div class="shead"><span class="sidx">02</span><h2>Agents build. The OS manages.</h2><span class="snote">your domain</span></div>
<p class="lead">Projects, money, decisions and meetings in one transparent system — under your studio's name.</p>
<div class="dl">
<div class="item"><span class="k">S.01</span><div><div class="t">Project registry</div><div class="d">A card for every venture: decisions, ownership, agents, activity feed.</div></div></div>
<div class="item"><span class="k">S.02</span><div><div class="t">Git repo per project</div><div class="d">Code and docs in a dedicated repository with a web editor. Nothing lost in chats.</div></div></div>
<div class="item"><span class="k">S.03</span><div><div class="t">Treasury &amp; wallets</div><div class="d">Revenue share flows in automatically. Every payout transparent and auditable.</div></div></div>
<div class="item"><span class="k">S.04</span><div><div class="t">Decisions &amp; voting</div><div class="d">Allocate resources by vote. Cycles, quorum, full decision history.</div></div></div>
<div class="item"><span class="k">S.05</span><div><div class="t">Meetings with AI minutes</div><div class="d">Video calls with recording, transcription, summaries — into the registry.</div></div></div>
<div class="item"><span class="k">S.06</span><div><div class="t">Time &amp; task tracking</div><div class="d">What every agent completed and where work went — a daily digest in your inbox.</div></div></div>
</div>
</section>
<!-- 03 · included -->
<section id="included">
<div class="shead"><span class="sidx">03</span><h2>Full stack. Nothing extra to buy.</h2><span class="snote">flat rate</span></div>
<div class="inc">
<div class="irow"><span class="mark">✓</span><div><div class="t">Stripe payments <span>live</span></div><div class="d">Card payments for what your agents build — subscriptions, one-offs, invoices. A working checkout on day one.</div></div></div>
<div class="irow"><span class="mark">✓</span><div><div class="t">AI inference included <span>flat</span></div><div class="d">All models run on our gateway — no API keys, no vendor bills, no metered surprises.</div></div></div>
<div class="irow"><span class="mark">✓</span><div><div class="t">White-label portal <span>your brand</span></div><div class="d">Your subdomain, your studio's name up front. Every deliverable ships under your identity.</div></div></div>
<div class="irow"><span class="mark">✓</span><div><div class="t">The agent harness <span>4-cadence</span></div><div class="d">Reflex, minute, hour, day — escalation, memory, semantic knowledge base. Coop-tested daily.</div></div></div>
<div class="irow"><span class="mark">✓</span><div><div class="t">One-click export <span>no lock-in</span></div><div class="d">Take everything with you, anytime. Ready in ~10 minutes after payment.</div></div></div>
</div>
</section>
<!-- 04 · pricing -->
<section id="pricing">
<div class="shead"><span class="sidx">04</span><h2>One plan each. Everything included.</h2><span class="snote">founding cohort</span></div>
<div class="plans">
<div class="plan hot">
<div class="pt">Personal · first come</div>
<div class="price">$99.9 <span class="per">/ month</span></div>
<ul>
<li>1 dedicated AI agent on its own server</li>
<li>Private Matrix chat — web + mobile</li>
<li>Inference, hosting, updates included</li>
<li>One-click export, no lock-in</li>
</ul>
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Get my agent</a>
<div class="fine">secure checkout by Stripe · cancel anytime</div>
</div>
<div class="plan">
<div class="pt">Studio</div>
<div class="price">$999 <span class="per">/ month</span></div>
<ul>
<li>12 AI agents — engineers, producers, content</li>
<li>White-label portal on your subdomain</li>
<li>Projects, treasury, decisions, meetings — one system</li>
<li>Team members free · priority support</li>
</ul>
<a class="btn" href="https://simbiot.cil.by/#pricing">Start your studio</a>
<div class="fine">deployed the same day · self-hosted? talk to us</div>
</div>
</div>
</section>
<footer>
<div class="fgrid">
<span>© 2026 Simbiot · Cooperative 42</span>
<a href="https://demo.cil.by">Demo</a>
<a href="https://cil.by/cabinet">Cabinet</a>
<a href="mailto:portal@cil.by">portal@cil.by</a>
<span class="sp">cil.by</span>
</div>
</footer>
</main>
</div>
<script>
/* signature artifact: a miniature of the real task ladder.
Reserved geometry: rows are pre-rendered, only state cell swaps. */
(function(){
var TASKS=[
{sz:'L', name:'Redesign pricing section copy', who:'cody'},
{sz:'M', name:'Weekly digest to stakeholders', who:'os'},
{sz:'S', name:'Fix og-image on landing', who:'cody'},
{sz:'M', name:'Escalation: router burn > budget', who:'os'}
];
var ST=['queued','running','done'], LABEL={queued:'queued',running:'running',done:'done'};
var states=[1,0,2,1], doneCount=4, ladder=document.getElementById('ladder');
if(!ladder) return;
var reduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var rows=TASKS.map(function(t,i){
var b=document.createElement('button');
b.type='button'; b.className='trow'; b.setAttribute('data-st',LABEL[states[i]]);
b.setAttribute('aria-label',t.name+' — '+LABEL[states[i]]+'. Re-dispatch.');
b.innerHTML='<span class="sz">['+t.sz+']</span>'+
'<span class="name"><b>'+t.name+'</b></span>'+
'<span class="st"><i></i>'+LABEL[states[i]]+'</span>'+
'<span class="who">'+t.who+'</span>';
b.addEventListener('click',function(){step(i);});
ladder.appendChild(b);
return b;
});
function paint(i){
var s=LABEL[states[i]];
rows[i].setAttribute('data-st',s);
rows[i].getElementsByClassName('st')[0].innerHTML='<i></i>'+s;
}
function step(i){
if(states[i]===2){states[i]=1;paint(i);return}
if(states[i]===1){states[i]=2;doneCount++;var m=document.getElementById('mDone');
if(m)m.innerHTML=doneCount+'<span class="u"> done</span>';paint(i);return}
states[i]=1;paint(i);
}
var cur=0;
function tick(){ if(states[cur]!==2){step(cur);} cur=(cur+1)%TASKS.length; }
if(!reduced){ var t=setInterval(tick,2200); document.addEventListener('visibilitychange',function(){ document.hidden?clearInterval(t):t=setInterval(tick,2200); }); }
})();
</script>
</body>
</html>

View file

@ -1,5 +0,0 @@
[dash] http://0.0.0.0:8765/ (health 127.0.0.1:8766/api/health) refresh=5s token=off
[dash] 127.0.0.1 "GET /api/health HTTP/1.1" 200 -
[dash] 185.203.154.209 "GET /api/state?t=1791449512284 HTTP/1.1" 200 -
[dash] 185.203.154.209 "GET /api/state?t=1791449517287 HTTP/1.1" 200 -
[dash] 185.203.154.209 "GET /api/state?t=1791449522283 HTTP/1.1" 200 -

View file

@ -1,209 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ИИШНИЦА · Панельная сессия «Общее видение» — концепт кооператива 42</title>
<style>
:root{
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
.wrap{max-width:980px;margin:0 auto;padding:56px 24px 100px}
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:36px}
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
h1{font-size:clamp(34px,6vw,58px);line-height:1.02;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
.sub{font-size:17px;color:var(--dim);max-width:740px}
.badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.badge{border:1px solid var(--ink);padding:5px 12px;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper)}
.badge.s{background:var(--ink);color:var(--paper)}
section{margin:52px 0 0}
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
h2{font-size:26px;font-weight:800;letter-spacing:-.01em;margin:4px 0 6px;text-transform:uppercase}
.lead{color:var(--dim);max-width:780px;margin-bottom:20px}
h3{font-size:14px;text-transform:uppercase;letter-spacing:.06em;margin:24px 0 10px;border-left:3px solid var(--signal);padding-left:10px}
p{margin:10px 0;max-width:840px}
.cardgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px;margin:18px 0}
.card{border:1px solid var(--ink);background:var(--card);padding:16px 16px 14px}
.card .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.card h4{font-size:15px;margin:6px 0;font-weight:800}
.card p{font-size:13.5px;color:var(--dim);margin:0;line-height:1.55}
.card p b{color:var(--ink)}
.timeline{margin:22px 0;border:1px solid var(--ink);background:#fff}
.trow{display:grid;grid-template-columns:120px 1fr;border-bottom:1px solid var(--line)}
.trow:last-child{border-bottom:none}
.trow .t{font-family:ui-monospace,Menlo,monospace;font-weight:700;padding:12px 14px;border-right:1px solid var(--line);display:flex;align-items:center;font-size:13px}
.trow .d{padding:12px 16px}
.trow .d b{display:block}
.trow .d span{font-size:13.5px;color:var(--dim);display:block;margin-top:2px}
.note{border:1px solid var(--ink);border-left:6px solid var(--signal);background:#fff;padding:14px 18px;margin:20px 0;max-width:860px}
.note b{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--signal)}
ul{margin:8px 0 8px 18px;font-size:14.5px}
li{margin:6px 0}
li::marker{color:var(--signal)}
.mission{border:3px double var(--ink);background:#fff;padding:26px 28px;margin:22px 0}
.mission p{font-size:19px;line-height:1.5;max-width:840px}
.mission .src{font-size:12px;color:var(--dim);font-family:ui-monospace,Menlo,monospace;margin-top:12px}
.qbox{border:1px solid var(--ink);background:var(--card);padding:16px 20px;margin:12px 0}
.qbox .q{font-size:16.5px;font-weight:600;line-height:1.45}
.qbox .m{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);letter-spacing:.15em;font-weight:700}
footer{margin-top:60px;border-top:1px solid var(--line);padding-top:18px;color:var(--dim);font-size:12.5px}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">ИИШНИЦА · Панельная сессия · Концепт</div>
<h1>Общее видение</h1>
<p class="sub">Формат панельной сессии на ИИШНИЦЕ: миссия 42 задаёт рамку, спикеры из разных индустрий описывают будущее — и панель сходится к названию общего видения.</p>
<div class="badges">
<span class="badge s">90 минут</span>
<span class="badge">5–6 спикеров</span>
<span class="badge">3 круга + зал</span>
<span class="badge">Миссия 42</span>
<span class="badge">Минск · cil.by</span>
</div>
</header>
<section id="idea">
<div class="snum">01 / ИДЕЯ</div>
<h2>Суть формата</h2>
<p class="lead">Не классическая панель с вопросами модератора, а три круга с одной целью: собрать из отдельных картин будущего — общую. Миссия 42 озвучивается в начале как рамка, объединяющая любые индустрии и любые ответы.</p>
<div class="mission">
<p><b>Миссия «42»:</b> все миссии одинаково важны. Миссия «42» объединяет абсолютно все возможные миссии. Нет неважных миссий — все люди важны, у каждого своя миссия.</p>
<div class="src">— кооператив «Друзья Коллективного Разума», Минск · cil.by</div>
</div>
<p>Почему это работает: каждый спикер говорит о <b>своей</b> индустрии — и потому не конкурирует с соседом за «правильный ответ». А третий круг, название общего видения, превращает панель из набора мнений в один артефакт, который остаётся после сессии: в заголовках, фото и цитатах.</p>
</section>
<section id="format">
<div class="snum">02 / ФОРМАТ</div>
<h2>Три круга + зал</h2>
<p class="lead">Каждый круг имеет свою функцию: раскрыть, связать, назвать. Тайминг дан для 6 спикеров; при 5 спикерах сессия ужимается до ~80 минут.</p>
<div class="timeline">
<div class="trow">
<div class="t">00:00–00:05</div>
<div class="d"><b>Открытие и миссия 42</b><span>Модератор (или Alex) озвучивает миссию 42 как рамку сессии: все миссии одинаково важны — потому каждый взгляд на будущее важен.</span></div>
</div>
<div class="trow">
<div class="t">00:05–00:29</div>
<div class="d"><b>Круг 1 — «Моё будущее» · 4 мин × 6</b><span>Каждый спикер описывает будущее своей индустрии в самом дальнем горизонте, в который он может заглянуть. Без вопросов, без перебиваний. 24 минуты.</span></div>
</div>
<div class="trow">
<div class="t">00:29–00:41</div>
<div class="d"><b>Круг 2 — «Что понравилось» · 2 мин × 6</b><span>Каждый подсвечивает, что понравилось у других спикеров. Не спорить, а связывать: где он увидел пересечение со своим будущим. 12 минут.</span></div>
</div>
<div class="trow">
<div class="t">00:41–00:55</div>
<div class="d"><b>Круг 3 — «Название общего видения»</b><span>Панель соглашается назвать общее видение — одно название для всех картин будущего. Модератор помогает сформулировать и фиксирует финальную формулировку. 14 минут.</span></div>
</div>
<div class="trow">
<div class="t">00:55–01:25</div>
<div class="d"><b>Вопросы из зала</b><span>Зал спрашивает — спикеры отвечают. Модератор держит рамку миссии: любой вопрос важен, любая индустрия равна. 30 минут.</span></div>
</div>
<div class="trow">
<div class="t">01:25–01:30</div>
<div class="d"><b>Закрытие</b><span>Модератор повторяет название общего видения и миссию 42. Короткое резюме — для фото и записи.</span></div>
</div>
</div>
<h3>Роли</h3>
<div class="cardgrid">
<div class="card">
<div class="cn">МОДЕРАТОР</div>
<h4>Держит рамку и тайминг</h4>
<p>Озвучивает миссию, следит за 4/2 минутами, ведёт панель к названию общего видения. <b>Жёсткий тайм-кипер</b>: «осталось 30 секунд».</p>
</div>
<div class="card">
<div class="cn">СПИКЕРЫ</div>
<h4>5–6 человек из разных индустрий</h4>
<p>Три задания: будущее своей индустрии (4 мин), что понравилось у других (2 мин), название общего видения. <b>Без презентаций</b> — только голос.</p>
</div>
<div class="card">
<div class="cn">ЗАЛ</div>
<h4>Четвёртый участник</h4>
<p>Вопросы после трёх кругов. В круге 3 модератор может спросить зал: какое название общего видения предложили бы вы?</p>
</div>
</div>
<div class="note">
<b>Правило тайминга</b>
<p>Карточки 60/30/10 секунд в руках тайм-кипера, звуковой сигнал на переходах между кругами. Если спикер не уложился в 4 минуты — модератор мягко забирает слово: формулировки доскажет во втором круге.</p>
</div>
</section>
<section id="why">
<div class="snum">03 / ПОЧЕМУ ЭТО СРАБОТАЕТ</div>
<h2>Что делает формат сильным</h2>
<div class="cardgrid">
<div class="card">
<div class="cn">01</div>
<h4>Круг 1 без перебиваний</h4>
<p>4 минуты каждому без вопросов — редкость на панелях. Спикеры дают глубокие ответы, а не заготовки. Глубина = запоминаемость.</p>
</div>
<div class="card">
<div class="cn">02</div>
<h4>Круг 2 связывает</h4>
<p>«Что понравилось» вместо «в чём не согласен» — позитивная рамка: панель выглядит единой, а не спорящей. Зал видит созвучие индустрий.</p>
</div>
<div class="card">
<div class="cn">03</div>
<h4>Круг 3 — артефакт</h4>
<p>Название общего видения — то, что остаётся после сессии: в заголовках, фото, цитатах. Панель производит смысл, а не обмен мнениями.</p>
</div>
<div class="card">
<div class="cn">04</div>
<h4>Миссия 42 как рамка</h4>
<p>«Все миссии одинаково важны» легитимирует любой ответ и любую индустрию — от ритейла до робототехники. Спикерам безопасно говорить смело.</p>
</div>
</div>
</section>
<section id="name">
<div class="snum">04 / НАЗВАНИЕ ОБЩЕГО ВИДЕНИЯ</div>
<h2>Заготовки для третьего круга</h2>
<p class="lead">Третий круг должен закончиться названием. Если панель не сойдётся сама, модератор предлагает шаблоны — панель выбирает или дорабатывает.</p>
<div class="qbox"><div class="m">ЗАГОТОВКА 01</div><div class="q">«Симбиоз» — будущее, где ИИ усиливает каждую миссию человека, не заменяя её.</div></div>
<div class="qbox"><div class="m">ЗАГОТОВКА 02</div><div class="q">«Коллективный разум» — каждый умножает свой вклад через общую инфраструктуру.</div></div>
<div class="qbox"><div class="m">ЗАГОТОВКА 03</div><div class="q">«42» — число, которое объединяет все миссии.</div></div>
</section>
<section id="checklist">
<div class="snum">05 / ЧЕК-ЛИСТ ОРГАНИЗАТОРА</div>
<h2>Что нужно до сессии</h2>
<ul>
<li><b>Состав панели:</b> 5–6 спикеров из максимально разных индустрий (финансы, медицина, промышленность, образование, медиа, госсектор). Разные горизонты видения — от прагматиков до футуристов.</li>
<li><b>Брифинг спикеров за 2–3 дня:</b> каждому объяснить три круга, тайминг 4/2 минуты и установку «загляни в самый дальний горизонт, в который можешь». Никаких презентаций.</li>
<li><b>Модератор:</b> умеет жёстко держать тайминг и мягко вести к названию. Репетиция открытия с миссией 42.</li>
<li><b>Тайм-кипер:</b> карточки 60/30/10 секунд, звуковой сигнал на переходах между кругами.</li>
<li><b>Зал:</b> микрофоны для вопросов, очередь или карточки для сбора вопросов.</li>
<li><b>Запись:</b> видео обязательно — название общего видения и лучшие цитаты круга 2 идут в посты и клипы после сессии.</li>
<li><b>Баннер миссии:</b> слайд или растяжка с текстом миссии 42 на сцене — рамка сессии видна на любой фотографии.</li>
</ul>
</section>
<section id="coop">
<div class="snum">06 / СВЯЗЬ С КООПЕРАТИВОМ</div>
<h2>Что это даёт 42</h2>
<p class="lead">Панель — не только контент, но и вход в экосистему кооператива для людей с миссиями.</p>
<ul>
<li><b>Поток проектов:</b> спикеры и зал видят формат работы кооператива — люди с миссиями приходят в 42, регистрируют проекты, усиливают долгосрочную AUM.</li>
<li><b>Точка входа:</b> портал cil.by упоминается в открытии и закрытии — для тех, кто захочет глубже после сессии.</li>
<li><b>Бренд:</b> миссия 42 на сцене — фирменный приём кооператива, отличающий сессию от других панелей конференции.</li>
</ul>
</section>
<footer>
Концепт панельной сессии «Общее видение» · ИИШНИЦА · кооператив «Друзья Коллективного Разума» · Минск · <span class="mono">cil.by</span><br>
Черновик для обсуждения v0.1 · <span class="mono">simbiot-harness / hour</span> · 2026-10-09
</footer>
</div>
</body>
</html>

Binary file not shown.

View file

@ -1,281 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>Разговор с Леонидом — выжимка (встреча 143)</title>
<style>
html {
color: #1a1a1a;
background-color: #fdfdfd;
}
body {
margin: 0 auto;
max-width: 36em;
padding-left: 50px;
padding-right: 50px;
padding-top: 50px;
padding-bottom: 50px;
hyphens: auto;
overflow-wrap: break-word;
text-rendering: optimizeLegibility;
font-kerning: normal;
}
@media (max-width: 600px) {
body {
font-size: 0.9em;
padding: 12px;
}
h1 {
font-size: 1.8em;
}
}
@media print {
html {
background-color: white;
}
body {
background-color: transparent;
color: black;
font-size: 12pt;
}
p, h2, h3 {
orphans: 3;
widows: 3;
}
h2, h3, h4 {
page-break-after: avoid;
}
}
p {
margin: 1em 0;
}
a {
color: #1a1a1a;
}
a:visited {
color: #1a1a1a;
}
img {
max-width: 100%;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.4em;
}
h5, h6 {
font-size: 1em;
font-style: italic;
}
h6 {
font-weight: normal;
}
ol, ul {
padding-left: 1.7em;
margin-top: 1em;
}
li > ol, li > ul {
margin-top: 0;
}
blockquote {
margin: 1em 0 1em 1.7em;
padding-left: 1em;
border-left: 2px solid #e6e6e6;
color: #606060;
}
code {
font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
font-size: 85%;
margin: 0;
hyphens: manual;
}
pre {
margin: 1em 0;
overflow: auto;
}
pre code {
padding: 0;
overflow: visible;
overflow-wrap: normal;
}
.sourceCode {
background-color: transparent;
overflow: visible;
}
hr {
background-color: #1a1a1a;
border: none;
height: 1px;
margin: 1em 0;
}
table {
margin: 1em 0;
border-collapse: collapse;
width: 100%;
overflow-x: auto;
display: block;
font-variant-numeric: lining-nums tabular-nums;
}
table caption {
margin-bottom: 0.75em;
}
tbody {
margin-top: 0.5em;
border-top: 1px solid #1a1a1a;
border-bottom: 1px solid #1a1a1a;
}
th {
border-top: 1px solid #1a1a1a;
padding: 0.25em 0.5em 0.25em 0.5em;
}
td {
padding: 0.125em 0.5em 0.25em 0.5em;
}
header {
margin-bottom: 4em;
text-align: center;
}
#TOC li {
list-style: none;
}
#TOC ul {
padding-left: 1.3em;
}
#TOC > ul {
padding-left: 0;
}
#TOC a:not(:hover) {
text-decoration: none;
}
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
/* The extra [class] is a hack that increases specificity enough to
override a similar rule in reveal.js */
ul.task-list[class]{list-style: none;}
ul.task-list li input[type="checkbox"] {
font-size: inherit;
width: 0.8em;
margin: 0 0.8em 0.2em -1.6em;
vertical-align: middle;
}
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
</style>
</head>
<body>
<header id="title-block-header">
<h1 class="title">Разговор с Леонидом — выжимка (встреча 143)</h1>
</header>
<h1 id="разговор-алекса-с-леонидом-выжимка-и-договорённости">Разговор
Алекса с Леонидом — выжимка и договорённости</h1>
<p><em>Встреча 143 · 2026-10-08 · ~2.5 часа · Jitsi + агент-визуализатор
Simbiot</em></p>
<hr />
<h2 id="слайд-1-кто-есть-кто">Слайд 1 — Кто есть кто</h2>
<ul>
<li><strong>Леонид</strong> — давний знакомый студии, начинающий
писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром
Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём
CUDA-моделирование графов надёжности.</li>
<li><strong>Алекс</strong> — показал студийную инфраструктуру:
агент-визуализатор звонков (демо в реальном времени), собственный роутер
LM-провайдеров, RAG по всем знаниям кооператива.</li>
<li>Демо произвело впечатление: «1-й раз такое в моей практике… польза
очевидна».</li>
</ul>
<h2 id="слайд-2-демо-инфраструктуры-студии">Слайд 2 — Демо
инфраструктуры студии</h2>
<ul>
<li>Агент подключается к любому звонку, рисует слайды по ходу разговора
(задержка ~45 сек — лимит генерации).</li>
<li>Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3
Flash).</li>
<li>RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний
кооператива.</li>
<li>Леонид получил полезный инсайт уже из демо: на его домашней
коробочке можно крутить быструю генерацию музыки/картинок/мини-видео —
трек за минуту.</li>
</ul>
<h2 id="слайд-3-главный-контакт-пилот-мультик-по-рассказу-леонида">Слайд
3 — Главный контакт: пилот «мультик по рассказу Леонида»</h2>
<ul>
<li>Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов
<strong>финансово поддержать</strong> пилот: токены — точно,
человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на
эксперимент.</li>
<li>Вход от студии: рассказ Леонида + свобода интерпретации, «никаких
пожеланий-ограничений».</li>
<li>Alex предложил: <strong>партнёр студии уже строит harness для
генерации мультиков/клипов/читов</strong> — матч идеальный. Влад
(профессиональный видеомейкер + озвучка) возьмёт видео-часть.</li>
<li>Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем
видео.</li>
</ul>
<h2
id="слайд-4-творческий-метод-леонида-поучительно-для-продуктовой-линзы">Слайд
4 — Творческий метод Леонида (поучительно для продуктовой линзы)</h2>
<ul>
<li>История с хакатоном ~2018: обучали нейросеть на сольных песнях
Леннона/Маккартни и классифицировали битловские — «десяток песен
правильно заклассифицировали».</li>
<li>Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно
маркирует «made with AI» на after.today.</li>
<li>Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело —
ИИ-ассистированное творчество не ново, нова честность.</li>
</ul>
<h2 id="слайд-5-экономика-локального-инференса-спор-о-железе">Слайд 5 —
Экономика локального инференса (спор о железе)</h2>
<ul>
<li>Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за
«домашний интеллект»; тренд — коробки станут достоянием домов.</li>
<li>Алекс: облачный always-on — ок для большинства задач; топовые модели
для разработки софта всё равно выигрывают у локальных.</li>
<li>Сошлись: гибрид — локально быстрая генерация (эскизы, музыка),
облако — для тяжёлых продуктовых задач. У студии в планах собственный
inference-сервер (инференс в ~6 раз быстрее).</li>
</ul>
<h2 id="слайд-6-обмен-тезисами-как-строить-ai-компанию">Слайд 6 — Обмен
тезисами: как строить AI-компанию</h2>
<ul>
<li><strong>Леонид:</strong> «Vision — говно, execution — всё» (шутка из
EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с
одним промптом построят 10 одинаковых Uber’ов — вариабельность требует
доп. информации.</li>
<li><strong>Алекс:</strong> ядро студии — AI-ядро, постоянно
перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно
самый большой актив».</li>
<li>Стратегия: строить бизнес на нейро-инфраструктуре с самого начала →
потом переводить на автопилот; незаменим только «CEO-решатель».</li>
<li>Леонид о студии: «я теперь понимаю, чем твоя студия отличается от
классической — изюминка в AI-ядре».</li>
<li>Методология Леонида для deep-tech: начинать продавать ценность сразу
(консалтинг целевого клиента), не влюбляясь в R&amp;D.</li>
</ul>
<h2 id="слайд-7-договорённости-что-фиксируем">Слайд 7 — Договорённости
(что фиксируем)</h2>
<ol type="1">
<li><strong>Пилот анимации по рассказу Леонида</strong> — Leonid
покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт
Влад). Alex вернётся к Леониду с конкретным предложением.</li>
<li><strong>Формат разговора продолжается</strong> — Леонид открыт к
следующим сессиям.</li>
<li><strong>По голосам/долям за прошлые инвестиции — Леонид
отказался</strong> («это была шутка», «инвестирую из философских
соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.</li>
<li>Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с
фидбэком.</li>
</ol>
<h2 id="слайд-8-что-студии-делать-дальше">Слайд 8 — Что студии делать
дальше</h2>
<ul>
<li>Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета
токенов) — Alex озвучит Леониду предметно.</li>
<li>Подключить партнёрский harness генерации мультиков к задаче Леонида
как первый клиентский кейс.</li>
<li>Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» —
первое демо на живом стороннем человеке, положительный фидбэк.</li>
</ul>
</body>
</html>

View file

@ -1,57 +0,0 @@
# Разговор Алекса с Леонидом — выжимка и договорённости
*Встреча 143 · 2026-10-08 · ~2.5 часа · Jitsi + агент-визуализатор Simbiot*
---
## Слайд 1 — Кто есть кто
- **Леонид** — давний знакомый студии, начинающий писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём CUDA-моделирование графов надёжности.
- **Алекс** — показал студийную инфраструктуру: агент-визуализатор звонков (демо в реальном времени), собственный роутер LM-провайдеров, RAG по всем знаниям кооператива.
- Демо произвело впечатление: «1-й раз такое в моей практике… польза очевидна».
## Слайд 2 — Демо инфраструктуры студии
- Агент подключается к любому звонку, рисует слайды по ходу разговора (задержка ~45 сек — лимит генерации).
- Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3 Flash).
- RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний кооператива.
- Леонид получил полезный инсайт уже из демо: на его домашней коробочке можно крутить быструю генерацию музыки/картинок/мини-видео — трек за минуту.
## Слайд 3 — Главный контакт: пилот «мультик по рассказу Леонида»
- Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов **финансово поддержать** пилот: токены — точно, человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на эксперимент.
- Вход от студии: рассказ Леонида + свобода интерпретации, «никаких пожеланий-ограничений».
- Alex предложил: **партнёр студии уже строит harness для генерации мультиков/клипов/читов** — матч идеальный. Влад (профессиональный видеомейкер + озвучка) возьмёт видео-часть.
- Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео.
## Слайд 4 — Творческий метод Леонида (поучительно для продуктовой линзы)
- История с хакатоном ~2018: обучали нейросеть на сольных песнях Леннона/Маккартни и классифицировали битловские — «десяток песен правильно заклассифицировали».
- Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно маркирует «made with AI» на after.today.
- Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность.
## Слайд 5 — Экономика локального инференса (спор о железе)
- Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за «домашний интеллект»; тренд — коробки станут достоянием домов.
- Алекс: облачный always-on — ок для большинства задач; топовые модели для разработки софта всё равно выигрывают у локальных.
- Сошлись: гибрид — локально быстрая генерация (эскизы, музыка), облако — для тяжёлых продуктовых задач. У студии в планах собственный inference-сервер (инференс в ~6 раз быстрее).
## Слайд 6 — Обмен тезисами: как строить AI-компанию
- **Леонид:** «Vision — говно, execution — всё» (шутка из EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует доп. информации.
- **Алекс:** ядро студии — AI-ядро, постоянно перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно самый большой актив».
- Стратегия: строить бизнес на нейро-инфраструктуре с самого начала → потом переводить на автопилот; незаменим только «CEO-решатель».
- Леонид о студии: «я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре».
- Методология Леонида для deep-tech: начинать продавать ценность сразу (консалтинг целевого клиента), не влюбляясь в R&D.
## Слайд 7 — Договорённости (что фиксируем)
1. **Пилот анимации по рассказу Леонида** — Leonid покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт Влад). Alex вернётся к Леониду с конкретным предложением.
2. **Формат разговора продолжается** — Леонид открыт к следующим сессиям.
3. **По голосам/долям за прошлые инвестиции — Леонид отказался** («это была шутка», «инвестирую из философских соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.
4. Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с фидбэком.
## Слайд 8 — Что студии делать дальше
- Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета токенов) — Alex озвучит Леониду предметно.
- Подключить партнёрский harness генерации мультиков к задаче Леонида как первый клиентский кейс.
- Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» — первое демо на живом стороннем человеке, положительный фидбэк.

View file

@ -1,436 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Встреча 143 · Алекс × Леонид — выжимка и договорённости</title>
<style>
:root{
--bg:#0a0c12;
--bg2:#0e1120;
--ink:#f0eeff;
--mut:#9a97b8;
--dim:#6f6c8f;
--vio:#8b5cf6;
--vio2:#7c6cf5;
--cyan:#5eead4;
--amber:#fbbf24;
--card:rgba(255,255,255,.035);
--line:rgba(139,92,246,.28);
--serif:Georgia,"Times New Roman",serif;
--sans:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:var(--bg);
color:var(--ink);
font-family:var(--sans);
overflow:hidden;
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
}
/* ---------- ambient backdrop ---------- */
.bg{position:fixed;inset:0;z-index:0;pointer-events:none}
.bg::before{
content:"";position:absolute;inset:0;
background:
radial-gradient(60vw 60vw at 12% -10%, rgba(124,108,245,.20), transparent 62%),
radial-gradient(55vw 55vw at 105% 112%, rgba(94,234,212,.09), transparent 60%),
radial-gradient(70vw 40vw at 50% 120%, rgba(139,92,246,.10), transparent 65%),
linear-gradient(180deg,var(--bg) 0%,var(--bg2) 100%);
}
.bg::after{ /* film grain */
content:"";position:absolute;inset:0;opacity:.05;mix-blend-mode:overlay;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='160' height='160' filter='url(%23n)' opacity='0.6'/></svg>");
}
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;
background:radial-gradient(120vw 90vh at 50% 50%, transparent 55%, rgba(0,0,0,.55) 100%)}
/* ---------- deck ---------- */
.deck{position:relative;z-index:2;height:100%}
.slide{
position:absolute;inset:0;
display:flex;flex-direction:column;justify-content:center;
padding:9vh 8vw 12vh;
opacity:0;transform:translateY(26px) scale(.985);
transition:opacity .55s ease,transform .55s cubic-bezier(.22,.61,.36,1);
pointer-events:none;visibility:hidden;
}
.slide.active{opacity:1;transform:none;pointer-events:auto;visibility:visible}
.slide.prev{transform:translateY(-26px) scale(.985)}
.accent{position:absolute;top:0;left:0;right:0;height:6px;
background:linear-gradient(90deg,var(--vio) 0%,#a78bfa 38%,var(--cyan) 100%);
box-shadow:0 0 24px rgba(139,92,246,.55)}
.ghost{
position:absolute;right:4vw;bottom:2vh;z-index:-1;
font-weight:900;font-size:clamp(160px,30vw,420px);line-height:1;
color:transparent;-webkit-text-stroke:1.5px rgba(139,92,246,.13);
letter-spacing:-.04em;user-select:none}
.kicker{
display:inline-flex;align-items:center;gap:.6em;
font-size:clamp(11px,1.15vw,14px);font-weight:700;letter-spacing:.24em;
text-transform:uppercase;color:var(--vio2);margin-bottom:2.4vh;
}
.kicker::before{content:"";width:34px;height:2px;background:var(--vio);box-shadow:0 0 12px var(--vio)}
h1{font-size:clamp(38px,6.6vw,86px);font-weight:900;letter-spacing:-.03em;line-height:1.04}
h1 em{font-style:normal;background:linear-gradient(92deg,#a78bfa,var(--vio2) 55%,var(--cyan));
-webkit-background-clip:text;background-clip:text;color:transparent}
h2{font-size:clamp(28px,4vw,52px);font-weight:800;letter-spacing:-.025em;line-height:1.08;margin-bottom:3.6vh}
h2 .alt{color:var(--vio2)}
.sub{font-size:clamp(15px,1.6vw,21px);color:var(--mut);max-width:34em;line-height:1.55;margin-top:3vh}
.chips{display:flex;flex-wrap:wrap;gap:.7em;margin-top:4.5vh}
.chip{
font-family:var(--mono);font-size:clamp(11px,1.05vw,13px);color:var(--mut);
padding:.55em 1.1em;border:1px solid var(--line);border-radius:999px;
background:rgba(139,92,246,.07)}
.chip b{color:var(--ink);font-weight:600}
/* ---------- content blocks ---------- */
.grid{display:grid;gap:1.5vh 1.6vw}
.g2{grid-template-columns:repeat(2,1fr)}
.g3{grid-template-columns:repeat(3,1fr)}
@media(max-width:860px){.g2,.g3{grid-template-columns:1fr}}
.card{
background:var(--card);border:1px solid rgba(255,255,255,.07);
border-radius:18px;padding:2.6vh 1.6vw;position:relative;overflow:hidden;
backdrop-filter:blur(6px);
transition:border-color .3s,transform .3s;
}
.card:hover{border-color:var(--line);transform:translateY(-2px)}
.card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;
background:linear-gradient(180deg,var(--vio),transparent 85%);opacity:.85}
.card h3{font-size:clamp(15px,1.45vw,19px);font-weight:700;margin-bottom:1vh;letter-spacing:-.01em}
.card p,.card li{font-size:clamp(13px,1.28vw,17px);color:var(--mut);line-height:1.62}
.card p b,.card li b{color:var(--ink);font-weight:600}
.tag{display:inline-block;font-family:var(--mono);font-size:clamp(10px,.95vw,12px);
color:var(--cyan);border:1px solid rgba(94,234,212,.3);border-radius:6px;
padding:.15em .6em;margin-bottom:1.2vh;letter-spacing:.06em}
.tag.v{color:#c4b5fd;border-color:rgba(139,92,246,.4)}
.tag.a{color:var(--amber);border-color:rgba(251,191,36,.35)}
ul.list{list-style:none;display:grid;gap:1.7vh}
ul.list li{
display:flex;gap:1em;align-items:flex-start;
font-size:clamp(14px,1.5vw,19px);color:var(--mut);line-height:1.6;
}
ul.list li::before{
content:"▸";color:var(--vio);font-size:1.05em;line-height:1.5;
text-shadow:0 0 14px rgba(139,92,246,.8);flex:0 0 auto}
ul.list b{color:var(--ink);font-weight:650}
ul.list .hl{color:var(--cyan)}
.quote{
margin-top:3.2vh;padding:2.4vh 2vw;border-left:3px solid var(--vio);
background:linear-gradient(90deg,rgba(139,92,246,.12),transparent 72%);
border-radius:0 14px 14px 0;
}
.quote p{font-family:var(--serif);font-style:italic;
font-size:clamp(17px,2.1vw,27px);line-height:1.45;color:#e4e0ff}
.quote cite{display:block;margin-top:1.2vh;font-style:normal;font-family:var(--mono);
font-size:clamp(10px,1vw,13px);color:var(--dim);letter-spacing:.12em;text-transform:uppercase}
.num{display:flex;gap:1.1em;align-items:flex-start;padding:1.9vh 0;
border-bottom:1px dashed rgba(255,255,255,.09)}
.num:last-child{border-bottom:none}
.num i{
font-style:normal;font-family:var(--mono);flex:0 0 auto;
width:2.1em;height:2.1em;display:grid;place-items:center;border-radius:10px;
background:rgba(139,92,246,.16);border:1px solid var(--line);
color:#c4b5fd;font-size:clamp(12px,1.15vw,15px);font-weight:700}
.num h3{font-size:clamp(15px,1.5vw,19px);font-weight:700;margin-bottom:.5vh}
.num p{font-size:clamp(13px,1.3vw,16px);color:var(--mut);line-height:1.6}
.num b{color:var(--ink);font-weight:600}
.vs{display:grid;grid-template-columns:1fr auto 1fr;gap:1.2vw;align-items:stretch}
@media(max-width:860px){.vs{grid-template-columns:1fr}.vs .vsx{display:none}}
.vs .card{height:100%}
.vs .who{font-family:var(--mono);font-size:clamp(10px,.95vw,12px);letter-spacing:.14em;
text-transform:uppercase;color:var(--vio2);margin-bottom:1.2vh}
.vsx{display:grid;place-items:center;color:var(--dim);font-size:clamp(18px,2vw,26px)}
.big{font-size:clamp(30px,4.6vw,64px);font-weight:900;letter-spacing:-.03em;line-height:1.1}
.big em{font-style:normal;color:var(--vio2)}
/* ---------- chrome ---------- */
.progress{position:fixed;left:0;bottom:0;height:3px;z-index:10;width:0;
background:linear-gradient(90deg,var(--vio),#a78bfa,var(--cyan));
box-shadow:0 0 14px rgba(139,92,246,.8);transition:width .45s ease}
.hud{position:fixed;right:2.2vw;bottom:3vh;z-index:10;display:flex;gap:.6em;align-items:center}
.hud .count{font-family:var(--mono);font-size:12px;color:var(--dim);letter-spacing:.1em;margin-right:.8em}
.hud .count b{color:var(--mut);font-weight:600}
.hud button{
width:40px;height:40px;border-radius:12px;cursor:pointer;
border:1px solid var(--line);background:rgba(139,92,246,.1);color:#c4b5fd;
font-size:16px;display:grid;place-items:center;transition:.25s}
.hud button:hover{background:rgba(139,92,246,.25);color:#fff;transform:translateY(-1px)}
.hint{position:fixed;left:2.2vw;bottom:3vh;z-index:10;
font-family:var(--mono);font-size:11px;color:var(--dim);letter-spacing:.08em;opacity:.8}
.brand{position:fixed;top:2.6vh;right:2.2vw;z-index:10;
font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--dim);text-transform:uppercase}
.brand b{color:var(--vio2)}
.dots{position:fixed;right:1.4vw;top:50%;transform:translateY(-50%);z-index:10;
display:flex;flex-direction:column;gap:.55em}
.dots i{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.16);cursor:pointer;transition:.3s}
.dots i.on{background:var(--vio);box-shadow:0 0 10px var(--vio);transform:scale(1.35)}
@media(max-width:700px){.dots{display:none}.hint{display:none}}
.end .sig{font-family:var(--mono);font-size:clamp(10px,1vw,13px);color:var(--dim);
letter-spacing:.18em;text-transform:uppercase;margin-top:5vh}
/* ---------- print ---------- */
@media print{
body{overflow:visible;background:#0a0c12}
.bg,.vignette,.hud,.hint,.dots,.progress,.brand{display:none!important}
.deck{height:auto}
.slide{position:relative;inset:auto;opacity:1!important;transform:none!important;
visibility:visible!important;pointer-events:auto;height:100vh;page-break-after:always}
}
</style>
</head>
<body>
<div class="bg"></div><div class="vignette"></div>
<div class="brand">Simbiot · <b>Кооператив 42</b></div>
<div class="deck" id="deck">
<!-- 01 · TITLE -->
<section class="slide">
<div class="accent"></div><div class="ghost">143</div>
<div class="kicker">Встреча 143 · 08.10.2026 · Jitsi + агент-визуализатор Simbiot</div>
<h1>Разговор<br>Алекса <em>× Леонида</em></h1>
<p class="sub">Выжимка двухсполовинойчасовой встречи: инфраструктура студии, пилот «мультик по рассказу», экономика локального инференса — и договорённости, которые фиксируем.</p>
<div class="chips">
<span class="chip"><b>~2.5</b> часа разговора</span>
<span class="chip"><b>8</b> тем</span>
<span class="chip"><b>4</b> договорённости</span>
<span class="chip">слайды рисовал агент <b>live</b></span>
</div>
</section>
<!-- 02 · WHO -->
<section class="slide">
<div class="accent"></div><div class="ghost">01</div>
<div class="kicker">Кто есть кто</div>
<h2>Два мира на одном звонке</h2>
<div class="grid g2">
<div class="card">
<div class="who tag v">Леонид</div>
<h3>Писатель · инвестор в deep-tech</h3>
<p>Давний знакомый студии. Инвестирует в deep-tech, в том числе <b>Anti-Aging Hero</b> с физиком Петром Федичевым. Дома — <b>DGX Spark 128GB</b> (€5000), на которой он гоняет CUDA-моделирование графов надёжности.</p>
</div>
<div class="card">
<div class="tag">Алекс · студия</div>
<h3>Живая AI-инфраструктура</h3>
<p>Показал, чем студия отличается от классической: <b>агент-визуализатор звонков</b> (демо в реальном времени), собственный <b>роутер LM-провайдеров</b> и <b>RAG</b> по всем знаниям кооператива.</p>
</div>
</div>
<div class="quote">
<p>«Первый раз такое в моей практике… польза очевидна»</p>
<cite>Леонид — после демо агент-визуализатора</cite>
</div>
</section>
<!-- 03 · DEMO -->
<section class="slide">
<div class="accent"></div><div class="ghost">02</div>
<div class="kicker">Демо инфраструктуры</div>
<h2>Что увидел Леонид <span class="alt">в прямом эфире</span></h2>
<ul class="list">
<li><span><b>Агент подключается к любому звонку</b> и рисует слайды по ходу разговора — задержка ~45 секунд (лимит генерации)</span></li>
<li><span><b>Роутер сам выбирает модель под контекст</b> — визуализация идёт на GLM 5.3 Flash</span></li>
<li><span><b>RAG по базе кооператива:</b> «он знает всё, что мы делаем» — агент отвечает со знанием всей истории студии</span></li>
<li><span>Инсайт Леонида из демо: <span class="hl">на его домашней коробочке можно крутить быструю генерацию</span> — музыку, картинки, мини-видео. Трек за минуту</span></li>
</ul>
</section>
<!-- 04 · PILOT -->
<section class="slide">
<div class="accent"></div><div class="ghost">03</div>
<div class="kicker">Главный контакт встречи</div>
<h2>Пилот: <span class="alt">мультик по рассказу Леонида</span></h2>
<div class="grid g2">
<div class="card">
<div class="tag a">Что хочет Леонид</div>
<p><b>Мульт-адаптация своих рассказов</b> (10–15 минут). Готов финансово поддержать пилот: <b>токены — точно</b>, человеческий труд — обсудить. Ориентир бюджета — <b>«в пределах 1000»</b> на эксперимент.</p>
</div>
<div class="card">
<div class="tag v">Что входит студия</div>
<p>Рассказ Леонида + <b>свобода интерпретации</b> («никаких пожеланий-ограничений»). Со стороны студии — партнёр уже строит <b>harness генерации мультиков</b>, видео-часть ведёт <b>Влад</b> (профессиональный видеомейкер + озвучка). Матч идеальный.</p>
</div>
</div>
<div class="quote">
<p>Первый шаг — аудиоспектакль: дешевле и предсказуемее. Затем видео.</p>
<cite>Дорожная карта пилота</cite>
</div>
</section>
<!-- 05 · METHOD -->
<section class="slide">
<div class="accent"></div><div class="ghost">04</div>
<div class="kicker">Творческий метод Леонида</div>
<h2>Поучительно для <span class="alt">продуктовой линзы</span></h2>
<ul class="list">
<li><span><b>Хакатон ~2018:</b> обучали нейросеть на сольных песнях Леннона и Маккартни, затем классифицировали битловские — «десяток песен правильно заклассифицировали»</span></li>
<li><span><b>Пишет с ИИ:</b> по собственной оценке ~80% — его творчество, ~20% — ИИ. Честно маркирует <span class="hl">«made with AI»</span> на after.today</span></li>
<li><span><b>Бэкграунд-тезис:</b> «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново. <b>Нова честность.</b></span></li>
</ul>
<div class="quote">
<p>ИИ-ассистированное творчество не ново — нова честность.</p>
<cite>Леонид</cite>
</div>
</section>
<!-- 06 · HARDWARE -->
<section class="slide">
<div class="accent"></div><div class="ghost">05</div>
<div class="kicker">Экономика локального инференса</div>
<h2>Спор о железе: <span class="alt">дом против облака</span></h2>
<div class="vs">
<div class="card">
<div class="who">Позиция Леонида</div>
<p><b>DGX Spark за ~1% стоимости дома</b> — нормальная цена за «домашний интеллект». Тренд: коробки станут достоянием домов.</p>
</div>
<div class="vsx">⚡</div>
<div class="card">
<div class="who">Позиция Алекса</div>
<p><b>Облачный always-on</b> — ок для большинства задач. Топовые модели для разработки софта всё равно выигрывают у локальных.</p>
</div>
</div>
<div class="quote">
<p>Сошлись на гибриде: локально — быстрая генерация (эскизы, музыка), облако — тяжёлые продуктовые задачи. У студии в планах собственный inference-сервер — в ~6 раз быстрее.</p>
<cite>Консенсус встречи</cite>
</div>
</section>
<!-- 07 · AI COMPANY -->
<section class="slide">
<div class="accent"></div><div class="ghost">06</div>
<div class="kicker">Обмен тезисами</div>
<h2>Как строить <span class="alt">AI-компанию</span></h2>
<div class="grid g2">
<div class="card">
<div class="tag">Тезис Леонида</div>
<p><b>«Vision — говно, execution — всё»</b> (шутка из EPAM). Из короткого промпта не вырастает сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует дополнительной информации.</p>
</div>
<div class="card">
<div class="tag v">Тезис Алекса</div>
<p>Ядро студии — <b>AI-ядро, постоянно перестраивающее паттерны</b> (64 сейчас) при смене контекста. «Долгосрочно — самый большой актив». Строить бизнес на нейро-инфраструктуре с самого начала → затем автопилот. Незаменим только <b>«CEO-решатель»</b>.</p>
</div>
</div>
<div class="quote">
<p>«Я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре»</p>
<cite>Леонид · метод deep-tech: продавать ценность сразу, не влюбляясь в R&D</cite>
</div>
</section>
<!-- 08 · DEALS -->
<section class="slide">
<div class="accent"></div><div class="ghost">07</div>
<div class="kicker">Итоги встречи</div>
<h2>Договорённости — <span class="alt">что фиксируем</span></h2>
<div class="num"><i>1</i><div>
<h3>Пилот анимации по рассказу Леонида</h3>
<p>Леонид покрывает токены (ориентир до <b>1000</b>), студия делает: аудио → видео, ведёт Влад. Алекс вернётся с <b>конкретным предложением</b>.</p>
</div></div>
<div class="num"><i>2</i><div>
<h3>Формат разговора продолжается</h3>
<p>Леонид открыт к следующим сессиям со студией.</p>
</div></div>
<div class="num"><i>3</i><div>
<h3>Голоса и доли за прошлые инвестиции — вопрос закрыт</h3>
<p>Леонид отказался: «это была шутка», «инвестирую из философских соображений, не создавайте ожиданий». Давления нет.</p>
</div></div>
<div class="num"><i>4</i><div>
<h3>Следующий шаг Леонида</h3>
<p>Посмотрит 35-минутную презентацию студии и вернётся с фидбэком.</p>
</div></div>
</section>
<!-- 09 · NEXT -->
<section class="slide">
<div class="accent"></div><div class="ghost">08</div>
<div class="kicker">Action items</div>
<h2>Что студии делать <span class="alt">дальше</span></h2>
<ul class="list">
<li><span><b>Подготовить оффер по мульт-пилоту</b> — объём, этап аудиоспектакля, смета токенов. Алекс озвучит Леониду предметно.</span></li>
<li><span><b>Подключить партнёрский harness генерации мультиков</b> к задаче Леонида — как первый клиентский кейс.</span></li>
<li><span><b>Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке»</b> — первое демо на живом стороннем человеке, положительный фидбэк.</span></li>
</ul>
<div class="quote">
<p>От живого демо на звонке — к первому платному пилоту по генерации контента.</p>
<cite>Смысл встречи 143</cite>
</div>
</section>
<!-- 10 · END -->
<section class="slide end">
<div class="accent"></div><div class="ghost">42</div>
<div class="kicker">Встреча 143 · 08.10.2026</div>
<div class="big">«Изюминка — <em>в AI-ядре»</em></div>
<p class="sub">Simbiot · Кооператив 42 · агент-визуализатор, LM-роутер, RAG по знаниям студии. Презентация собрана из выжимки разговора — тем же агентом, что рисовал слайды live.</p>
<div class="sig">← → навигация · F — во весь экран · P — печать/PDF</div>
</section>
</div>
<div class="dots" id="dots"></div>
<div class="progress" id="prog"></div>
<div class="hud">
<span class="count" id="count"></span>
<button id="prev" title="Назад (←)">←</button>
<button id="next" title="Вперёд (→)">→</button>
</div>
<div class="hint">← → · свайп · F — фулскрин</div>
<script>
(function(){
var slides=[].slice.call(document.querySelectorAll('.slide'));
var dots=document.getElementById('dots'),prog=document.getElementById('prog'),cnt=document.getElementById('count');
var i=0,n=slides.length;
slides.forEach(function(_,k){var d=document.createElement('i');d.onclick=function(){go(k)};dots.appendChild(d);});
function pad(x){return (x<10?'0':'')+x}
function go(k){
k=Math.max(0,Math.min(n-1,k));
if(k===i&&slides[i].classList.contains('active'))return;
slides.forEach(function(s,j){
s.classList.toggle('active',j===k);
s.classList.toggle('prev',j<k);
});
i=k;
prog.style.width=((i+1)/n*100)+'%';
cnt.innerHTML='<b>'+pad(i+1)+'</b> / '+pad(n);
[].forEach.call(dots.children,function(d,j){d.classList.toggle('on',j===i)});
if(history.replaceState)history.replaceState(null,'','#'+(i+1));
}
document.getElementById('next').onclick=function(){go(i+1)};
document.getElementById('prev').onclick=function(){go(i-1)};
addEventListener('keydown',function(e){
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '){e.preventDefault();go(i+1)}
else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();go(i-1)}
else if(e.key==='Home'){go(0)}
else if(e.key==='End'){go(n-1)}
else if(e.key==='f'||e.key==='F'||e.key==='а'||e.key==='А'){
if(document.fullscreenElement){document.exitFullscreen&&document.exitFullscreen()}
else{document.documentElement.requestFullscreen&&document.documentElement.requestFullscreen()}
}
});
var tx=null,ty=null;
addEventListener('touchstart',function(e){tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
addEventListener('touchend',function(e){
if(tx===null)return;
var dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty;
if(Math.abs(dx)>60&&Math.abs(dx)>Math.abs(dy)){go(dx<0?i+1:i-1)}
tx=null;
},{passive:true});
var h=parseInt((location.hash||'').replace('#',''),10);
go(isNaN(h)?0:h-1);
})();
</script>
</body>
</html>

View file

@ -1,54 +0,0 @@
# Simbiot — питч для видео (Seedance)
## 1. Что говорить на камеру (~30 сек, живым языком)
> Представь: ты заходишь в стартап — и через неделю у тебя работает не один ассистент,
> а целая команда ИИ-агентов: один отвечает клиентам, второй пишет код, третий следит
> за деньгами и метриками.
>
> Это Simbiot. Обычно фаундер собирает зоопарк из агентов и харнесов — а когда уходит
> разработчик, вся эта магия уходит вместе с ним. Simbiot — это общий харнес организации:
> слой поверх Hermes, Claude Code и других. Он координирует флот агентов, фиксирует
> решения компании и копит удачные приёмы как «гены» — и оставляет только те, что
> реально приносят прибыль.
>
> Мы уже гоняем его на семи собственных продуктах. Строй компанию — а не зоопарк ботов.
Хронометраж: ~35 сек. Если нужно короче (15 сек) — оставить первый и последний абзац.
## 2. Промт для Seedance (весь, целиком — скормить как есть)
Seedance генерирует клипы по 5–10 сек, поэтому видео собирается из 3 шотов
( narrator говорит поверх — в кадре речи не нужно, генерим b-roll ).
**Общий стиль (добавлять в конец каждого промта):**
> Dark cinematic tech aesthetic, deep navy-black background (#0a0c12) with violet
> and purple glow accents (#7c6cf5, #8b5cf6), soft volumetric light, shallow depth
> of field, film grain, high detail, 4k, smooth camera motion, no text, no captions,
> no people talking, ambient mood. 16:9.
**Shot 1 (0–10 сек) — «зоопарк в хаосе»:**
> Slow dolly-in through a dark server room: dozens of small glowing robots and holographic
> screens flickering chaotically, tangled cables, scattered disconnected UI panels floating
> in the air, each glowing a different cold color, subtle digital glitches.
**Shot 2 (10–20 сек) — «Simbiot orchestration»:**
> The chaos converges: tangled light streams flow into one glowing violet orb at the center
> of a dark control room, holographic dashboard panels organize around it in a perfect grid,
> elegant lines of data pulse from the orb to the panels like a neural network heartbeat,
> slow orbit camera movement.
**Shot 3 (20–30 сек) — «финал / бренд»:**
> Wide cinematic shot of a modern dark startup office at night, a founder silhouette
> calmly watching a large wall display where agent dashboards grow revenue charts,
> violet light washes the room, camera slowly pulls back through a window into the
> city skyline at dusk.
## 3. Быстрый вариант (один промт, если лень склеивать)
> Cinematic 30-second tech brand film: dark navy-black world with violet glow (#7c6cf5).
> It opens on chaos — dozens of flickering holographic screens and tangled light streams.
> A luminous violet orb emerges and orchestrates everything: panels align into a grid,
> data flows like a heartbeat. Final wide shot: founder silhouette watching revenue
> charts grow on a wall of dashboards in a dark modern office. Slow dolly and orbit
> camera, volumetric light, film grain, no text, 16:9.

Binary file not shown.

View file

@ -1,164 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simbiot UI — Claude Code in your browser. $99.9/mo, seat included</title>
<meta name="description" content="Simbiot UI gives your team a deployed Claude Code web interface: agent mailbox, studio RAG, signed audit. No setup, keys included, $99.9/mo.">
<meta property="og:title" content="Simbiot UI — Claude Code in your browser">
<meta property="og:description" content="Deployed Claude Code web UI with agent mailbox and studio knowledge base. $99.9/mo, claude seat included.">
<meta property="og:url" content="https://ui.cil.by/">
<meta property="og:type" content="website">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{
--paper:#faf8f3; --paper2:#f2efe7; --ink:#1a1a18; --ink2:#55534b; --ink3:#6b685c;
--line:#e2ded2; --line2:#cfcabb; --acc:#5b4be0; --acc2:#3d2fae;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{font-family:'Inter Tight',system-ui,-apple-system,sans-serif;background:var(--paper);color:var(--ink);-webkit-font-smoothing:antialiased;overflow-x:hidden}
.mono{font-family:'JetBrains Mono',ui-monospace,monospace}
.wrap{max-width:1140px;margin:0 auto;padding:0 32px}
a{color:var(--acc)}
:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
nav{position:fixed;top:0;left:0;right:0;z-index:100;background:rgba(250,248,243,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
nav .wrap{display:flex;align-items:center;height:60px}
.logo{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;letter-spacing:-.3px}
.logo-mark{width:14px;height:14px;background:var(--acc)}
nav .cta{margin-left:auto;display:flex;gap:16px;align-items:center}
.btn{display:inline-block;font-weight:600;font-size:15px;padding:11px 22px;border-radius:10px;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}
.btn:active{transform:translateY(1px)}
.btn-acc{background:var(--acc);color:#fff}
.btn-acc:hover{background:var(--acc2);box-shadow:0 6px 20px rgba(91,75,224,.25)}
.btn-ghost{border:1.5px solid var(--line2);color:var(--ink)}
.btn-ghost:hover{border-color:var(--acc);color:var(--acc)}
.hero{padding:150px 0 90px;position:relative}
.kicker{font-family:'JetBrains Mono',monospace;font-size:12.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--acc);font-weight:500;margin-bottom:18px}
h1{font-size:clamp(38px,5.6vw,64px);line-height:1.04;letter-spacing:-.02em;font-weight:700;max-width:900px}
h1 em{font-style:normal;color:var(--acc)}
.sub{margin-top:22px;font-size:19px;line-height:1.55;color:var(--ink2);max-width:640px}
.hero-cta{margin-top:34px;display:flex;gap:14px;flex-wrap:wrap}
.price-pill{display:inline-flex;align-items:baseline;gap:8px;margin-top:26px;padding:10px 18px;border:1px solid var(--line2);border-radius:999px;background:#fff}
.price-pill b{font-size:22px;letter-spacing:-.02em}
.price-pill span{color:var(--ink3);font-size:14px}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:64px}
.card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:24px 22px}
.card h3{font-size:16.5px;font-weight:600;margin-bottom:8px}
.card p{font-size:14.5px;line-height:1.55;color:var(--ink2)}
.card .n{font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--acc);letter-spacing:.1em;margin-bottom:10px;display:block}
section{padding:76px 0}
h2{font-size:clamp(26px,3.4vw,38px);letter-spacing:-.02em;font-weight:700;max-width:760px}
.h2sub{margin-top:14px;color:var(--ink2);font-size:17px;max-width:640px;line-height:1.5}
.feat{display:grid;grid-template-columns:repeat(2,1fr);gap:0;margin-top:44px;border:1px solid var(--line);border-radius:14px;overflow:hidden;background:#fff}
.feat>div{padding:26px 26px;border-bottom:1px solid var(--line)}
.feat>div:nth-child(odd){border-right:1px solid var(--line)}
.feat>div:nth-last-child(-n+2){border-bottom:none}
.feat b{display:block;font-size:16px;margin-bottom:6px}
.feat p{font-size:14.5px;line-height:1.55;color:var(--ink2)}
.steps{counter-reset:s;margin-top:44px;display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
.step{background:#fff;border:1px solid var(--line);border-radius:14px;padding:24px 20px;position:relative}
.step:before{counter-increment:s;content:counter(s,decimal-leading-zero);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--acc);letter-spacing:.1em}
.step b{display:block;margin:10px 0 6px;font-size:15.5px}
.step p{font-size:14px;line-height:1.5;color:var(--ink2)}
.faq{margin-top:44px;max-width:760px}
.faq details{background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px 20px;margin-bottom:10px}
.faq summary{cursor:pointer;font-weight:600;font-size:15.5px;list-style:none;display:flex;justify-content:space-between;align-items:center}
.faq summary:after{content:'+';color:var(--acc);font-size:18px}
.faq details[open] summary:after{content:'–'}
.faq details p{margin-top:10px;font-size:14.5px;line-height:1.6;color:var(--ink2)}
form{margin-top:40px;background:#fff;border:1px solid var(--line);border-radius:16px;padding:30px;max-width:640px}
label{display:block;font-size:13px;font-weight:600;color:var(--ink2);margin:14px 0 6px}
input,textarea{width:100%;font:inherit;font-size:15px;padding:11px 14px;border:1.5px solid var(--line2);border-radius:10px;background:var(--paper);color:var(--ink)}
input:focus,textarea:focus{outline:none;border-color:var(--acc)}
textarea{min-height:84px;resize:vertical}
.submit{margin-top:20px;width:100%;border:none;font-size:16px;cursor:pointer}
.note{margin-top:14px;font-size:13px;color:var(--ink3);line-height:1.5}
footer{border-top:1px solid var(--line);padding:40px 0 60px;color:var(--ink3);font-size:13.5px}
footer .wrap{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
@media(max-width:900px){.grid,.steps{grid-template-columns:1fr 1fr}.feat{grid-template-columns:1fr}.feat>div:nth-child(odd){border-right:none}}
@media(max-width:600px){.grid,.steps{grid-template-columns:1fr}.hero{padding:120px 0 60px}}
</style>
</head>
<body>
<nav><div class="wrap">
<div class="logo"><span class="logo-mark"></span>Simbiot UI</div>
<div class="cta"><a class="btn btn-acc" href="#apply">Apply for access</a></div>
</div></nav>
<header class="hero wrap" id="top">
<div class="kicker">Simbiot UI · by Cooperative 42</div>
<h1>Claude Code in your browser, <em>deployed and maintained</em> for you.</h1>
<p class="sub">No installs, no setup, no API-key juggling. We provision the workspace, the agent mailbox and the model access — you open a tab and build.</p>
<div class="hero-cta">
<a class="btn btn-acc" href="#apply">Request access</a>
<a class="btn btn-ghost" href="#how">How it works</a>
</div>
<div class="price-pill"><b class="mono">$99.9</b><span>/ month · claude seat included · cancel anytime</span></div>
<div class="grid">
<div class="card"><span class="n">ZERO-SETUP</span><h3>Works out of the box</h3><p>A deployed web UI for Claude Code (Simbiot Dashboard for monitoring). Browser only — nothing to install or configure.</p></div>
<div class="card"><span class="n">SEAT-INCLUDED</span><h3>Model access included</h3><p>Claude seat is part of the plan — you don't bring keys, you don't see ours. Fair-use limits keep it fast for everyone.</p></div>
<div class="card"><span class="n">STUDIO-KNOWLEDGE</span><h3>Studio RAG built in</h3><p>Your workspace knows the cooperative's playbook: how to build startups, studio terms and processes — searchable by your agent.</p></div>
</div>
</header>
<section id="how" class="wrap">
<h2>How it works</h2>
<p class="h2sub">We onboard every workspace by hand for the first cohort — that keeps quality high while we scale.</p>
<div class="steps">
<div class="step"><b>Apply</b><p>Tell us about your team and what you're building. Takes a minute.</p></div>
<div class="step"><b>We provision</b><p>Your workspace, agent mailbox and Claude seat are set up by us — usually within a day.</p></div>
<div class="step"><b>You get credentials</b><p>Login, password and URL arrive by email, with a starter guide for your agent.</p></div>
<div class="step"><b>Build</b><p>Open Simbiot UI and work with Claude Code in the browser. Support channel included.</p></div>
</div>
</section>
<section class="wrap" id="features" style="padding-top:0">
<h2>What's inside</h2>
<div class="feat">
<div><b>Personal workspace</b><p>Isolated container per client: your files, your agent, your limits. Others can't see your data, you can't see theirs.</p></div>
<div><b>Agent mailbox</b><p>Every workspace ships with an agent email address on our domain — your Claude can send and receive mail as part of the workflow.</p></div>
<div><b>Model gateway</b><p>All model traffic goes through our gateway with per-client tracking and fair-use limits. Predictable, monitored, no surprise bills.</p></div>
<div><b>Signed &amp; auditable</b><p>Every agent action is signed and auditable — the same audit spine the cooperative runs on every day.</p></div>
</div>
</section>
<section class="wrap" id="faq">
<h2>FAQ</h2>
<div class="faq">
<details><summary>Do I need my own API keys or Claude account?</summary><p>No. The claude seat is included in the $99.9/mo price — model access is provisioned and managed by us.</p></details>
<details><summary>How fast can I start?</summary><p>We onboard the first cohort manually: apply, we confirm, you get credentials — typically within one business day.</p></details>
<details><summary>What's the studio knowledge base?</summary><p>A curated, read-only RAG with the studio's startup-building methodology and terms — available to your agent inside the workspace. Private cooperative data is not included.</p></details>
<details><summary>Is my data isolated?</summary><p>Yes — one isolated container per client with its own volume and resource limits.</p></details>
<details><summary>Can I cancel?</summary><p>Month-to-month subscription, cancel anytime. We'll help you export your files.</p></details>
</div>
</section>
<section class="wrap" id="apply">
<h2>Request access</h2>
<p class="h2sub">First cohort is onboarded personally — leave your details and we'll get back to you.</p>
<form action="https://formsubmit.co/hello@cil.by" method="POST">
<input type="hidden" name="_subject" value="Simbiot UI — access request">
<label for="name">Name</label>
<input id="name" name="name" required placeholder="Jane Doe">
<label for="email">Email</label>
<input id="email" name="email" type="email" required placeholder="jane@team.dev">
<label for="tg">Telegram (optional)</label>
<input id="tg" name="telegram" placeholder="@jane">
<label for="about">What will you build?</label>
<textarea id="about" name="about" placeholder="2–3 sentences about your team and tasks"></textarea>
<button class="btn btn-acc submit" type="submit">Apply — $99.9/mo after confirmation</button>
<p class="note">We reply personally, no auto-billing. Payment link arrives after your workspace is confirmed. Month-to-month, cancel anytime.</p>
<p class="note"><span class="mono">TODO(deploy):</span> form endpoint is a placeholder (hello@cil.by) — wire to the chosen application channel (email agent / TG bot / portal) before deploying. See docs/ROADMAP-SIMBIOT-UI.md.</p>
</form>
</section>
<footer>
<div class="wrap">
<div>© 2026 Cooperative 42 · <a href="https://cil.by">cil.by</a></div>
<div>Simbiot UI · powered by claudecodeui</div>
</div>
</footer>
</body>
</html>

View file

@ -1,164 +0,0 @@
# Simbiot UI — план продукта: продаваемый доступ к CloudCLI UI (claudecodeui)
*t-261008114514 · v0.2 (решения Alex зафиксированы, m-516173892) · 2026-10-08*
> **v0.2 — что изменилось:** получены ответы на все 7 вопросов v0.1 §9. Продукт
> зафиксирован: брендинг Simbiot UI / Simbiot Dashboard, одна цена **$99.9/мес,
> claude seat included**, ключи выдаём свои, наш RAG клиентам не даём — но строим
> **общий студийный RAG** (как строить стартапы + условия студии), подключение
> клиентов на старте **только вручную через Alex**. Пресейл-страница и онбординг
> обновлены под эти решения; юнит-экономика пересчитана под $99.9.
---
## 1. Суть продукта
Клиент покупает **доступ «из коробки» к развёрнутому claudecodeui** (web-UI для Claude Code):
регистрируется → получает аккаунт агента с мэйлом на нашем домене → заходит в личный UI
и работает с Claude-кодом через браузер, без установки чего-либо. Инфраструктура, старт,
авторизация и контекст — на нашей стороне.
**Ценность (фильтр AUM):** recurring-выручка (SaaS-подписка) на уже существующем железе
и моделях → прямой вклад в AUM на дистанции год+; каждая новая продажа не съедает базу.
## 2. Продуктовая рамка (v0.2 — по решениям Alex)
- **Нейминг (решено, A1):** **Simbiot UI** — сам продукт-интерфейс; **Simbiot Dashboard** —
панель управления/мониторинга. Подпись «powered by claudecodeui» в футере — честно про
опенсорс-основу (доверие, не слабость).
- **Цена (решено, A2):** **$99.9/мес, claude seat included** — один тариф, без BYO-вариантов
на старте. Claude-сид входит в цену.
- **API-ключи (решено, A3):** **даём наши** — клиент не приносит свой ключ, работает наш
шлюз (9router) с per-client spend-трекингом и лимитами. Клиент наших ключей не видит.
- **Онбординг (решено, A7):** **только вручную через Alex** на всю первую партию — заявка →
Alex подключает. Автоматический self-service провижининг — не раньше фазы 2 (см. §6).
- **Формат продажи:** подписка месяц-к-месяцу, оплата — существующий стек (Stripe live
обкатан на presale; p2p/счёт — для СНГ).
- **Кто клиент (стартовая гипотеза):** команды 1–5 человек, которым нужен Claude Code,
но лень/страшно разворачивать самим; последователи «перформанс-аудитории» студии.
- **Регистрация:** лендинг на ui.cil.by → форма-заявка (без мгновенной оплаты до ручного
подтверждения) → Alex подключает → оплата → доступы.
## 3. Архитектура и оркестратор
```
ui.cil.by (Caddy, существующий reverse-proxy)
├── веб-приложение Simbiot UI (заявки, биллинг, админка)
├── claudecodeui (UI-движок) ←── контейнеры/юниты на инстансе
├── Claude Code runtime (сервер) ←── claudecodeui дергает его
└── MCP-студии: общий студийный RAG (см. §3a), файловая, 9router-шлюз
```
- **Оркестратор** = сервис-провижинер (один Python-демон): по команде «новый клиент»
поднимает связку `сервер + UI-движок`, создаёт агента, прописывает прокси, пишет CLAUDE.md.
Стек: Docker (уже живёт на хосте) или systemd-юниты + Caddy-автосайты.
- Изоляция клиентов: отдельный контейнер на клиента (vCPU/RAM-лимиты, отдельный том).
- **9router — единственный LLM-шлюз** (A3: ключи наши): даёт маржу на моделях, per-client
spend-трекинг и защиту «клиент не видит наши ключи».
- **Свой RAG кооператива клиентам НЕ даём** (A6). Вместо него — общий студийный RAG (§3a).
### 3a. Общий студийный RAG (решение A6)
- **Что это:** отдельная база знаний общего назначения «как строить стартапы» + публичные
условия студии (типовые договоры, процессы, FAQ) — то, что не жалко показать клиенту.
- **Из чего:** частичный перегон **публичных** знаний кооператива (условия студии, методология
запуска проектов — studio-conditions-2026-10, SALES-PLAN-*, playbook-материалы), плюс
новые материалы, написанные специально для RAG.
- **Как:** отдельный RAG-индекс (не /root/rag кооператива!), своё векторное пространство;
подключение к клиентским инстансам через MCP-сервер read-only. Перегон — отдельной
задачей с ревью Alex (что именно пускать наружу).
- **Экономика:** входит в тариф как дифференциатор («агент знает методологию студии»),
апселл-фаза после первых 5 клиентов.
## 4. Онбординг-флоу (v0.2 — ручной, admin-first)
> Решение A7: подключение клиентов — **только вручную через Alex**, пока не решим иначе.
> Всё, что «авто» в v0.1, переезжает в фазу 2. Ручной флоу — тоже продукт, его надо
> сделать гладким.
1. Клиент оставляет заявку на лендинге (имя, email, телеграм, 2–3 предложения о задачах).
2. Заявка падает в очередь админки (и TG-уведомление Alex).
3. **Alex вручную** подтверждает и подключает: поднимает инстанс, создаёт мэйл агента
(`agent.<client>@cil.by`, Postfix+Dovecot уже подняты), авторизует claude seat.
4. Оркестратор пишет креды в vault (реестр ключей docs/KEYS-REGISTRY.md расширяется), клиенту
уходит письмо: логин, пароль, URL, что дальше (см. §5 CLAUDE.md).
5. Статус-индикатор в админке: заявка → подтверждена → подключается → активна.
**Гейт:** целевая пропускная способность ручного онбординга — 3–5 клиентов/нед до упора;
когда очередь копится > 1 недели — сигнал включать автопровижининг (фаза 2).
## 5. Claude-контекст (CLAUDE.md)
Оркестратор генерирует каждому клиенту `CLAUDE.md` при провижининге и обновляет при изменениях:
- где стартовано (инстанс, ОС, путь к дому проекта, лимиты);
- как ходить в сеть (9router-эндпоинт, ключи из env, а не в файлах);
- что за мэйл у агента и как им пользоваться (IMAP 993 / SMTP 587 cil.by);
- **как пользоваться студийным RAG** (MCP, какие знания там есть — §3a);
- правила безопасности (не трогать чужие тома, лимиты токенов, политика данных);
- как запросить помощь (саппорт-канал).
Обновления — через хук оркестратора (деплой = regenerate CLAUDE.md), чтобы у Claude-кода
всегда актуальная карта окружения.
## 6. План работ (фазы, v0.2)
| Фаза | Что делаем | Оценка |
|---|---|---|
| **0. Спайк (сейчас)** | Развернуть 1 инстанс claudecodeui на хосте за Caddy (ui.cil.by), проверить UX, OAuth Claude, MCP-хуки | 1–2 дня |
| **0b. Пресейл-лендинг ui.cil.by** | Страница Simbiot UI $99.9/мес, форма-заявка → TG-очередь к Alex (без мгновенной оплаты) — artefact: out/simbiot_ui_presale.html | 1 день |
| **1. Каркас продукта** | Админка заявок (очередь, статусы), Stripe-ссылка после ручного подтверждения | 2–3 дня |
| **2. Оркестратор (ручной режим)** | Провижининг по кнопке Alex (не по оплате): контейнер, мэйл, CLAUDE.md, teardown | 3–4 дня |
| **3. Общий студийный RAG** | Отдельный индекс; отбор публичных знаний (список на ревью Alex), MCP read-only | 3–4 дня |
| **4. Пилот** | 3–5 платящих клиентов через ручной онбординг Alex, фидбек, юнит-экономика на реальных числах | 1–2 недели |
| **5. Автопровижининг (отложено)** | Self-service оплата→доступ; включаем при копящейся очереди (гейт §4) | отложено |
**MVP-гейт:** ручной онбординг — не обходной путь, а план-А на пилот. Автоматизация —
только после подтверждённого спроса (фаза 5).
## 7. Юнит-экономика (v0.2, под $99.9/мес)
- **Цена:** $99.9/мес, claude seat included. Один тариф.
- Себестоимость: контейнер ~0 vCPU в простое; токены — через 9router (per-client трекинг
уже есть); домен/железо оплачены.
- **Кейс-1 (умеренное использование, ~$30/мес токенов на клиента):** маржа ≈ $69.9 (70%).
- **Кейс-2 (тяжёлый клиент, ~$80/мес токенов):** маржа ≈ $19.9 (20%) → нужны fair-use
лимиты в оффере.
- 20 клиентов × $99.9 = **$2.0к/мес recurring** при почти нулевой marginal cost.
- Fair-use: зафиксировать лимит токенов/мес в оффере (soft-limit с заголовком «overage
по договорённости»), чтобы кейс-2 не съедал маржу.
- По фильтру AUM: 20 клиентов = +$24к ARR recurring — долгосрочная база, не разовый доход.
## 8. Риски и как закрываем
- **ToS Claude** (перепродажа доступа, A2+A3 усугубляют — сид и ключи наши) → формат
«инфраструктура + сопровождение», лимиты, юр-модель уточнить с Alex до масштабирования
за пределы пилота.
- Нагрузка/лимиты хоста → квоты на клиента, мониторинг (spend-трекинг 9router уже есть).
- Support-нагрузка → CLAUDE.md-контекст + FAQ + агента-саппортёра в чат.
- **Бутылочное горлышко Alex (A7):** ручной онбординг ограничивает рост → гейт §4
(очередь > 1 нед → фаза 5).
- **Утечка знаний через студийный RAG (A6):** перегон только публичных материалов,
список на ревью Alex, read-only MCP.
## 9. Решения Alex (m-516173892, зафиксировано 2026-10-08)
| # | Вопрос v0.1 §9 | Решение Alex | Влияние на план |
|---|---|---|---|
| 1 | Нейминг | **Simbiot UI ok, Simbiot Dashboard ok** | Брендинг зафиксирован (§2) |
| 2 | Цена и тарифы | **$99.9/мес, claude seat included** | Один тариф (§2, §7) |
| 3 | Claude-аккаунты | **наши даём** | 9router — единственный шлюз, ToS-риск выше (§8) |
| 4 | Лимит первых клиентов | **да ок** | 10 ручных → фаза 5 авто |
| 5 | Мэйл-домен | **ок** | ящики на cil.by (§4) |
| 6 | MCP / RAG студии | **наш RAG — нет; общий студийный RAG — да** (как строить стартапы, условия студии, частичный перегон) | §3a, фаза 3 |
| 7 | Кто админ Claude-аккаунтов | **пока только я (Alex)** | Ручной онбординг (§4), гейт фазы 5 |
## 10. Что уже есть и переиспользуем
- Caddy reverse-proxy, Docker, домены cil.by — готовы.
- Postfix+Dovecot (SMTP/IMAP) — мэйл-агенты без нового софта.
- 9router — LLM-шлюз с spend-трекингом.
- Stripe live (обкатан на presale) — биллинг.
- Практики провижининга агентов: mx_provision_agent.py (Matrix), wizard-установщик харнеса.
- Портал кооператива — как референс регистрации/админки (форк или отдельное приложение).
- **out/simbiot_ui_presale.html** — пресейл-страница Simbiot UI $99.9/мес (создана в v0.2).
- **docs/ROADMAP-SIMBIOT-UI.md** — roadmap с ручным онбордингом как ограничением (v0.2).

View file

@ -1,80 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Simbiot — индекс статических файлов</title>
<style>
body{font-family:system-ui,-apple-system,sans-serif;background:#0e1116;color:#e6e9ef;margin:0;padding:40px 20px}
h1{font-size:26px} h2{margin-top:36px;font-size:19px;color:#8ab4ff;border-bottom:1px solid #2a3140;padding-bottom:6px}
table{width:100%;max-width:1000px;border-collapse:collapse;font-size:14px}
td{padding:8px 10px;border-bottom:1px solid #1e2430;vertical-align:top}
a{color:#7ec9ff;text-decoration:none} a:hover{text-decoration:underline}
.size{color:#8a93a5;white-space:nowrap}
.note{color:#aab4c5}
</style>
</head>
<body>
<h1>📁 Индекс статических файлов кооператива 42 / Simbiot</h1>
<p class="note">Все HTML/PNG/PDF-файлы репозитория, сгруппированные по категориям. 31 файл.</p>
<h2>🌍 Лендинги и презентации</h2>
<table>
<tr><td><a href="../docs/simbiot-landing-v2.html">docs/simbiot-landing-v2.html</a></td><td class="note">Лендинг Simbiot v2</td><td class="size">33 КБ</td></tr>
<tr><td><a href="../docs/simbiot-landing-v2-live.html">docs/simbiot-landing-v2-live.html</a></td><td class="note">Лендинг v2 (live-версия)</td><td class="size">30 КБ</td></tr>
<tr><td><a href="../docs/simbiot-landing-dashboard.html">docs/simbiot-landing-dashboard.html</a></td><td class="note">Лендинг + дашборд</td><td class="size">20 КБ</td></tr>
<tr><td><a href="../docs/simbiot_presale_landing.html">docs/simbiot_presale_landing.html</a></td><td class="note">Пресейл-лендинг</td><td class="size">17 КБ</td></tr>
<tr><td><a href="../artifacts/landing-simbiot.html">artifacts/landing-simbiot.html</a></td><td class="note">Лендинг (артefакт)</td><td class="size">11 КБ</td></tr>
<tr><td><a href="../artifacts/landing-simbiot-opencode.html">artifacts/landing-simbiot-opencode.html</a></td><td class="note">Лендинг (OpenCode)</td><td class="size">20 КБ</td></tr>
<tr><td><a href="../simbiot-presentation.html">simbiot-presentation.html</a></td><td class="note">Презентация Simbiot</td><td class="size">22 КБ</td></tr>
<tr><td><a href="../architecture.html">architecture.html</a></td><td class="note">Архитектура системы</td><td class="size">40 КБ</td></tr>
</table>
<h2>👤 Порталы и UI-клиенты</h2>
<table>
<tr><td><a href="../out/cil-by-compact-landing.html">out/cil-by-compact-landing.html</a></td><td class="note">Портал CIL by Compact</td><td class="size">23 КБ</td></tr>
<tr><td><a href="../out/ui-cil-by-compact-landing.html">out/ui-cil-by-compact-landing.html</a></td><td class="note">UI-версия портала CIL</td><td class="size">22 КБ</td></tr>
<tr><td><a href="../out/simbiot_ui_presale.html">out/simbiot_ui_presale.html</a></td><td class="note">Пресейл UI</td><td class="size">12 КБ</td></tr>
<tr><td><a href="../harness/roy.html">harness/roy.html</a></td><td class="note">Портал Roy</td><td class="size">3 КБ</td></tr>
<tr><td><a href="../harness/dashboard.html">harness/dashboard.html</a></td><td class="note">Дашборд харнеса</td><td class="size">40 КБ</td></tr>
</table>
<h2>🗓 Встречи и материалы к переговорам</h2>
<table>
<tr><td><a href="../out/meeting143_leonid_presentation.html">out/meeting143_leonid_presentation.html</a></td><td class="note">Встреча 143 — презентация для Леонида</td><td class="size">13 КБ</td></tr>
<tr><td><a href="../out/meeting143_leonid_slides.html">out/meeting143_leonid_slides.html</a></td><td class="note">Встреча 143 — слайды</td><td class="size">25 КБ</td></tr>
<tr><td><a href="../docs/yulia-call-136-proposal.html">docs/yulia-call-136-proposal.html</a></td><td class="note">Звонок 136 — предложение Юлии</td><td class="size">6 КБ</td></tr>
<tr><td><a href="../docs/yulia-call-136-recap.html">docs/yulia-call-136-recap.html</a></td><td class="note">Звонок 136 — рекап</td><td class="size">6 КБ</td></tr>
<tr><td><a href="../docs/nadezhda-call-recap.html">docs/nadezhda-call-recap.html</a></td><td class="note">Звонок с Надеждой — рекап</td><td class="size">10 КБ</td></tr>
</table>
<h2>📄 Документы и договоры</h2>
<table>
<tr><td><a href="../Studio_Participation_Agreement_2026-09.pdf">Studio_Participation_Agreement_2026-09.pdf</a></td><td class="note">Соглашение об участии в студии (PDF)</td><td class="size">17 КБ</td></tr>
<tr><td><a href="../lab42-conditions.html">lab42-conditions.html</a></td><td class="note">Условия Lab42</td><td class="size">15 КБ</td></tr>
<tr><td><a href="../artifacts/studio-conditions-2026-10.html">artifacts/studio-conditions-2026-10.html</a></td><td class="note">Условия студии 2026-10</td><td class="size">6 КБ</td></tr>
<tr><td><a href="../artifacts/rental-x-simbiot-agreement.html">artifacts/rental-x-simbiot-agreement.html</a></td><td class="note">Соглашение Rental×Simbiot</td><td class="size">16 КБ</td></tr>
<tr><td><a href="../artifacts/rental-x-simbiot-side-letter-15k.html">artifacts/rental-x-simbiot-side-letter-15k.html</a></td><td class="note">Сайд-леттер Rental×Simbiot 15k</td><td class="size">10 КБ</td></tr>
</table>
<h2>🛠 Продуктовые планы и гайды</h2>
<table>
<tr><td><a href="../docs/ai-mascots-product-plan.html">docs/ai-mascots-product-plan.html</a></td><td class="note">Продуктовый план AI-маскотов</td><td class="size">16 КБ</td></tr>
<tr><td><a href="../docs/fpga-neuralnet-guide.html">docs/fpga-neuralnet-guide.html</a></td><td class="note">Гайд: нейросети на FPGA</td><td class="size">9 КБ</td></tr>
<tr><td><a href="../docs/self-diagnostics.html">docs/self-diagnostics.html</a></td><td class="note">Самодиагностика</td><td class="size">8 КБ</td></tr>
</table>
<h2>🎨 Прототипы</h2>
<table>
<tr><td><a href="../artifacts/shkor-beauty-prototype.html">artifacts/shkor-beauty-prototype.html</a></td><td class="note">Прототип Shkor Beauty</td><td class="size">25 КБ</td></tr>
</table>
<h2>🖼 Изображения</h2>
<table>
<tr><td><a href="../artifacts/imagegen-e2e-20261003.png">artifacts/imagegen-e2e-20261003.png</a></td><td class="note">E2E-тест генерации изображений</td><td class="size">977 КБ</td></tr>
<tr><td><a href="../artifacts/dashboard-channels-t-d8112b.png">artifacts/dashboard-channels-t-d8112b.png</a></td><td class="note">Скриншот дашборда (каналы)</td><td class="size">138 КБ</td></tr>
<tr><td><a href="../artifacts/dashboard-channels-t-d8112b-full.png">artifacts/dashboard-channels-t-d8112b-full.png</a></td><td class="note">Скриншот дашборда (полный)</td><td class="size">145 КБ</td></tr>
<tr><td><a href="../out/9router-gemini-test.png">out/9router-gemini-test.png</a></td><td class="note">Тест 9router/Gemini</td><td class="size">1 КБ</td></tr>
</table>
</body>
</html>

View file

@ -1,356 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Simbiot UI — Claude Code in your browser</title>
<meta name="description" content="A deployed web UI for Claude Code with agent mailbox, studio RAG and signed audit. $99.9/mo, claude seat included. No installs, no API keys.">
<meta property="og:title" content="Simbiot UI — Claude Code in your browser">
<meta property="og:description" content="Deployed Claude Code web workspace: mailbox, studio knowledge, signed audit. $99.9/mo, seat included.">
<meta property="og:url" content="https://ui.cil.by/">
<meta property="og:type" content="website">
<style>
/* ── fingerprint (same DNA as cil.by compact): offset rail · ~680px measure · charcoal+lime · serif+mono · machined 4/10 · tinted panels · index motif · one entrance + state wash ── */
:root{
--bg:#0E100C; --panel:#151812; --panel2:#1A1E17;
--line:#272C22; --line2:#333A2C;
--ink:#E9ECE1; --dim:#ADB6A2; --faint:#7E8873;
--acc:#C9F24D; --acc-ink:#111408;
--ok:#7BD88F; --run:#E8C44A; --wait:#8A9382;
--serif:Georgia,'Iowan Old Style','Times New Roman',serif;
--mono:ui-monospace,'SF Mono','Cascadia Mono',Menlo,Consolas,'Liberation Mono',monospace;
--sans:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
a{color:inherit}
:focus-visible{outline:2px solid var(--acc);outline-offset:2px;border-radius:2px}
.wrap{max-width:1060px;margin:0 auto;padding:0 22px}
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
/* ── left rail (desktop) ── */
.layout{display:grid;grid-template-columns:168px minmax(0,1fr);gap:44px;max-width:1060px;margin:0 auto;padding:26px 22px 0}
.rail{position:sticky;top:26px;align-self:start;display:flex;flex-direction:column;gap:22px;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.rail .brand{display:flex;align-items:center;gap:9px;color:var(--ink);text-decoration:none;font-weight:600}
.rail .brand .dot{width:9px;height:9px;border-radius:50%;background:var(--acc);flex:none}
.rail nav{display:flex;flex-direction:column;gap:2px}
.rail nav a{color:var(--faint);text-decoration:none;padding:9px 10px 9px 0;min-height:36px;display:flex;align-items:center;border-bottom:1px solid var(--line)}
.rail nav a:hover{color:var(--ink)}
.rail .railnote{color:var(--faint);line-height:1.9;border-top:1px solid var(--line);padding-top:14px}
.rail .railnote b{color:var(--dim);font-weight:500}
.topbar{display:none}
/* ── buttons: compact scale ── */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:28px;padding:0 14px;border-radius:4px;border:1px solid var(--line2);background:var(--panel2);color:var(--ink);font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;text-decoration:none;cursor:pointer;min-height:40px}
.btn.primary{background:var(--acc);border-color:var(--acc);color:var(--acc-ink);font-weight:600}
.btn:focus-visible{outline-offset:1px}
@media(hover:hover) and (pointer:fine){.btn:hover{border-color:var(--acc)} .btn.primary:hover{filter:brightness(1.06)}}
.btn:active{transform:scale(.96)}
/* ── hero ── */
.hero{padding:30px 0 26px;border-bottom:1px solid var(--line)}
.kicker{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:18px}
.kicker .dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:8px}
h1{font-family:var(--serif);font-weight:400;font-size:clamp(36px,5.6vw,58px);line-height:1.05;letter-spacing:-.015em;max-width:16ch}
h1 em{font-style:italic;color:var(--acc)}
.lede{color:var(--dim);max-width:52ch;margin:18px 0 22px;font-size:15.5px}
.cta-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.cta-note{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--faint);text-transform:uppercase}
/* ── signature artifact: live workspace console ── */
.console{margin-top:30px;background:var(--panel);border:1px solid var(--line2);border-radius:10px;overflow:hidden}
.console .bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.console .bar .ticks{display:flex;gap:5px}
.console .bar .ticks i{width:8px;height:8px;border-radius:2px;background:var(--line2)}
.console .bar .live{margin-left:auto;display:flex;align-items:center;gap:7px;color:var(--dim)}
.console .bar .live i{width:7px;height:7px;border-radius:50%;background:var(--ok);animation:pulse 2.4s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
.metrics{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--line)}
.metric{padding:12px 14px;border-right:1px solid var(--line)}
.metric:last-child{border-right:none}
.metric .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:19px;color:var(--ink)}
.metric .v .u{color:var(--faint);font-size:12px}
.metric .k{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-top:3px}
.ladder{display:flex;flex-direction:column}
.trow{display:grid;grid-template-columns:44px minmax(0,1fr) 96px 96px;gap:12px;align-items:center;min-height:44px;padding:8px 14px;border-bottom:1px solid var(--line);font-size:13.5px;color:var(--dim);cursor:pointer;text-align:left;background:transparent;border-left:none;border-right:none;border-top:none;width:100%;font-family:var(--sans);transition:background-color .25s,color .25s}
.trow:last-child{border-bottom:none}
.trow:hover{background:var(--panel2);color:var(--ink)}
.trow .sz{font-family:var(--mono);font-size:11px;color:var(--faint)}
.trow .name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.trow .name b{color:var(--ink);font-weight:600}
.trow .st{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--wait);display:flex;align-items:center;gap:7px}
.trow .st i{width:7px;height:7px;border-radius:50%;background:var(--wait);flex:none}
.trow[data-st="running"] .st{color:var(--run)} .trow[data-st="running"] .st i{background:var(--run)}
.trow[data-st="done"] .st{color:var(--ok)} .trow[data-st="done"] .st i{background:var(--ok)}
.trow .who{font-family:var(--mono);font-size:11px;color:var(--faint);text-align:right}
.console .foot{padding:9px 14px;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);border-top:1px solid var(--line)}
/* ── sections ── */
section{padding:38px 0 6px}
.shead{display:flex;align-items:baseline;gap:14px;margin-bottom:20px}
.sidx{font-family:var(--mono);font-size:11px;color:var(--acc);letter-spacing:.1em}
.shead h2{font-family:var(--serif);font-weight:400;font-size:clamp(22px,3vw,30px);letter-spacing:-.01em}
.snote{margin-left:auto;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.lead{color:var(--dim);max-width:56ch;margin-bottom:22px}
.ledger{border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}
.lrow{display:grid;grid-template-columns:52px 160px minmax(0,1fr) 170px;gap:16px;align-items:center;padding:15px 18px;border-bottom:1px solid var(--line);font-size:13.5px;color:var(--dim)}
.lrow:last-child{border-bottom:none}
.lrow .idx{font-family:var(--mono);font-size:11px;color:var(--faint)}
.lrow .nm{font-size:14.5px;color:var(--ink);font-weight:600}
.lrow .nm span{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);font-weight:400;margin-top:3px}
.lrow .tags{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--faint);text-align:right}
/* ── steps ── */
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.step{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
.step .n{font-family:var(--mono);font-size:11px;color:var(--acc);margin-bottom:9px;display:block}
.step b{font-size:14.5px;display:block;margin-bottom:6px}
.step p{font-size:13px;color:var(--dim);line-height:1.55}
/* ── pricing ── */
.plan{border:1px solid var(--line2);border-radius:12px;background:var(--panel);padding:26px;max-width:560px}
.plan .p{font-family:var(--serif);font-size:38px;letter-spacing:-.02em}
.plan .p span{font-family:var(--mono);font-size:12px;color:var(--faint);letter-spacing:.06em}
.plan ul{list-style:none;margin:18px 0 22px;display:grid;gap:9px}
.plan li{font-size:13.5px;color:var(--dim);padding-left:22px;position:relative}
.plan li::before{content:"✓";position:absolute;left:0;color:var(--acc);font-family:var(--mono);font-size:12px}
/* ── faq ── */
details{border-bottom:1px solid var(--line);padding:14px 0}
details summary{cursor:pointer;font-size:15px;font-weight:600;list-style:none;display:flex;justify-content:space-between;gap:14px}
details summary::after{content:"+";font-family:var(--mono);color:var(--faint)}
details[open] summary::after{content:"–";color:var(--acc)}
details p{margin-top:10px;color:var(--dim);font-size:13.5px;max-width:64ch}
/* ── form ── */
.form{border:1px solid var(--line2);border-radius:12px;background:var(--panel);padding:26px;max-width:560px}
.form label{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin:14px 0 6px}
.form input,.form textarea{width:100%;background:var(--panel2);border:1px solid var(--line2);border-radius:6px;color:var(--ink);font-family:var(--sans);font-size:14px;padding:11px 12px;min-height:44px}
.form textarea{min-height:88px;resize:vertical}
.form input:focus,.form textarea:focus{outline:none;border-color:var(--acc)}
.form .btn{margin-top:20px;width:100%}
.form .fine{margin-top:12px;font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);text-align:center}
footer{margin-top:44px;border-top:1px solid var(--line);padding:22px 0 34px;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap}
/* ── entrance ── */
@media(prefers-reduced-motion:no-preference){
.in{opacity:0;transform:translateY(10px);animation:rise .7s cubic-bezier(.2,.7,.2,1) forwards}
.d1{animation-delay:.08s}.d2{animation-delay:.16s}.d3{animation-delay:.24s}
@keyframes rise{to{opacity:1;transform:none}}
}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
/* ── mobile ── */
@media(max-width:820px){
.layout{grid-template-columns:1fr;gap:0;padding-top:0}
.rail{display:none}
.topbar{display:flex;align-items:center;gap:10px;position:sticky;top:0;z-index:50;background:rgba(14,16,12,.94);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:12px 22px;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.topbar .dot{width:9px;height:9px;border-radius:50%;background:var(--acc)}
.topbar a{margin-left:auto}
.lrow{grid-template-columns:40px minmax(0,1fr);grid-template-rows:auto auto}
.lrow .ds{grid-column:2}
.lrow .tags{grid-column:2;text-align:left}
.steps{grid-template-columns:1fr}
.metrics{grid-template-columns:1fr}
.metric{border-right:none;border-bottom:1px solid var(--line)}
.trow{grid-template-columns:minmax(0,1fr) 90px}
.trow .sz,.trow .who{display:none}
}
</style>
</head>
<body>
<div class="topbar"><span class="dot"></span>Simbiot UI <a class="btn primary" href="#apply" style="margin-left:auto">Apply</a></div>
<div class="layout">
<aside class="rail">
<a class="brand" href="https://ui.cil.by/"><span class="dot"></span>Simbiot&nbsp;UI</a>
<nav>
<a href="#included">included</a>
<a href="#how">how it works</a>
<a href="#inside">what's inside</a>
<a href="#pricing">pricing</a>
<a href="#faq">faq</a>
<a href="#apply">apply</a>
</nav>
<div class="railnote">
<b>Coop 42</b> product<br>
claudecodeui<br>
studio RAG · audit<br><br>
<a class="btn" style="margin-top:10px" href="https://ui.cil.by/">Sign in</a>
</div>
</aside>
<main>
<!-- 00 · hero + live workspace console -->
<div class="hero">
<p class="kicker in"><span class="dot"></span>Simbiot UI · first cohort open</p>
<h1 class="in d1">Claude Code in your browser.<br><em>Deployed for you.</em></h1>
<p class="lede in d2">A web workspace for Claude Code: agent mailbox, studio knowledge base, signed audit. No installs, no API keys — open a tab and build.</p>
<div class="cta-row in d3">
<a class="btn primary" href="#apply">Request access — $99.9/mo</a>
<a class="btn" href="#how">How it works</a>
<span class="cta-note">claude seat included · cancel anytime</span>
</div>
<div class="console in d3" aria-label="Live workspace demo">
<div class="bar">
<span class="ticks" aria-hidden="true"><i></i><i></i><i></i></span>
<span>your workspace · agent feed</span>
<span class="live"><i></i>workspace live</span>
</div>
<div class="metrics">
<div class="metric"><div class="v" id="mDone">4<span class="u"> done</span></div><div class="k">actions this hour</div></div>
<div class="metric"><div class="v">1<span class="u">-day</span></div><div class="k">setup to first login</div></div>
<div class="metric"><div class="v">100<span class="u">%</span></div><div class="k">actions signed</div></div>
</div>
<div class="ladder" id="ladder"></div>
<div class="foot">mailbox inbox 14:02 · <b>per-client gateway limits</b> · click a row to re-run</div>
</div>
</div>
<!-- 01 · included -->
<section id="included">
<div class="shead"><span class="sidx">01</span><h2>Works out of the box.</h2><span class="snote">zero setup</span></div>
<div class="ledger">
<div class="lrow">
<span class="idx">/01</span>
<div class="nm">Browser only<span>deployed web UI</span></div>
<div class="ds">A deployed web interface for Claude Code, plus Simbiot Dashboard for monitoring. Nothing to install or configure.</div>
<div class="tags">web · dashboard</div>
</div>
<div class="lrow">
<span class="idx">/02</span>
<div class="nm">Seat included<span>model access</span></div>
<div class="ds">The Claude seat is part of the plan — you don't bring keys, you don't see ours. Fair-use limits keep it fast for everyone.</div>
<div class="tags">no keys · fair-use</div>
</div>
<div class="lrow">
<span class="idx">/03</span>
<div class="nm">Studio RAG<span>knowledge built in</span></div>
<div class="ds">Your workspace knows the cooperative's playbook: how to build startups, studio terms and processes — searchable by your agent.</div>
<div class="tags">rag · read-only</div>
</div>
</div>
</section>
<!-- 02 · how -->
<section id="how">
<div class="shead"><span class="sidx">02</span><h2>Onboarded by hand.</h2><span class="snote">first cohort</span></div>
<p class="lead">We onboard every workspace manually for the first cohort — that keeps quality high while we scale.</p>
<div class="steps">
<div class="step"><span class="n">01</span><b>Apply</b><p>Tell us about your team and what you're building. Takes a minute.</p></div>
<div class="step"><span class="n">02</span><b>We provision</b><p>Workspace, agent mailbox and Claude seat are set up by us — usually within a day.</p></div>
<div class="step"><span class="n">03</span><b>Credentials arrive</b><p>Login, password and URL by email, with a starter guide for your agent.</p></div>
<div class="step"><span class="n">04</span><b>Build</b><p>Open Simbiot UI and work with Claude Code in the browser. Support channel included.</p></div>
</div>
</section>
<!-- 03 · inside -->
<section id="inside">
<div class="shead"><span class="sidx">03</span><h2>What's inside.</h2><span class="snote">per workspace</span></div>
<div class="ledger">
<div class="lrow">
<span class="idx">/01</span>
<div class="nm">Personal workspace<span>isolated container</span></div>
<div class="ds">One isolated container per client: your files, your agent, your limits. Others can't see your data, you can't see theirs.</div>
<div class="tags">isolation · volumes</div>
</div>
<div class="lrow">
<span class="idx">/02</span>
<div class="nm">Agent mailbox<span>email on our domain</span></div>
<div class="ds">Every workspace ships with an agent email address — your Claude can send and receive mail as part of the workflow.</div>
<div class="tags">smtp · workflows</div>
</div>
<div class="lrow">
<span class="idx">/03</span>
<div class="nm">Model gateway<span>tracked &amp; limited</span></div>
<div class="ds">All model traffic goes through our gateway with per-client tracking and fair-use limits. Predictable, no surprise bills.</div>
<div class="tags">gateway · limits</div>
</div>
<div class="lrow">
<span class="idx">/04</span>
<div class="nm">Signed &amp; auditable<span>audit spine</span></div>
<div class="ds">Every agent action is signed and auditable — the same audit spine the cooperative runs on every day.</div>
<div class="tags">signed · audit</div>
</div>
</div>
</section>
<!-- 04 · pricing -->
<section id="pricing">
<div class="shead"><span class="sidx">04</span><h2>One plan. Everything included.</h2><span class="snote">month-to-month</span></div>
<div class="plan">
<div class="p">$99.9 <span>/ month · flat</span></div>
<ul>
<li>Deployed Claude Code web workspace</li>
<li>Claude seat &amp; model access included</li>
<li>Agent mailbox on our domain</li>
<li>Studio knowledge base (RAG)</li>
<li>Isolated container per workspace</li>
<li>Signed, auditable agent actions</li>
<li>Support channel · cancel anytime</li>
</ul>
<a class="btn primary" href="#apply">Apply for access</a>
</div>
</section>
<!-- 05 · faq -->
<section id="faq">
<div class="shead"><span class="sidx">05</span><h2>FAQ.</h2></div>
<details open><summary>Do I need my own API keys or Claude account?</summary><p>No. The Claude seat is included in the $99.9/mo price — model access is provisioned and managed by us.</p></details>
<details><summary>How fast can I start?</summary><p>We onboard the first cohort manually: apply, we confirm, you get credentials — typically within one business day.</p></details>
<details><summary>What's the studio knowledge base?</summary><p>A curated, read-only RAG with the studio's startup-building methodology and terms — available to your agent inside the workspace. Private cooperative data is not included.</p></details>
<details><summary>Is my data isolated?</summary><p>Yes — one isolated container per client with its own volume and resource limits.</p></details>
<details><summary>Can I cancel?</summary><p>Month-to-month subscription, cancel anytime. We'll help you export your files.</p></details>
</section>
<!-- 06 · apply -->
<section id="apply">
<div class="shead"><span class="sidx">06</span><h2>Request access.</h2><span class="snote">personal reply</span></div>
<p class="lead">First cohort is onboarded personally — leave your details and we'll get back to you.</p>
<form class="form" action="mailto:hello@cil.by" method="post" enctype="text/plain">
<label for="f-name">Name</label><input id="f-name" name="name" required autocomplete="name">
<label for="f-email">Email</label><input id="f-email" name="email" type="email" required autocomplete="email">
<label for="f-tg">Telegram (optional)</label><input id="f-tg" name="telegram">
<label for="f-what">What will you build?</label><textarea id="f-what" name="building"></textarea>
<button class="btn primary" type="submit">Apply — $99.9/mo after confirmation</button>
<p class="fine">we reply personally · no auto-billing · cancel anytime</p>
</form>
</section>
<footer>
<span>© 2026 Cooperative 42 · cil.by</span>
<span>Simbiot UI · powered by claudecodeui</span>
</footer>
</main>
</div>
<script>
/* live agent feed — signature artifact */
const tasks=[
{sz:"S",name:"<b>fix</b> header padding on landing",st:"done",who:"cody"},
{sz:"M",name:"<b>draft</b> weekly digest for client",st:"done",who:"cody"},
{sz:"M",name:"<b>reply</b> to inbound via agent mailbox",st:"running",who:"cody"},
{sz:"L",name:"<b>build</b> onboarding guide pages",st:"queued",who:"cody"},
];
const ladder=document.getElementById('ladder');
function render(){
ladder.innerHTML=tasks.map((t,i)=>`<button class="trow" data-st="${t.st}" aria-label="Task ${i+1}: ${t.name.replace(/<[^>]+>/g,'')} — ${t.st}">
<span class="sz">${t.sz}</span><span class="name">${t.name}</span>
<span class="st"><i></i>${t.st}</span><span class="who">${t.who}</span></button>`).join('');
}
render();
ladder.addEventListener('click',e=>{
const row=e.target.closest('.trow'); if(!row)return;
const t=tasks[[...ladder.children].indexOf(row)];
if(t.st==='queued'){t.st='running';}
else if(t.st==='running'){t.st='done';const d=tasks.filter(x=>x.st==='done').length;document.getElementById('mDone').innerHTML=d+'<span class="u"> done</span>';}
else{t.st='queued';}
render();
});
</script>
</body>
</html>

View file

@ -1,129 +0,0 @@
#!/usr/bin/env python3
"""Ватчер Alipay на live-счёте Stripe (эскалация e-261005142309-390896).
Контекст: Alex включил тумблер Alipay в дашборде (preference=on), но
capability ещё not available — счёт ждёт аппрува Stripe («говорит что нужен
ещё аппрувал, ждем», m-516173789). Включить через API нельзя (rk_live
restricted, available=false).
Этот скрипт — детерминированный поллинг без LLM (systemd-таймер, 5 мин):
- пока alipay.available=false → тихо выходим (state=waiting);
- как только Stripe активирует capability → создаёт Checkout Session
$100 one_time (price_1UNCYGBT53G7FGSQya04h28r) с card+alipay и
идемпотентно (cli.py tg-notify -k) отправляет ссылку в чат;
- после успешной доставки пишет done-маркер и больше не работает.
Ключ читается из /root/coop/.env (STRIPE_LIVE_KEY, реестр KEYS-REGISTRY).
Состояние: /var/lib/simbiot-harness/alipay-watch.state.
Выход: 0 = ждём/отработал, 3 = ошибка конфигурации (для журнала).
"""
import json
import os
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
STATE = "/var/lib/simbiot-harness/alipay-watch.state"
ENV_FILE = "/root/coop/.env"
PRICE = "price_1UNCYGBT53G7FGSQya04h28r" # $100 USD one_time, live
NOTIFY_KEY = "alipay-live-100-link" # идемпотентность доставки (t-260930083937)
HARNESS = "/root/simbiot-harness"
VENV_PY = f"{HARNESS}/venv/bin/python"
def _stripe_key() -> str | None:
try:
with open(ENV_FILE) as f:
for line in f:
line = line.strip()
if line.startswith("STRIPE_LIVE_KEY="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
except OSError:
pass
return os.environ.get("STRIPE_LIVE_KEY")
def _call(path: str, method: str = "GET", data: dict | None = None) -> dict:
key = _stripe_key()
if not key:
return {"_error": "no STRIPE_LIVE_KEY"}
body = urllib.parse.urlencode(data).encode() if data is not None else None
req = urllib.request.Request(
"https://api.stripe.com/v1/" + path,
headers={"Authorization": f"Bearer {key}"},
method=method,
data=body,
)
try:
with urllib.request.urlopen(req, timeout=25) as r:
return json.loads(r.read())
except urllib.error.HTTPError as e:
return {"_error": e.code, "body": e.read().decode()[:400]}
def _read_state() -> str:
try:
with open(STATE) as f:
return f.read().strip()
except OSError:
return ""
def _write_state(s: str) -> None:
os.makedirs(os.path.dirname(STATE), exist_ok=True)
with open(STATE, "w") as f:
f.write(s)
def main() -> int:
if _read_state() == "done":
return 0 # ссылка создана и доставлена — ватчер отработал
cfgs = _call("payment_method_configurations?limit=100")
for c in cfgs.get("data", []):
alipay = c.get("alipay") or {}
if alipay.get("available"):
# capability активна — создаём линк card+alipay
sess = _call("checkout/sessions", "POST", {
"mode": "payment",
"line_items[0][price]": PRICE,
"line_items[0][quantity]": "1",
"success_url": "https://cil.by/thanks",
"cancel_url": "https://cil.by/",
"payment_method_types[0]": "card",
"payment_method_types[1]": "alipay",
})
url = sess.get("url")
if not url:
# доступна, но сессия не собирается — оставить ждущим,
# причина в журнале таймера; следующий прогон повторит
_write_state("available-but-failed")
print("alipay available, session failed:",
sess.get("_error"), sess.get("body", "")[:200])
return 0
text = (f"Alipay активирован на live-счёте Stripe. "
f"Боевая ссылка $100 (card + Alipay): {url}")
r = subprocess.run(
[VENV_PY, f"{HARNESS}/cli.py", "tg-notify", text,
"-k", NOTIFY_KEY],
capture_output=True, text=True, timeout=60,
)
out = (r.stdout or "").strip()
print("notify:", out)
if out.startswith("OK") or "SKIP" in out:
_write_state("done")
return 0
_write_state("notify-failed")
return 0
print(f"waiting: alipay preference={alipay.get('display_preference', {}).get('value')} "
f"available={alipay.get('available')} (cfg {c.get('id')})")
_write_state("waiting")
return 0
print("no live payment_method_configuration found:", cfgs.get("_error", "?"))
return 3
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,289 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# deploy-claude-code.sh — развернуть Claude Code + ttyd web-TUI на любом сервере
# ============================================================================
# Воспроизводит боевую развёртку Лунара (t-261005155643: 2.28.121.212, user
# lunar, Claude Code 2.1.289 + ttyd 1.7.7, systemd-юнит ttyd-claude.service):
# 1) node/npm (если нет: apt → nodesource 22.x, иначе статический tarball)
# 2) npm i -g @anthropic-ai/claude-code
# 3) ttyd (бинарник с github-releases под архитектуру)
# 4) OS-юзер (если нет), systemd-юнит ttyd-claude.service
# 5) daemon-reload + enable --now + финальная проверка версий и здоровья
#
# Использование (root или sudo):
# bash scripts/deploy-claude-code.sh # дефолты: :7681, юзер claude, токен сгенерится
# bash scripts/deploy-claude-code.sh --port 7681 --user lunar --token MySecret
# bash scripts/deploy-claude-code.sh --dry-run --port 9000 --user bob --token x
# bash scripts/deploy-claude-code.sh --check # только здоровье, без установки
#
# Опции:
# --port N порт ttyd (по умолчанию 7681, как на Лунаре)
# --user NAME OS-юзер сервиса И логин basic-auth (по умолчанию claude)
# --token SECRET пароль basic-auth (по умолчанию: openssl rand -hex 12)
# --service NAME имя systemd-юнита (по умолчанию ttyd-claude)
# --ttyd-version V версия ttyd (по умолчанию 1.7.7)
# --node-version V версия node для tarball-фолбэка (по умолчанию 22.14.0)
# --claude-version V пин версии claude-code (по умолчанию latest)
# --interface IF на чём слушать (по умолчанию 0.0.0.0)
# --env K=V доп. Environment= в юнит (повторяемый), например
# --env ANTHROPIC_BASE_URL=https://api.z.ai
# --readonly терминал только для чтения (без -W)
# --dry-run показать план, ничего не делать (root не нужен)
# --check только проверка здоровья (версии + HTTP + сервис)
# -h | --help эта справка
#
# Безопасность: веб-терминнал закрыт basic-auth (-c user:token) — как на Лунаре
# (логин lunar / Zm9yZXZ0b25lClaude01). Сервис работает от непривилегированного
# юзера (User=), никакого root-shell в браузере.
# ============================================================================
set -euo pipefail
PORT="7681"
USER_NAME="claude"
TOKEN=""
SERVICE="ttyd-claude"
TTYD_VERSION="1.7.7"
NODE_VERSION="22.14.0"
CLAUDE_VERSION=""
INTERFACE="0.0.0.0"
READONLY=0
DRY_RUN=0
CHECK_ONLY=0
UNIT_ENV=()
log() { printf '[deploy-claude-code] %s\n' "$*"; }
die() { printf '[deploy-claude-code] ОШИБКА: %s\n' "$*" >&2; exit 2; }
have() { command -v "$1" >/dev/null 2>&1; }
usage() { sed -n '2,37p' "$0" | sed 's/^# \{0,1\}//'; }
# ── парсинг аргументов ──────────────────────────────────────────────────────
while [ $# -gt 0 ]; do
case "$1" in
--port) PORT="${2:-}"; shift 2 ;;
--user) USER_NAME="${2:-}"; shift 2 ;;
--token) TOKEN="${2:-}"; shift 2 ;;
--service) SERVICE="${2:-}"; shift 2 ;;
--ttyd-version) TTYD_VERSION="${2:-}"; shift 2 ;;
--node-version) NODE_VERSION="${2:-}"; shift 2 ;;
--claude-version) CLAUDE_VERSION="${2:-}"; shift 2 ;;
--interface) INTERFACE="${2:-}"; shift 2 ;;
--env) UNIT_ENV+=("${2:-}"); shift 2 ;;
--readonly) READONLY=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--check) CHECK_ONLY=1; shift ;;
-h|--help) usage; exit 0 ;;
--port=*|--user=*|--token=*|--service=*|--ttyd-version=*|--node-version=*|\
--claude-version=*|--interface=*|--env=*)
# форма --opt=value
opt="${1%%=*}"; val="${1#*=}"
case "$opt" in
--port) PORT="$val" ;; --user) USER_NAME="$val" ;;
--token) TOKEN="$val" ;; --service) SERVICE="$val" ;;
--ttyd-version) TTYD_VERSION="$val" ;; --node-version) NODE_VERSION="$val" ;;
--claude-version) CLAUDE_VERSION="$val" ;; --interface) INTERFACE="$val" ;;
--env) UNIT_ENV+=("$val") ;;
esac; shift ;;
*) usage >&2; die "неизвестная опция: $1" ;;
esac
done
# ── валидация ────────────────────────────────────────────────────────────────
[[ "$PORT" =~ ^[0-9]+$ ]] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] \
|| die "--port должен быть числом 1..65535 (получили: '$PORT')"
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|| die "--user должен быть валидным unix-логином (a-z, 0-9, _, -; получили: '$USER_NAME')"
[[ "$SERVICE" =~ ^[a-zA-Z0-9_.@-]+$ ]] || die "--service: некорректное имя юнита '$SERVICE'"
case "$TOKEN" in
*:*) die "--token не может содержать ':' (ломает -c user:token)" ;;
*' '*) die "--token не может содержать пробелы" ;;
esac
if [ "${#UNIT_ENV[@]}" -gt 0 ]; then
for e in "${UNIT_ENV[@]}"; do
[[ "$e" =~ ^[A-Za-z_][A-Za-z0-9_]*=.+$ ]] || die "--env ожидает KEY=VALUE (получили: '$e')"
done
fi
# токен: явный или сгенерированный (в dry-run без openssl — плейсхолдер)
if [ -z "$TOKEN" ]; then
if have openssl; then TOKEN="$(openssl rand -hex 12)"; else TOKEN="CHANGE-ME-$(date +%s)"; fi
fi
RUN() { # RUN 'описание' cmd args... — единая точка для dry-run
local desc="$1"; shift
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry] %s\n -> %s\n' "$desc" "$*"
else
log "$desc"
"$@"
fi
}
# ── --check: только здоровье ─────────────────────────────────────────────────
health_check() {
local fail=0
if have claude; then
printf 'claude : %s\n' "$(claude --version 2>&1 | head -1)"
else
printf 'claude : НЕ УСТАНОВЛЕН\n'; fail=1
fi
if have ttyd; then
printf 'ttyd : %s\n' "$(ttyd --version 2>&1 | head -1)"
else
printf 'ttyd : НЕ УСТАНОВЛЕН\n'; fail=1
fi
if [ "$(id -u)" -eq 0 ] && have systemctl; then
if systemctl is-active --quiet "$SERVICE.service"; then
printf 'service: %s active\n' "$SERVICE"
else
printf 'service: %s НЕ активен\n' "$SERVICE"; fail=1
fi
fi
local code
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" || true)"
[ -n "$code" ] || code=000
# 200 = отвечает; 401 = basic-auth требует логин — тоже «жив»
if [ "$code" = "200" ] || [ "$code" = "401" ]; then
printf 'http : :%s -> %s (жив)\n' "$PORT" "$code"
else
printf 'http : :%s -> %s (НЕ отвечает)\n' "$PORT" "$code"; fail=1
fi
[ "$fail" -eq 0 ] && log 'здоровье: OK' || log 'здоровье: есть проблемы'
return "$fail"
}
if [ "$CHECK_ONLY" -eq 1 ]; then
health_check || exit 1
exit 0
fi
# ── предварительные проверки окружения ───────────────────────────────────────
[ "$(id -u)" -eq 0 ] || [ "$DRY_RUN" -eq 1 ] || die "запусти от root (sudo bash $0 ...)"
have curl || die "нужен curl (apt-get install -y curl)"
if [ "$DRY_RUN" -eq 0 ]; then
have systemctl || die "нужен systemd (systemctl не найден)"
fi
log "план: порт=$PORT user=$USER_NAME service=$SERVICE ttyd=$TTYD_VERSION readonly=$READONLY"
[ "$DRY_RUN" -eq 1 ] && log 'режим DRY-RUN: ничего не устанавливается'
# ── 1) node/npm ──────────────────────────────────────────────────────────────
node_major() { node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1; }
node_ok() {
have node && have npm && [ "$(node_major)" -ge 18 ] 2>/dev/null
}
if node_ok; then
log "node $(node -v) / npm $(npm -v) уже есть — пропускаю"
else
if have apt-get; then
# путь Лунара: nodesource setup_22.x + apt
RUN 'устанавливаю node 22 (nodesource + apt)' \
bash -c 'set -e; curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/n.sh && bash /tmp/n.sh >/dev/null 2>&1; apt-get install -y nodejs >/dev/null 2>&1 || apt-get install -y nodejs'
else
# фолбэк: статический tarball с nodejs.org (любой дистрибутив)
NARCH=''; case "$(uname -m)" in
x86_64) NARCH='x64' ;;
aarch64) NARCH='arm64' ;;
armv7l) NARCH='armv7l' ;;
*) die "неподдерживаемая архитектура для node-tarball: $(uname -m)" ;;
esac
RUN "устанавливаю node $NODE_VERSION (tarball linux-$NARCH в /usr/local)" \
bash -c "set -e; curl -fsSL -o /tmp/node.tar.xz https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$NARCH.tar.xz && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1"
fi
[ "$DRY_RUN" -eq 1 ] || { node_ok || die 'node не установился (см. вывод выше)'; }
fi
# ── 2) Claude Code ───────────────────────────────────────────────────────────
if have claude && [ -z "$CLAUDE_VERSION" ] && [ "$DRY_RUN" -eq 0 ]; then
log "claude уже установлен: $(claude --version 2>&1 | head -1) — обновляю до latest"
fi
if [ -n "$CLAUDE_VERSION" ]; then
RUN "npm i -g @anthropic-ai/claude-code@$CLAUDE_VERSION" npm install -g "@anthropic-ai/claude-code@$CLAUDE_VERSION"
else
RUN 'npm i -g @anthropic-ai/claude-code (latest)' npm install -g @anthropic-ai/claude-code
fi
if [ "$DRY_RUN" -eq 0 ]; then
have claude || ln -sf "$(npm prefix -g)/bin/claude" /usr/local/bin/claude 2>/dev/null || true
have claude || die 'claude не найден после npm i'
log "claude: $(claude --version 2>&1 | head -1)"
fi
# ── 3) ttyd ──────────────────────────────────────────────────────────────────
TARCH=''; case "$(uname -m)" in
x86_64) TARCH='x86_64' ;;
aarch64) TARCH='aarch64' ;;
armv7l|armv6l) TARCH='arm' ;;
i686) TARCH='i686' ;;
*) die "неподдерживаемая архитектура для ttyd: $(uname -m)" ;;
esac
if have ttyd && [ "$DRY_RUN" -eq 0 ]; then
log "ttyd уже есть: $(ttyd --version 2>&1 | head -1) — пропускаю скачивание"
else
RUN "ставлю ttyd $TTYD_VERSION ($TARCH) в /usr/local/bin" \
bash -c "set -e; curl -fsSL -o /usr/local/bin/ttyd https://github.com/tsl0922/ttyd/releases/download/$TTYD_VERSION/ttyd.$TARCH && chmod +x /usr/local/bin/ttyd"
fi
# ── 4) OS-юзер и systemd-юнит ────────────────────────────────────────────────
if [ "$DRY_RUN" -eq 0 ]; then
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
log "создаю юзера $USER_NAME (без root-shell в веб-терминале)"
useradd -m -s /bin/bash "$USER_NAME"
fi
fi
WRITABLE='-W'; [ "$READONLY" -eq 1 ] && WRITABLE=''
UNIT_PATH="/etc/systemd/system/${SERVICE}.service"
ENV_LINES="Environment=HOME=/home/${USER_NAME}
Environment=PATH=/usr/local/bin:/usr/bin:/bin"
if [ ${#UNIT_ENV[@]} -gt 0 ]; then
for e in "${UNIT_ENV[@]}"; do ENV_LINES+="
Environment=$e"; done
fi
UNIT_CONTENT="[Unit]
Description=ttyd web terminal -> Claude Code (deploy-claude-code.sh)
After=network-online.target
Wants=network-online.target
[Service]
User=${USER_NAME}
${ENV_LINES}
ExecStart=/usr/local/bin/ttyd ${WRITABLE} -c ${USER_NAME}:${TOKEN} -p ${PORT} -i ${INTERFACE} claude
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
"
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry] пишу %s:\n%s\n' "$UNIT_PATH" "$UNIT_CONTENT"
else
CHANGED=1
if [ -f "$UNIT_PATH" ] && [ "$(cat "$UNIT_PATH")" = "$UNIT_CONTENT" ]; then
CHANGED=0; log 'юнит не изменился — перезапуск не нужен'
fi
printf '%s\n' "$UNIT_CONTENT" > "$UNIT_PATH"
systemctl daemon-reload
systemctl enable --now "$SERVICE.service" >/dev/null 2>&1 || true
if [ "$CHANGED" -eq 1 ] || ! systemctl is-active --quiet "$SERVICE.service"; then
systemctl restart "$SERVICE.service"
fi
sleep 1
fi
# ── 5) финальная проверка ────────────────────────────────────────────────────
if [ "$DRY_RUN" -eq 1 ]; then
log 'dry-run завершён: план выше, запускай без --dry-run'
exit 0
fi
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -n "$IP" ] || IP='<ip-сервера>'
log '─'
health_check || die 'финальная проверка не прошла (см. выше)'
log '─'
log "ГОТОВО: Claude Code + ttyd web-TUI"
log " URL : http://${IP}:${PORT}"
log " логин : ${USER_NAME}"
log " пароль : ${TOKEN}"
log " сервис : systemctl status ${SERVICE}"
log " первый вход: открой URL, пройди авторизацию claude (или передай --env ANTHROPIC_BASE_URL=... при развёртке)"

View file

@ -1,133 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Генерация PPTX: (1) разговор с Леонидом, (2) питч Simbiot (Seedance)."""
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
BG = RGBColor(0x0A, 0x0C, 0x12)
ACCENT = RGBColor(0x8B, 0x5C, 0xF6)
ACCENT2 = RGBColor(0x7C, 0x6C, 0xF5)
TEXT = RGBColor(0xF0, 0xEE, 0xFF)
MUT = RGBColor(0x9A, 0x97, 0xB8)
def add_slide(prs, title, bullets, kicker=None):
s = prs.slides.add_slide(prs.slide_layouts[6])
s.background.fill.solid(); s.background.fill.fore_color.rgb = BG
w, h = prs.slide_width, prs.slide_height
# accent bar
bar = s.shapes.add_shape(1, Emu(0), Emu(0), Emu(w), Inches(0.09))
bar.fill.solid(); bar.fill.fore_color.rgb = ACCENT; bar.line.fill.background()
if kicker:
tb = s.shapes.add_textbox(Inches(0.7), Inches(0.45), Inches(11.8), Inches(0.4))
tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = kicker.upper()
r.font.size = Pt(12); r.font.color.rgb = ACCENT2; r.font.bold = True
tb = s.shapes.add_textbox(Inches(0.7), Inches(0.85), Inches(12.0), Inches(1.0))
tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = title
r.font.size = Pt(30); r.font.color.rgb = TEXT; r.font.bold = True
tb = s.shapes.add_textbox(Inches(0.75), Inches(1.85), Inches(11.9), Inches(4.9))
tf = tb.text_frame; tf.word_wrap = True
first = True
for b in bullets:
lvl = 0
if isinstance(b, tuple): b, lvl = b
p = tf.paragraphs[0] if first else tf.add_paragraph(); first = False
r = p.add_run(); r.text = ("• " if lvl == 0 else "– ") + b
r.font.size = Pt(16 if lvl == 0 else 14); r.font.color.rgb = TEXT if lvl == 0 else MUT
p.space_after = Pt(8)
return s
prs = Presentation(); prs.slide_width = Inches(13.333); prs.slide_height = Inches(7.5)
K = "Встреча 143 · 2026-10-08"
add_slide(prs, "Разговор Алекса с Леонидом", [
"Выжимка разговора и договорённости · ~2.5 часа",
"Jitsi + агент-визуализатор Simbiot (слайды рисуются в реальном времени)",
"Кооператив 42 / студия Simbiot",
], kicker=K)
add_slide(prs, "Кто есть кто", [
"Леонид — давний знакомый студии, писатель, инвестор в deep-tech (Anti-Aging Hero с физиком Петром Федичевым)",
"DGX Spark 128GB (€5000) — дома гоняет CUDA-моделирование графов надёжности",
"Алекс — инфраструктура студии: агент-визуализатор, собственный роутер LM, RAG по знаниям кооператива",
("«1-й раз такое в моей практике… польза очевидна» — Леонид о демо", 1),
], kicker=K)
add_slide(prs, "Демо инфраструктуры студии", [
"Агент подключается к любому звонку и рисует слайды по ходу разговора (~45 сек на слайд)",
"Роутер сам выбирает LM под контекст (визуализация — GLM 5.3 Flash)",
"RAG: «он знает всё, что мы делаем» — общая база знаний кооператива",
("Инсайт Леонида: домашняя коробочка → быстрая генерация музыки/картинок/мини-видео", 1),
], kicker=K)
add_slide(prs, "Пилот: мультик по рассказу Леонида", [
"Мульт-адаптация рассказов (10–15 мин); Леонид готов финансово поддержать пилот",
"Ориентир бюджета — «в пределах 1000» на эксперимент (токены — точно, труд — обсудить)",
"Вход от студии: рассказ + свобода интерпретации, без пожеланий-ограничений",
"Партнёр уже строит harness генерации мультиков — матч идеальный; видео-часть ведёт Влад",
("Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео", 1),
], kicker=K)
add_slide(prs, "Творческий метод Леонида", [
"Хакатон ~2018: нейросеть на сольных песнях Леннона/Маккартни классифицировала битловские",
"Пишет с ИИ (~80% своё / ~20% ИИ), честно маркирует «made with AI» на after.today",
("«Литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность", 1),
], kicker=K)
add_slide(prs, "Экономика локального инференса", [
"Леонид: DGX Spark — нормальная цена за «домашний интеллект»; коробки станут достоянием домов",
"Алекс: облачный always-on — ок для большинства; топ-модели для разработки всё равно выигрывают",
("Сошлись на гибриде: локально — быстрая генерация, облако — тяжёлые продуктовые задачи", 1),
("У студии в планах свой inference-сервер (в ~6 раз быстрее)", 1),
], kicker=K)
add_slide(prs, "Как строить AI-компанию", [
"Леонид: «Vision — говно, execution — всё». Из короткого промпта не вырастет сложная система",
"Алекс: ядро студии — AI-ядро, перестраивающее паттерны (64) при смене контекста — главный долгосрочный актив",
"Стратегия: бизнес на нейро-инфраструктуре с начала → перевод на автопилот; незаменим только «CEO-решатель»",
("«Изюминка студии — в AI-ядре» — Леонид", 1),
("Методология deep-tech: продавать ценность сразу, не влюбляясь в R&D", 1),
], kicker=K)
add_slide(prs, "Договорённости", [
"1. Пилот анимации по рассказу Леонида: токены покрывает Леонид (до ~1000), ведёт Влад; Alex вернётся с конкретным предложением",
"2. Формат разговора продолжается — Леонид открыт к следующим сессиям",
"3. Голоса/доли за прошлые инвестиции — Леонид отказался: «инвестирую из философских соображений, не создавайте ожиданий»",
"4. Леонид посмотрит 35-минутную презентацию студии и вернётся с фидбэком",
], kicker=K)
add_slide(prs, "Что делать студии дальше", [
"Подготовить оффер по мульт-пилоту: объём, этап аудиоспектакля, смета токенов",
"Подключить партнёрский harness генерации мультиков — Леонид как первый клиентский кейс",
"Зафиксировать кейс «агент-визуализатор на внешнем звонке» — первое живое демо, положительный фидбэк",
], kicker=K)
prs.save("out/leonid_meeting143.pptx")
# --- Pitch deck: Simbiot / Seedance ---
prs2 = Presentation(); prs2.slide_width = Inches(13.333); prs2.slide_height = Inches(7.5)
K2 = "Simbiot · питч"
add_slide(prs2, "Simbiot — общий харнес организации", [
"Стартап за неделю получает не одного ассистента, а команду ИИ-агентов:",
"один отвечает клиентам, второй пишет код, третий следит за деньгами и метриками",
"Слой поверх Hermes, Claude Code и других — вместо зоопарка агентов и харнесов",
], kicker=K2)
add_slide(prs2, "Проблема", [
"Фаундер собирает зоопарк из агентов и харнесов вручную",
"Ушёл разработчик — ушла вся магия: знания и автоматизации не переживают людей",
"Каждый новый инструмент — ещё один остров, не связанный с остальными",
], kicker=K2)
add_slide(prs2, "Решение", [
"Simbiot координирует флот агентов и фиксирует решения компании",
"Удачные приёмы копятся как «гены» — остаются только те, что приносят прибыль",
"Компания превращается в самообучающуюся нейро-инфраструктуру",
], kicker=K2)
add_slide(prs2, "Тракшен", [
"Уже гоняется на семи собственных продуктах студии",
"AI-ядро: 64 паттерна, перестраивающихся при смене контекста",
"Агент-визуализатор, RAG по знаниям, собственный LM-роутер — в продакшене",
], kicker=K2)
add_slide(prs2, "Видео (Seedance), 3 шота", [
"Shot 1 «зоопарк в хаосе»: тёмная серверная, хаотичные голограммы и глитчи (0–10 сек)",
"Shot 2 «orchestration»: хаос сходится в фиолетовую сферу, панели выстраиваются в сетку (10–20 сек)",
"Shot 3 «финал»: фаундер у стены дашбордов с растущими revenue-графиками, отъезд в город (20–30 сек)",
("Стиль: dark cinematic, #0a0c12 + violet #7c6cf5, film grain, 4k, 16:9", 1),
], kicker=K2)
add_slide(prs2, "Строй компанию — а не зоопарк ботов", [
"Simbiot · Кооператив 42 · simbiot",
"Хронометраж питча: ~35 сек (15 сек — первый и последний абзац)",
], kicker=K2)
prs2.save("out/simbiot_pitch.pptx")
print("OK")

View file

@ -1,14 +0,0 @@
#!/bin/bash
# Ночная самодоработка харнеса (директива Alex m-516173718, эскалация e-261005074512):
# подаёт задачу [M] в очередь харнеса — ближайший hour-тик подбирает и исполняет.
# Журнал итераций: artifacts/self-improvement-log.md
cd /root/simbiot-harness || exit 1
TITLE="Ночная самодоработка харнеса: ретро + эксперимент"
# идемпотентность: не дублируем, если задача уже ждёт/выполняется
if ./venv/bin/python cli.py tasks 2>/dev/null | grep -F "$TITLE" | grep -qE ' (ready|running)'; then
echo "$(date -u +%FT%TZ) skip: задача уже в очереди" >> /var/log/simbiot-selfimprove.log
exit 0
fi
./venv/bin/python cli.py task "$TITLE" \
-d "Директива Alex m-516173718 (e-261005074512). 1) Ретро суток: cli.py events, cli.py status, cli.py aum — что сработало/что сломалось (упавшие тики, эскалации, закрытые задачи, burn). 2) Один эксперимент над собой: гипотеза вида «метрика X -> изменение -> ожидание»; лёгкий провести сейчас в пределах контекста, тяжёлый оформить задачей на день. 3) Журнал: artifacts/self-improvement-log.md — дописать строку по формату (#N | дата | гипотеза | метрика до->после | вывод). 4) Ночь — тишина в чате (кроме критичного), результат уходит в ответ задачи, его поднимет утренний дайджест. Фильтр AUM: эксперимент служит +1% к AUM в год+." \
-m 30

View file

@ -1,23 +0,0 @@
[Unit]
Description=Simbiot Harness - Telegram inbox poller
After=network-online.target simbiot-harness.service
Wants=network-online.target
[Service]
Environment=SIMBIOT_HOME=__DATA_DIR__
Environment=SIMBIOT_RAG_DB=/root/rag/rag.db
Type=simple
WorkingDirectory=__INSTALL_DIR__
ExecStart=__INSTALL_DIR__/venv/bin/python -m harness.tg_inbox
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
# t-261004160531-780666: поллер лёгкий — тяжёлая транскрипция (faster-whisper
# large-v3-turbo ~2-3G, ffmpeg) вынесена в отдельные transient-scope'ы
# (systemd-run --scope, см. harness/voice.py _heavy_run) с их MemoryMax.
# Жёсткий потолок юнита: 3.7G peak больше невозможен физически.
MemoryMax=1G
MemoryHigh=768M
[Install]
WantedBy=multi-user.target

View file

@ -1,168 +0,0 @@
#!/usr/bin/env python3
# ═══════════════════════════════════════════════════════════
# SPA filler — Studio_Participation_Agreement_2026-09 → per-founder
# Задачи t-261008143050 (Siarhei Zhukovich, когорта с 16-го) и
# t-261008144131 (7 участников 42 Labs за 2026-09).
# Паттерн подстановки — как _contract_docx_bytes в /root/coop/app.py.
# ═══════════════════════════════════════════════════════════
import os, sys, copy
import docx
TEMPLATE = "/root/coop/legal/Studio_Participation_Agreement_2026-09.docx"
OUT = "/root/coop/legal"
DRAFT_LINE = "DRAFT TEMPLATE — for review by U.S. counsel before first use. Not legal advice."
# Перепривязка платежного графика на 16-е число (когорты с 16-го, t-261008143050)
SIXTEENTH = [
("no later than the 1st day of the first month of participation",
"no later than the 16th day of the first month of participation"),
("each subsequent Monthly Fee no later than the 1st day of the corresponding month of participation",
"each subsequent Monthly Fee no later than the 16th day of the corresponding month of participation"),
("If the Agreement is signed after the 1st, the Program Start is the 1st day of the next calendar month",
"If the Agreement is signed after the 16th, the Program Start is the 16th day of the next calendar month"),
("for Monthly Fee tranches, the 1st day of the participation month",
"for Monthly Fee tranches, the 16th day of the participation month"),
("takes effect on the 1st day of the next participation month",
"takes effect on the 16th day of the next participation month"),
("payable in advance by the 1st of each participation month",
"payable in advance by the 16th of each participation month"),
("the 1st day of the month of signature if signed on the 1st, otherwise the 1st day of the next calendar month",
"the 16th day of the month of signature if signed on the 16th, otherwise the 16th day of the next calendar month"),
]
def founder_replacements(f):
"""f: dict(name, passport, pid, address, email, project, one_line, rev_share,
effective, cohort, program_start, sixteenth: bool)"""
r = {
"[DATE]": f["effective"],
"[FULL LEGAL NAME]": f["name"],
"[NAME]": f["name"],
"[EMAIL]": f.get("email") or "[EMAIL]",
"cohort [MONTH YEAR]": f"cohort {f['cohort']}",
"1 [MONTH] [YEAR]": f["program_start"],
"[MONTH YEAR]": f["cohort"],
}
if f.get("rev_share"):
r["[X]"] = str(f["rev_share"])
if f.get("project"):
r["[PROJECT NAME]"] = f["project"]
if f.get("one_line"):
r["[ONE LINE]"] = f["one_line"]
# Строка Founder: имя + статус + паспорт + адрес одной подстановкой
line_old = "[FULL LEGAL NAME], an individual / [ENTITY TYPE], residing at [ADDRESS], [COUNTRY]"
line_new = (f"{f['name']}, an individual, passport of the Republic of Belarus "
f"{f.get('passport') or '[PASSPORT NO.]'} (personal identification No. "
f"{f.get('pid') or '[PERSONAL ID NO.]'}), residing at "
f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus")
r[line_old] = line_new
# фолбэк, если строка разбита рант differently
r["an individual / [ENTITY TYPE]"] = "an individual"
# паспорт/ид.номер вставляются после схлопывания individual-строки
# (порядок ключей dict важен: этот ключ применяется после предыдущего)
pass_line = (f"passport of the Republic of Belarus {f.get('passport') or '[PASSPORT NO.]'}"
f" (personal identification No. {f.get('pid') or '[PERSONAL ID NO.]'}), residing at")
r["an individual, residing at"] = f"an individual, {pass_line}"
r["[ADDRESS], [COUNTRY]"] = f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus"
r["[ADDRESS]"] = f.get("address") or "[ADDRESS]"
r["[COUNTRY]"] = "Republic of Belarus"
return r
def fill(f, out_docx):
doc = docx.Document(TEMPLATE)
reps = founder_replacements(f)
if f.get("sixteenth"):
reps = dict(SIXTEENTH) | reps
reps[DRAFT_LINE] = ""
for p in doc.paragraphs:
if not p.runs:
continue
full = "".join(r.text for r in p.runs)
new = full
for k, v in reps.items():
if k in new:
new = new.replace(k, v)
if new != full:
p.runs[0].text = new
for r in p.runs[1:]:
r.text = ""
doc.save(out_docx)
return out_docx
def to_pdf(docx_path):
import subprocess, tempfile
with tempfile.TemporaryDirectory() as td:
r = subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "--outdir", td, docx_path],
capture_output=True, timeout=90)
pdf = os.path.join(td, os.path.basename(docx_path).replace(".docx", ".pdf"))
if os.path.exists(pdf):
dst = docx_path.replace(".docx", ".pdf")
open(dst, "wb").write(open(pdf, "rb").read())
return dst
return None
def leftover_placeholders(docx_path):
import re
d = docx.Document(docx_path)
found = set()
for p in d.paragraphs:
found.update(re.findall(r"\[[A-Z][A-Z0-9 /.,%()\[\]-]*\]", p.text))
return sorted(found)
FOUNDERS = [
# ── t-261008143050: Siarhei Zhukovich, когорта с 16-го ──
dict(file="SPA_Siarhei_Zhukovich", name="Siarhei Zhukovich",
passport="AB3393306", pid=None, # ид. номер — нет в базе, плейсхолдер
address="28 July str., 33B-42, Brest", email=None,
project="podrabotki.by (rebranding to StaffGo)",
one_line="a platform for hiring line staff: employment/contract placement and turnkey contractor paperwork (documents and payments)",
rev_share=50, effective="October 16, 2026", cohort="October 2026",
program_start="16 October 2026", sixteenth=True),
# ── t-261008144131: участники 42 Labs за 2026-09 (lab_debts) ──
dict(file="SPA_Alex_Shkor", name="Alex Shkor", passport=None, pid=None,
address=None, email="alex@shkor.com", project=None, one_line=None,
rev_share=None, effective="September 1, 2026", cohort="September 2026",
program_start="1 September 2026", sixteenth=False),
dict(file="SPA_Vlad", name="Vlad", passport=None, pid=None,
address=None, email="vla2music.com@gmail.com", project=None, one_line=None,
rev_share=None, effective="September 1, 2026", cohort="September 2026",
program_start="1 September 2026", sixteenth=False),
dict(file="SPA_Yura", name="Yura", passport=None, pid=None,
address=None, email="yura28440@gmail.com", project=None, one_line=None,
rev_share=None, effective="September 1, 2026", cohort="September 2026",
program_start="1 September 2026", sixteenth=False),
dict(file="SPA_Stas_Dashchinskiy", name="Stas Dashchinskiy", passport=None, pid=None,
address=None, email="dashchinskiystas@gmail.com", project=None, one_line=None,
rev_share=None, effective="September 1, 2026", cohort="September 2026",
program_start="1 September 2026", sixteenth=False),
dict(file="SPA_Alexander", name="Александр", passport=None, pid=None,
address=None, email="Justgbox@gmail.com", project=None, one_line=None,
rev_share=None, effective="September 1, 2026", cohort="September 2026",
program_start="1 September 2026", sixteenth=False),
dict(file="SPA_Melnikau", name="Andrei Melnikau", passport="MP2848482",
pid="3120386A064PB1", address=None, email="ex3.mac@gmail.com",
project="tYESter",
one_line="a family of software tools for testing computer hardware components without the need to purchase additional equipment",
rev_share=None, effective="September 1, 2026", cohort="September 2026",
program_start="1 September 2026", sixteenth=False),
dict(file="SPA_Dmitry_Siasitski", name="Dmitry Siasitski", passport=None, pid=None,
address=None, email="sesitski.dmitry@gmail.com", project=None, one_line=None,
rev_share=None, effective="September 1, 2026", cohort="September 2026",
program_start="1 September 2026", sixteenth=False),
]
if __name__ == "__main__":
only = sys.argv[1:] or None
for f in FOUNDERS:
if only and f["file"] not in only:
continue
docx_path = os.path.join(OUT, f["file"] + ".docx")
fill(f, docx_path)
pdf = to_pdf(docx_path)
print(f"{f['file']}: docx={'OK' if os.path.exists(docx_path) else 'FAIL'} "
f"pdf={'OK' if pdf else 'FAIL'} leftover={leftover_placeholders(docx_path)}")

View file

@ -12,7 +12,6 @@
|---|---|---|
| `meta-skill-factory` | Метаскилл: превращение повторяющейся работы в скилл репо (шаги, чеклисты, критерии качества) | Заметил, что делаю одну и ту же работу 2+ раз — прежде чем делать руками в третий |
| `presale-landing` | Производство пресейл/продуктовых лендингов: структура, честные мокапы, Stripe CTA, quality gates | Задача на лендинг/пресейл-страницу или её v2 |
| `gcal` | Google Calendar: команда из TG «сделай звонок на X» → событие с приглашениями в рабочий день (цикл 3 через 3); инструменты tools/gcal.py | Просьба спланировать звонок/встречу/событие, вопрос по графику |
| `systemone-bench` | Decision-эндпоинт Ollama /v1/systemone (nimble) + GA-оптимизация: иерархический выбор top-2 группы → choice ≤9, GA последовательностей действий с фитнесом=AUM | Тик, где нужно выбрать одно действие из >9 однородных опций; оптимизация политик на симуляторе |
## Сторонние наборы

View file

@ -1,73 +0,0 @@
---
description: Build compact premium landing pages with clear CTA hierarchy, quiet typography, and minimal visual noise. Install from Danilaa1 in one command.
---
# Compact Landing
Build a small, finished product world—not a generic marketing template. Preserve compactness and clarity; reinvent the visible design language for each product.
## Workflow
1. Inspect the product, supplied content, current stack, and existing design language. Reuse installed tools and components.
2. Ask for button size first—compact, medium, or large—unless the user already answered or asked you to choose.
3. Ask at most two adaptive questions only when their answers would materially change the result. Good subjects: brand mood, content depth, corner character, or motion level.
4. Create a design fingerprint before coding. If this skill includes scripts and Node is available, run `node scripts/pick-direction.mjs`. Otherwise choose from `references/style-dna.md` without taking the first option in every list.
5. Adapt the fingerprint to product meaning. Treat picker output as a creative constraint, not a command to make incoherent choices.
6. Identify one product-native signature artifact: a real demo, visualized output, interactive sample, comparison, miniature workflow, data specimen, or useful control. This should create identity more than decoration does.
7. Implement the smallest complete page. Prefer HTML/CSS and existing dependencies. Add JavaScript only for behavior the page actually needs.
8. Verify mobile layout, keyboard focus, reduced motion, stable dynamic states, and the real primary action.
If the user wants immediate execution, choose the button size and fingerprint yourself. Do not stop for a questionnaire.
## Design Fingerprint
Define these axes before implementation:
- **Composition:** field note, offset rail, split pamphlet, framed console, compact catalogue, stacked specimen, or another product-specific silhouette.
- **Measure:** choose a fitting compact width, usually `420-720px`; do not default to `440px` every time.
- **Palette:** warm paper, cool fog, monochrome ink, dark charcoal, product-tinted, or playful light.
- **Typography:** grotesk + mono, humanist sans + mono, editorial serif + sans, rounded sans + mono, condensed + sans, or purposeful all-mono.
- **Geometry:** sharp, machined, balanced, friendly, soft, or mixed/concentric radii.
- **Surface grammar:** whitespace, flat dividers, inset hairlines, layered rings, tinted panels, paper rules, or technical frames. Pick one dominant grammar.
- **Motif:** emoji punctuation, ASCII marks, indices, stamps, doodles, colored dots, tiny object icons, or none.
- **Motion:** choose one entrance behavior and one or two interaction behaviors.
Variation must change the first-screen silhouette, not merely colors or labels. Ensure at least four axes differ from the old canonical recipe: centered `440px` column, neutral white/zinc, Geist + Geist Mono, `6-10px` radii, inset hairline cards, Instrument order, staggered rise.
Do not randomize components independently. Choose one coherent art direction, then derive tokens from it. Use only one or two surprising details; repetition inside the page creates coherence.
Read `references/style-dna.md` for compatible ranges, direction families, motion patterns, and review checks.
## Invariants
- Keep the page compact in scope and information density, even when its canvas is wider or split.
- Put the primary action in the first viewport. Repeat it only when doing so removes friction.
- Use clear type hierarchy, concrete copy, and enough contrast.
- Use real product content. Avoid fake metrics, vague claims, filler testimonials, and decorative dashboards.
- Extend visually tiny controls to at least a `40px` hit area without making them look oversized.
- Show a visible `:focus-visible` state and preserve semantic HTML.
- Use tabular numerals for changing numbers.
- Respect `prefers-reduced-motion` and keep the reduced experience fully usable.
- Never add a dependency for a small effect CSS or the current stack can handle.
## Zero-Shift Motion
- Animate only `transform`, `opacity`, and `filter`. Never use `transition: all`.
- Reserve dimensions before animation: fixed/min heights, `aspect-ratio` for media, fixed icon slots, width-locked changing labels, and tabular numerals.
- Overlay alternate labels, icons, tabs, and panels in the same grid area or fixed box. Do not insert delayed content into document flow.
- Keep padding, borders, font size, font weight, line height, grid tracks, and flex sizing stable across hover and state changes.
- Prefer transforms for toggles, drawers, traces, and decorative motion. Absolute-position decorative particles so they never affect flow.
- Self-host fonts when practical. Otherwise use metric-compatible fallbacks and avoid hiding the page while fonts load.
- Gate hover-only effects behind `(hover: hover) and (pointer: fine)`.
## Buttons
Apply one scale consistently:
- **Compact:** `24-28px` visual height, `11-12px` label.
- **Medium:** `32-36px` visual height, `12.5-13px` label.
- **Large:** `40-44px` visual height, `13.5-14px` label.
Use `scale(0.96)` for press feedback. For wide command controls, use `0.98`. Keep label and icon slots stable during copied/loading/success states.
For `llms.txt` or assistant-copy actions, use a small mono button only when the product needs it. Reuse the project's icon system or a CSS glyph; do not embed SVG examples in skill output.
## Purposeful Personality
Emoji are allowed when they fit the product. Use `1-3` as punctuation, labels, tiny stickers, or state cues—not as a substitute for structure or iconography. Reserve their boxes so platform glyph differences do not move layout.
Color may be playful, dark, editorial, or product-tinted. Give every accent a role and repeat it. Avoid default purple-blue AI gradients, uncontrolled rainbows, and random decorative color.
Motion should explain state, focus attention, or express the product metaphor. A fixed-size trace, ambient floating mark, slot swap, clipped reveal, or state color wash can feel alive without causing reflow. Avoid perpetual motion everywhere.
## Avoid
- Reusing one section order, narrow width, palette, radius family, or component set across unrelated products.
- Generic oversized heroes, three equal feature cards, badge clouds, and feature grids by reflex.
- Copying the Cuelume waveform, cream palette, confetti trio, or exact layout unless the product genuinely calls for them.
- Making every surface a card or every control a pill.
- Decorative animation that competes with the primary action.
- Heavy animation libraries for opacity, transforms, tabs, counters, or simple reveals.
## Final Check
- Fingerprint chosen before code; at least four high-salience axes differ from the canonical recipe.
- Product-native signature artifact is visible and useful.
- First viewport has a distinct silhouette and clear primary action.
- Radius, palette, type, surface, motif, and motion form one coherent direction.
- Mobile becomes a clean single column without horizontal overflow.
- Dynamic content, media, icons, labels, and panels have reserved geometry.
- Keyboard, focus, contrast, touch targets, and reduced motion work.
- Build and the primary interaction pass using the project's existing verification path.

View file

@ -1,66 +0,0 @@
---
name: gcal
description: Use when Alex asks in TG to schedule a call/meeting ("сделай звонок на X с Владой", "запланируй созвон", "поставь встречу") — creates a Google Calendar event with guest invites in a workday slot (Alex's «3 через 3» cycle), or checks the schedule. Powered by tools/gcal.py.
metadata:
origin: task t-261008155436-a658e9, messages m-516173943/m-516173944
requires: secrets/gcal-service-account.json (service account key; until provided — dry-run/planning only)
---
# gcal: команда из TG → событие в календаре Alex
Одна команда из TG должна решаться одним прогоном этого скилла:
интеншн → валидный слот (рабочий день!) → событие с приглашением → ответ в чат.
## Шаг 1 — разобрать команду
Из сообщения извлечь:
- **что**: тема (по умолчанию «Звонок: <тема/имя гостя>»);
- **когда**: день (сегодня/завтра/дата) и время; длительность (по умолчанию 30 мин);
- **кого**: email гостей. Известные контакты: Влада — videoprodshema@gmail.com.
Email НЕ известен → спросить у Alex ДО создания (угадывать адрес нельзя);
- **время**: Alex в Минске (UTC+3). Время из TG трактовать как минское,
в инструмент передавать UTC.
## Шаг 2 — валидация слота (строго до создания)
```
./venv/bin/python tools/gcal.py next-workdays # ближайшие рабочие
./venv/bin/python tools/gcal.py create ... --dry-run # что попадёт в Google
```
Guard'ы (встроены в create, не обходить):
1. **Только рабочий день** — цикл «3 через 3» (docs/alex-schedule.md).
Якорь цикла: env GCAL_WORK_ANCHOR (по умолчанию 2026-10-02). Сдвиг цикла
Alex сообщил в TG → обновить docs/alex-schedule.md и якорь.
2. **Не позже 2ч до начала** (GCAL_MIN_LEAD_MIN=120) — анти-паттерн рекордера
(t-261008155708: встречи, созданные <2 мин до старта, не заходятся).
Выходной/поздний слот → НЕ создавать, ответить Alex с ближайшими рабочими
днями и попросить подтвердить слот.
## Шаг 3 — создать событие
```
./venv/bin/python tools/gcal.py create \
--title "Звонок: Влада" --day YYYY-MM-DD --time HH:MM \
--duration 30 --guests "videoprodshema@gmail.com" --timezone Europe/Minsk
```
`sendUpdates=all` — приглашения уходят гостям автоматически.
Ответ в чат: ссылка на событие (htmlLink), слот, гости.
## Ключ (один раз, если не настроен)
`tools/gcal.py check` печатает готовность и шаги:
Google Cloud → Calendar API → service account → JSON-ключ → дать
client_email право «Вносить изменения» на календаре Alex → положить в
`secrets/gcal-service-account.json` (0600). Ключа нет → работать dry-run'ом
и попросить Alex прислать ключ (это может сделать только владелец аккаунта).
## Критерии качества
- [ ] Слот — рабочий день цикла; иначе отказ с предложением ближайших рабочих
- [ ] До начала ≥2ч (или осознанный --force по явной просьбе Alex)
- [ ] Гости получили приглашения (sendUpdates=all), email не угадан
- [ ] В ответе Alex — время в его зоне (Минск) и ссылка на событие
- [ ] `check` зелёный; ключ лежит в secrets/ с правами 0600

View file

@ -1,111 +0,0 @@
# -*- coding: utf-8 -*-
"""t-261009160906: анти-гонка уровней min/hour на входящих TG (hermetic).
Кейс 09.10: Alex попросил концепт (m-516174013). Мин-тик ответил и доставил
свой файл (16:01). Часовой тик, не видя ответа мин-уровня, сгенерировал СВОЙ
файл и доставил вторым (16:06) — Alex получил два разных файла на один запрос.
Dedup по artifact_id=file:<путь> не ловит: два разных файла = два разных ключа.
Фикс: связка «артефакт → входящее» (-m m-NNN у tg-deliver-file, msg= у
deliver_artifact, reply_artifact в read model). Если на входящее уже отвечено
иным артефактом — SKIP с событием. force обходит (ручная повторная доставка).
Запуск: ./venv/bin/python tests/test_msg_artifact_race.py
"""
import os
import subprocess
import sys
# ── изоляция: своя БД в /tmp (prod не трогаем) ──
_DST = "/tmp/simbiot-test-msg-artifact-race.db"
for p in (_DST, _DST + "-wal", _DST + "-shm"):
if os.path.exists(p):
os.unlink(p)
os.environ["SIMBIOT_DB"] = _DST
sys.path.insert(0, "/root/simbiot-harness")
from harness import es, commands
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
if not cond:
fails.append(name)
es.init_db()
# ── подготовка: входящее m-990001, на которое мин-уровень уже ответил ──
es.append_event("inbox", "m-990001", "MessageReceived", {
"update_id": 990001, "chat_id": 42, "message_id": 1, "user_id": 1,
"text": "сделай концепт в html", "kind": "text", "date": 1791560000,
})
es.append_event("inbox", "m-990001", "ReplySent", {
"to_chat": 42, "text": "готово, отправил файлом", "tg_ok": True, "channel": "tg",
})
# файлы-двойники гонки: мин-уровень и часовой
_F1 = "/tmp/simbiot-test-race-min.html"
_F2 = "/tmp/simbiot-test-race-hour.html"
for f in (_F1, _F2):
with open(f, "w") as fh:
fh.write("<html><body>r</body></html>")
sends = []
# 1) мин-уровень доставил свой файл с привязкой к сообщению
ok1 = commands.deliver_artifact(f"file:{_F1}", kind="file", send=lambda: (sends.append(_F1) or True),
owner="test:min", caption="мин", msg="m-990001")
check("min-level file delivered", ok1 and sends == [_F1], sends)
model = es.run_projections()
m = model["inbox"]["m-990001"]
check("read model binds reply_artifact", m.get("reply_artifact") == f"file:{_F1}", m.get("reply_artifact"))
# 2) часовой уровень пытается доставить СВОЙ файл на то же входящее → SKIP
ok2 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
owner="test:hour", caption="час", msg="m-990001")
check("hour-level duplicate SKIPped", ok2 is False and sends == [_F1], sends)
db = es._conn()
skip = list(db.execute(
"SELECT data FROM events WHERE event_type='DeliverySkipped' AND data LIKE '%msg-already%'"))
check("DeliverySkipped event with reason", len(skip) == 1, len(skip))
# 3) force обходит защиту (ручная повторная доставка — явное решение человека)
ok3 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
owner="test:hour", caption="час", force=True, msg="m-990001")
check("force bypasses guard", ok3 and sends == [_F1, _F2], sends)
# 4) доставки БЕЗ привязки к сообщению не задеваются (обратная совместимость)
ok4 = commands.deliver_artifact("file:/tmp/simbiot-test-race-unbound.html", kind="file",
send=lambda: (sends.append("unbound") or True),
owner="test", caption="x")
check("unrelated delivery unaffected", ok4 and sends[-1] == "unbound", sends)
# 5) второе входящее без артефакта: доставка с привязкой проходит свободно
es.append_event("inbox", "m-990002", "MessageReceived", {
"update_id": 990002, "chat_id": 42, "message_id": 2, "user_id": 1,
"text": "ещё один запрос", "kind": "text", "date": 1791560100,
})
es.append_event("inbox", "m-990002", "ReplySent", {
"to_chat": 42, "text": "ок", "tg_ok": True, "channel": "tg",
})
ok5 = commands.deliver_artifact("file:/tmp/simbiot-test-race-second.html", kind="file",
send=lambda: (sends.append("second") or True),
owner="test:hour", caption="y", msg="m-990002")
check("fresh msg accepts its artifact", ok5 and sends[-1] == "second", sends)
# 6) CLI: tg-deliver-file -m m-990001 с чужим артефактом → SKIP, выход 0
r = subprocess.run(
["./venv/bin/python", "cli.py", "tg-deliver-file", _F2, "-m", "m-990001",
"-c", "dup", "-k", "file:" + _F2],
capture_output=True, text=True, cwd="/root/simbiot-harness")
check("CLI SKIP on raced msg", r.returncode == 0 and "SKIP" in r.stdout,
r.stdout.strip()[:120] + r.stderr.strip()[-200:])
print("=" * 40)
print("FAILS:", len(fails), fails if fails else "— все проверки прошли")
sys.exit(1 if fails else 0)

View file

@ -1,159 +0,0 @@
# -*- coding: utf-8 -*-
"""t-261004160532: служебный текст inbox не порождает задачу-клон.
Паттерн само-клонирования чинился дважды:
e-261003125707 — 'list' из чата → мин-агент звал `cli.py task list`,
создавался клон «list»; фикс: 'list'/'tasks'/'task list' = доска;
e-261004134613 — агент звал `cli.py task t-...` для ЧТЕНИЯ задачи,
создавался клон с title=ID; фикс: title=ID → показать / exit 2.
Регресс проверяет весь путь: парсер команд ('list' — команда, а не речь),
CLI-гварды (list/ID не создают задач), и инбокс-уровень (служебные тексты
распознаются как команды → answered, LLM их не плодит).
Запуск: ./venv/bin/python tests/test_no_clone_inbox.py | pytest tests/test_no_clone_inbox.py
"""
import os
import subprocess
import sys
# ── изоляция: свежая БД в /tmp, прод не нужен ──
_DST = "/tmp/simbiot-test-no-clone.db"
for p in (_DST, _DST + "-wal", _DST + "-shm"):
if os.path.exists(p):
os.unlink(p)
sys.path.insert(0, "/root/simbiot-harness")
os.environ["SIMBIOT_DB"] = _DST
from harness import es, tg_commands, tg_inbox
fails = []
_REPO = "/root/simbiot-harness"
_PY = os.path.join(_REPO, "venv", "bin", "python")
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "", flush=True)
if not cond:
fails.append(name)
raise AssertionError(f"{name} {extra}")
es.init_db()
def _cli(*args):
"""cli.py в изолированной БД (env наследуется)."""
r = subprocess.run([_PY, os.path.join(_REPO, "cli.py")] + list(args),
capture_output=True, text=True, timeout=60,
cwd=_REPO)
return r.returncode, (r.stdout or "") + (r.stderr or "")
def _task_count():
return len((es.run_projections().get("tasks") or {}))
# ── 1. парсер: служебные слова — команды, а не речь для LLM ──────────────────
def test_parse_service_words():
P = tg_commands.parse
check("parse 'list' → команда задачи (e-261001124837)",
P("list") == "задачи", f"{P('list')!r}")
check("parse 'tasks' → команда", P("tasks") == "задачи")
check("parse 'LIST' регистронезависимо", P("LIST") == "задачи")
check("parse '/задачи' → команда", P("/задачи") == "задачи")
check("parse 'list please' — НЕ команда (речь, но не служебный текст)",
P("list please") is None)
# ID задачи в тексте — тоже служебный шум, не команда и не «задача»
check("parse 't-261004160531-ed25ed' — не команда", P("t-261004160531-ed25ed") is None)
# ── 2. CLI-гварды: list / ID не создают задач ────────────────────────────────
def test_cli_list_is_board_not_clone():
"""`cli.py task list` → доска; число задач не растёт (e-261003125707)."""
from harness import commands
commands.submit_task("реальная работа", "", minutes=5)
before = _task_count()
check("задача-незабудка создана", before >= 1)
for variant in ("list", "tasks", "task list"):
rc, out = _cli("task", variant)
check(f"cli task '{variant}': rc=0", rc == 0, f"rc={rc} {out[:120]}")
check(f"cli task '{variant}': показывает доску",
"реальная работа" in out, out[:150])
after = _task_count()
check("cli task list/tasks/task list: клонов НЕТ (счёт задач тот же)",
after == before, f"было {before}, стало {after}")
def test_cli_task_id_reads_not_clones():
"""`cli.py task <id>` → чтение; несуществующий → exit 2 без клона."""
from harness import commands
real_id, _ = commands.submit_task("целевая задача для чтения", "", minutes=5)
before = _task_count()
rc, out = _cli("task", real_id)
check("cli task <существующий id>: rc=0", rc == 0, f"rc={rc}")
check("cli task <id>: показывает задачу", "целевая задача для чтения" in out, out[:200])
fake = "t-269999999999-deadbe"
rc2, out2 = _cli("task", fake)
check("cli task <несуществующий id>: exit 2", rc2 == 2, f"rc={rc2}")
check("cli task <несуществующий id>: отказ в создании клона",
"не найдена" in out2 and "запрещено" in out2, out2[:200])
after = _task_count()
check("cli task <id>: клонов НЕТ (e-261004134613)",
after == before, f"было {before}, стало {after}")
# ── 3. инбокс-уровень: служебный текст распознан и отвечен без клона ────────
def test_inbox_service_text_no_task():
"""'list' и ID задачи в inbox → ответ командой, задач не прибавилось."""
before = _task_count()
# в реальном стриме их кладёт поллер; кладём так же, как он
tg_inbox.receive_message({"update_id": 991001, "message": {
"message_id": 991001 % 100000, "date": 1780000100,
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
"text": "list"}})
tg_inbox.receive_message({"update_id": 991002, "message": {
"message_id": 991002 % 100000, "date": 1780000101,
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
"text": "t-261004160531-ed25ed что там по срокам?"}})
model = es.run_projections()
inbox = model.get("inbox") or {}
check("инбокс: оба служебных текста приняты",
"m-991001" in inbox and "m-991002" in inbox)
check("инбокс: 'list' распознан как команда ('задачи')",
tg_commands.parse(inbox["m-991001"]["text"]) == "задачи")
check("инбокс: ID в фразе — не команда (речь, уходит LLM)",
tg_commands.parse(inbox["m-991002"]["text"]) is None)
# minute-level фильтр: команды не показываются мин-LLM как «сырые» входящие
from harness.scheduler import build_context, LEVELS
fake = {"inbox": dict(inbox), "tasks": dict(model.get("tasks") or {}),
"escalations": {}, "policy": {}}
ctx = build_context(LEVELS["min"], fake)
check("build_context(min): 'list' не показан как открытое входящее",
"m-991001" not in ctx or "list" not in _line(ctx, "m-991001"))
check("после обработки служебных текстов задач-клонов НЕТ",
_task_count() == before, f"было {before}, стало {_task_count()}")
def _line(ctx, marker):
for ln in ctx.splitlines():
if marker in ln:
return ln
return ""
# ────────────────────────────── plain-run режим ──────────────────────────────
if __name__ == "__main__":
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for t in tests:
try:
t()
except AssertionError:
pass
print(f"\n{'='*60}")
print(f"{'ALL OK' if not fails else 'FAILED: ' + ', '.join(fails)} ({len(tests)} тестов)")
sys.exit(1 if fails else 0)

View file

@ -1,175 +0,0 @@
# -*- coding: utf-8 -*-
"""t-261010093054-f67dac: MCP портала кооператива как инструмент харнеса, hermetic.
Запуск: ./venv/bin/python tests/test_portal_mcp.py
Сеть НЕ трогается: harness.portal_mcp._rpc подменяется фейком, шлюз
отвечает заготовками. Проверяет:
- call_tool: happy-path — text из content[] портала;
- guardrail: product_create/product_set_mode запрещены (деньги/Stripe),
SIMBIOT_MCP_ALLOW_ALL=1 снимает запрет;
- неизвестный тул → error: со списком доступных (из tools/list);
- arguments: dict как есть, JSON-строка парсится, мусор — error:;
- нет токена → error:, не рэйз;
- обрезка длинного результата до MAX_RESULT_CHARS;
- HTTP/JSON-сбой шлюза → error:, не рэйз (тик не валится);
- трейс PortalMcpCalled в event store (level/trace_id, tool, ok);
- тул тика _portal_mcp_tool_for: лимит 4 вызова, дальше отказ текстом;
- paths.portal_mcp_token_path: env → secrets/ инстанса → legacy.
"""
import json
import os
import sys
import threading # noqa: F401 (паритет импортов с другими тестами)
_DST = "/tmp/simbiot-test-portal-mcp.db"
for p in (_DST, _DST + "-wal", _DST + "-shm"):
if os.path.exists(p):
os.unlink(p)
os.environ["SIMBIOT_DB"] = _DST
# токен для основного потока проверок (env сильнее файла)
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = "agt_test_token"
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from harness import es, portal_mcp
from harness import paths as _paths
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "")
if not cond:
fails.append(name)
es.init_db()
# ── фейк шлюза: tools/list + tools/call с заготовками ────────────────────────
SPEC = {"tools": [{"name": n} for n in
("whoami", "booking_slots", "booking_create", "booking_cancel",
"list_calls", "product_create", "product_set_mode")]}
rpc_calls = []
def _fake_rpc(method, params, timeout=15):
rpc_calls.append((method, params))
if method == "tools/list":
return SPEC
if method == "tools/call":
name = params.get("name")
if name == "whoami":
return {"content": [{"type": "text",
"text": json.dumps({"agent": "Simbiot"})}]}
if name == "booking_slots":
return {"content": [{"type": "text",
"text": json.dumps({"ok": True, "slots": [{"utc": "x"}]})}]}
raise AssertionError(f"неожиданный тул в тесте: {name}")
raise AssertionError(f"неожиданный метод: {method}")
portal_mcp._rpc = _fake_rpc
portal_mcp._tools_cache = None
# ── 1. happy-path: text из content[] ─────────────────────────────────────────
r = portal_mcp.call_tool("whoami", None, level="min", trace_id="t-test-mcp")
check("call_tool: text из content[]", json.loads(r)["agent"] == "Simbiot", r[:120])
check("call_tool: arguments None → {}", rpc_calls[-1][1]["arguments"] == {})
# ── 2. трейс PortalMcpCalled в сторе ────────────────────────────────────────
model = es.run_projections()
tevs = [e for e in model.get("traces", {}).values()
if isinstance(e, dict) and e.get("tool") == "whoami"] if "traces" in model else []
check("trace: PortalMcpCalled записан (best effort)", True) # наличие — не контракт тика
# прямой контракт: событие в сторе по aggregate_id
raw = [a for a in es.list_events() if a[2] == "PortalMcpCalled"] if hasattr(es, "list_events") else []
if raw:
check("trace: событие в сторе с level", any("min" in str(a[3]) for a in raw), str(raw[:1]))
else:
# API стора без list_events — проверяем через проекцию не падая
check("trace: событие в сторе с level", True)
# ── 3. arguments: dict / JSON-строка / мусор ─────────────────────────────────
r = portal_mcp.call_tool("booking_slots", {"type": "call"})
check("arguments: dict как есть", json.loads(r)["ok"] is True)
r = portal_mcp.call_tool("booking_slots", '{"type": "call"}')
check("arguments: JSON-строка парсится", json.loads(r)["ok"] is True)
r = portal_mcp.call_tool("booking_slots", "не json")
check("arguments: мусор → error:", r.startswith("error:"), r[:100])
# ── 4. guardrail: деньги-тулы запрещены по умолчанию ─────────────────────────
for name in ("product_create", "product_set_mode"):
r = portal_mcp.call_tool(name, {})
check(f"guardrail: {name} запрещён", r.startswith("error:") and "запрещён" in r, r[:120])
os.environ["SIMBIOT_MCP_ALLOW_ALL"] = "1"
portal_mcp._tools_cache = None # сбросить кеш имён (фейк знает тул)
try:
# фейк не умеет product_create — подменим ответ, проверяем только проход guardrail
portal_mcp._rpc = lambda m, p, timeout=15: (SPEC if m == "tools/list" else
{"content": [{"text": '{"ok": true}'}]})
r = portal_mcp.call_tool("product_create", {})
check("guardrail: ALLOW_ALL пропускает", not r.startswith("error:"), r[:120])
finally:
del os.environ["SIMBIOT_MCP_ALLOW_ALL"]
portal_mcp._rpc = _fake_rpc
portal_mcp._tools_cache = None
# ── 5. неизвестный тул → error со списком ───────────────────────────────────
r = portal_mcp.call_tool("nope", {})
check("неизвестный тул → error со списком доступных",
r.startswith("error:") and "whoami" in r, r[:150])
# ── 6. сбой шлюза → error:, не рэйз ──────────────────────────────────────────
def _boom(method, params, timeout=15):
raise portal_mcp.McpError("HTTP 500 от шлюза")
portal_mcp._rpc = _boom
portal_mcp._tools_cache = None
r = portal_mcp.call_tool("whoami", {})
check("шлюз упал → error: (тик не валится)", r.startswith("error: HTTP 500"), r[:120])
# ── 7. нет токена → error:, не рэйз ─────────────────────────────────────────
os.environ.pop("SIMBIOT_PORTAL_MCP_TOKEN", None)
saved = portal_mcp._load_token
portal_mcp._load_token = lambda: "" # и файла нет
portal_mcp._tools_cache = None
try:
r = portal_mcp.call_tool("whoami", {})
check("нет токена → error: (тул не поднимается)",
r.startswith("error:") and "токен" in r, r[:150])
finally:
portal_mcp._load_token = saved
os.environ["SIMBIOT_PORTAL_MCP_TOKEN"] = "agt_test_token"
portal_mcp._rpc = _fake_rpc
# ── 8. обрезка длинного результата ───────────────────────────────────────────
portal_mcp._rpc = lambda m, p, timeout=15: (SPEC if m == "tools/list" else
{"content": [{"text": "x" * 20000}]})
portal_mcp._tools_cache = None
r = portal_mcp.call_tool("whoami", {})
check("обрезка результата",
len(r) <= portal_mcp.MAX_RESULT_CHARS + 100 and "обрезано" in r,
f"len={len(r)}")
portal_mcp._rpc = _fake_rpc
portal_mcp._tools_cache = None
# ── 9. тул тика: лимит 4 вызова ──────────────────────────────────────────────
from harness import _llm_worker
tool = _llm_worker._portal_mcp_tool_for("hour", "t-test-mcp-tool")
outs = [tool(tool_name, "{}") for tool_name in
("whoami", "whoami", "whoami", "whoami", "whoami")]
check("тул тика: первые 4 прошли", all(not o.startswith("error:") for o in outs[:4]),
str([o[:40] for o in outs[:4]]))
check("тул тика: 5-й — лимит", outs[4].startswith("лимит"), outs[4][:100])
# ── 10. путь токена: env сильнее схемы, изолят → secrets/ ────────────────────
os.environ["SIMBIOT_PORTAL_MCP_TOKEN_FILE"] = "/tmp/my-token"
check("paths: env TOKEN_FILE бьёт схему",
_paths.portal_mcp_token_path() == "/tmp/my-token")
del os.environ["SIMBIOT_PORTAL_MCP_TOKEN_FILE"]
check("paths: изолят → secrets/portal-mcp-token",
_paths.portal_mcp_token_path().endswith("secrets/portal-mcp-token"),
_paths.portal_mcp_token_path())
print()
print("FAILURES:", fails if fails else "нет — все проверки пройдены")
sys.exit(1 if fails else 0)

View file

@ -9,9 +9,8 @@
(нет ни реакции, ни второго события), TG-сбой — False без события;
- receive_message сохраняет message_id (нативный, не хэш-фолбэк);
- typing_keepalive: держит sendChatAction typing и останавливается по Event;
- run_level_tick: поднимает keepalive по TG-чатам инбокса ВЕСЬ тик, без
привязки к pending/seen (t-261010092931), исключает портал-чаты, при
пустом инбоксе — фолбэк на чат харнеса; гасит поток в finally (любой исход).
- run_level_tick: поднимает keepalive по TG-чатам неотвеченного инбокса,
исключает портал-чаты, гасит поток в finally (на любом исходе возврата).
"""
import json
import os
@ -174,9 +173,7 @@ try:
check("tick: worker упал → finally всё равно гасит keepalive",
ka_seen.get("stop") is not None and ka_seen.get("stop").is_set())
# t-261010092931: всё seen → «печатает…» всё равно горит весь тик
# (индикатор больше не зависит от pending: Alex видит «печатает…»
# всё время, пока LLM-тик работает)
# все сообщения уже seen → keepalive не поднимается вовсе
ka_seen.clear()
model2 = dict(model)
model2["inbox"] = {
@ -185,50 +182,7 @@ try:
}
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
run_level_tick(LEVELS["hour"], 4244, model2)
check("tick: всё seen → keepalive всё равно поднят по TG-чату",
ka_seen.get("ids") == [42], str(ka_seen))
check("tick: всё seen → finally погасил keepalive",
ka_seen.get("stop") is not None and ka_seen.get("stop").is_set())
# t-261010092931: пустой инбокс → фолбэк на последний живой TG-чат
# (_default_chat_id, личка Alex): индикатор горит и без входящих
ka_seen.clear()
model3 = dict(model)
model3["inbox"] = {}
_orig_default_chat = tg_inbox._default_chat_id
tg_inbox._default_chat_id = lambda: 42
try:
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
run_level_tick(LEVELS["hour"], 4245, model3)
finally:
tg_inbox._default_chat_id = _orig_default_chat
check("tick: пустой инбокс → keepalive по чату харнеса (фолбэк)",
ka_seen.get("ids") == [42], str(ka_seen))
check("tick: пустой инбокс → finally погасил keepalive",
ka_seen.get("stop") is not None and ka_seen.get("stop").is_set())
# инбокс без TG-чатов (только портал/tui) → тоже фолбэк на чат харнеса
ka_seen.clear()
model4 = dict(model)
model4["inbox"] = {"m-2": model["inbox"]["m-2"]}
tg_inbox._default_chat_id = lambda: 42
try:
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
run_level_tick(LEVELS["hour"], 4246, model4)
finally:
tg_inbox._default_chat_id = _orig_default_chat
check("tick: только портал-чаты → keepalive по фолбэку, портал исключён",
ka_seen.get("ids") == [42], str(ka_seen))
# фолбэк недоступен (нет известного чата) → keepalive не поднимается
ka_seen.clear()
tg_inbox._default_chat_id = lambda: None
try:
_ir_module.run_llm_tick = staticmethod(_FakeRunner.run_llm_tick)
run_level_tick(LEVELS["hour"], 4247, model3)
finally:
tg_inbox._default_chat_id = _orig_default_chat
check("tick: чат неизвестен → keepalive не поднимается", ka_seen == {}, str(ka_seen))
check("tick: всё seen → keepalive не поднимается", ka_seen == {}, str(ka_seen))
finally:
scheduler.es = _orig_es
tg_inbox.typing_keepalive = _orig_tg_typing_backup

View file

@ -1,174 +0,0 @@
# -*- coding: utf-8 -*-
"""t-261005135853: входящие documents/photo → на диск, путь в событии.
Покрытие:
1. unit: _file_ext — имя/mime/мусор/пусто;
2. e2e: MessageReceived(document) → getFile/download (моки) → файл в
inbox/files/<unique>.pdf, file_path (абсолютный) + file_name + size
в проекции inbox;
3. e2e: photo (3 размера) → .jpg самого большого размера;
4. идемпотентность: повторный апдейт не перекачивает (счётчик getFile);
5. сбой getFile (ok=False / HTTP 400) → file_path=None, входящее живо
(file_id остаётся для ручного достёгивания).
Запуск: ./venv/bin/python tests/test_tg_documents.py | pytest tests/test_tg_documents.py
Telegram-сеть мокнута (нет реальных file_id), стор — свой в /tmp.
"""
import io
import os
import sys
import urllib.error
# ── изоляция: свой стор и свой каталог файлов, прод не трогаем ──
_DST = "/tmp/simbiot-test-tg-docs.db"
_FILES = "/tmp/simbiot-test-tg-docs-files"
for p in (_DST, _DST + "-wal", _DST + "-shm"):
if os.path.exists(p):
os.unlink(p)
os.environ["SIMBIOT_DB"] = _DST
os.environ["SIMBIOT_INBOX_FILES_DIR"] = _FILES
sys.path.insert(0, "/root/simbiot-harness")
from harness import es, tg_inbox
fails = []
_calls = {"getFile": 0}
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, (f"| {extra}" if not cond else ""), flush=True)
if not cond:
fails.append(name)
# ── моки Telegram: getFile → file_path, urlopen → байты файла ──
class _Resp(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *a):
return False
def _fake_api(method, params, timeout=35):
if method == "getFile":
_calls["getFile"] += 1
return {"ok": True, "result": {
"file_id": params["file_id"],
"file_path": f"documents/{params['file_id']}.pdf"}}
return {"ok": True, "result": {}}
def _fake_urlopen(url, timeout=60):
if "getFile-fail" in url or "expired" in url:
raise urllib.error.HTTPError(url, 400, "Bad Request", {}, io.BytesIO(b""))
return _Resp(b"%PDF-1.4 fake-test-blob")
tg_inbox._api = _fake_api
tg_inbox._load_env = lambda: setattr(tg_inbox, "TG_TOKEN", "TEST:TOKEN") # токен мока
_real_urlopen = urllib.request.urlopen
urllib.request.urlopen = lambda url, timeout=60: _fake_urlopen(url, timeout)
es.init_db()
# ────────────────────────────── 1. _file_ext ────────────────────────────────
def test_file_ext():
check("ext из имени", tg_inbox._file_ext("Alfa-Bank_выписка.PDF") == "PDF"
or tg_inbox._file_ext("Alfa-Bank_выписка.PDF").lower() == "pdf",
tg_inbox._file_ext("Alfa-Bank_выписка.PDF"))
check("ext из mime", tg_inbox._file_ext("", "image/jpeg") == "jpg",
tg_inbox._file_ext("", "image/jpeg"))
check("мусор вырезан", tg_inbox._file_ext("a.tar.gz/../../x", "") in ("gz", "x", "bin"),
tg_inbox._file_ext("a.tar.gz/../../x"))
check("пусто → bin", tg_inbox._file_ext("", "") == "bin")
# ─────────────────────── 2. document → файл + путь в событии ────────────────
def test_document_e2e():
upd = {"update_id": 990001, "message": {
"message_id": 111, "date": 1791200000,
"from": {"id": 1, "first_name": "A"},
"chat": {"id": 1},
"document": {"file_id": "file-AAA", "file_unique_id": "AgADdoc1",
"file_name": "Alfa_Statement.pdf",
"mime_type": "application/pdf", "file_size": 123456},
"caption": "выписка октябрь",
}}
mid = tg_inbox.receive_message(upd)
p = f"{_FILES}/AgADdoc1.pdf"
check("файл в inbox/files", os.path.exists(p), p)
check("содержимое скачано", os.path.exists(p) and open(p, "rb").read() == b"%PDF-1.4 fake-test-blob")
m = es.run_projections()["inbox"][mid]
check("file_path в событии (абс)", m.get("file_path") == p, m.get("file_path"))
check("путь открывается напрямую", os.path.exists(m.get("file_path") or ""))
check("file_name в событии", m.get("file_name") == "Alfa_Statement.pdf")
check("file_size в событии", m.get("file_size") == 123456)
check("kind=document", m.get("kind") == "document")
check("caption в text", m.get("text") == "выписка октябрь")
return upd
# ───────────────────────────── 3. photo → .jpg ───────────────────────────────
def test_photo_e2e():
upd = {"update_id": 990002, "message": {
"message_id": 112, "date": 1791200001,
"from": {"id": 1, "first_name": "A"},
"chat": {"id": 1},
"photo": [
{"file_id": "ph-sm", "file_unique_id": "UPsm", "file_size": 100, "width": 90, "height": 90},
{"file_id": "ph-md", "file_unique_id": "UPmd", "file_size": 900, "width": 320, "height": 320},
{"file_id": "ph-lg", "file_unique_id": "UPlg", "file_size": 9900, "width": 1280, "height": 1280},
],
}}
mid = tg_inbox.receive_message(upd)
p = f"{_FILES}/UPlg.jpg"
check("фото: самый большой размер сохранён", os.path.exists(p), p)
m = es.run_projections()["inbox"][mid]
check("photo file_path", m.get("file_path") == p, m.get("file_path"))
check("photo kind", m.get("kind") == "photo")
check("photo file_size", m.get("file_size") == 9900)
return upd
# ─────────────────── 4. идемпотентность: повтор не качает ────────────────────
def test_no_redownload(upd):
before = _calls["getFile"]
mtime = os.path.getmtime(f"{_FILES}/AgADdoc1.pdf")
tg_inbox.receive_message(upd) # тот же update_id → тот же файл
check("getFile не рос при повторе", _calls["getFile"] == before,
f"{before} → {_calls['getFile']}")
check("файл не переписан", os.path.getmtime(f"{_FILES}/AgADdoc1.pdf") == mtime)
# ──────────────────── 5. сбой скачивания не валит приём ─────────────────────
def test_download_failed():
upd = {"update_id": 990003, "message": {
"message_id": 113, "date": 1791200002,
"from": {"id": 1, "first_name": "A"},
"chat": {"id": 1},
"document": {"file_id": "getFile-fail-expired", "file_unique_id": "ADdead",
"file_name": "old.pdf", "mime_type": "application/pdf"},
}}
mid = tg_inbox.receive_message(upd)
m = es.run_projections()["inbox"][mid]
check("file_path=None при сбое", m.get("file_path") is None, m.get("file_path"))
check("file_id сохранился (ручной путь)", m.get("file_id") == "getFile-fail-expired")
check("входящее живо (не упало)", bool(mid))
check("мусора .part не осталось", not any(f.endswith(".part") for f in os.listdir(_FILES)))
if __name__ == "__main__":
test_file_ext()
upd = test_document_e2e()
test_photo_e2e()
test_no_redownload(upd)
test_download_failed()
print(f"\n{'ALL PASS' if not fails else 'FAILURES: ' + ', '.join(fails)}")
sys.exit(1 if fails else 0)

View file

@ -1,229 +0,0 @@
# -*- coding: utf-8 -*-
"""t-261004160531: видео→текст в voice-пайплайне (регресс t-261004152714).
Покрытие:
1. unit: extract_audio_track — реальный ffmpeg на живых фикстурах
(mp4 с дорожкой → ogg/opus байты; mp4 без дорожки и мусор → None);
2. unit: роутинг kind=video ≤60с → сразу faster-whisper turbo
(webservice-ярус не зовётся, движок = large-v3-turbo);
3. unit: длинное видео (>60с) → обычная лестница (webservice base первым);
4. unit: контейнер без дорожки → MessageTranscribed(audio_track_failed);
5. e2e на фикстуре: MessageReceived(kind=video) → download (мок) →
ffmpeg-извлечение → ASR (мок) → транскрипт в проекции inbox;
идемпотентность повтора (второй вызов — из read model, без хождений).
Запуск: ./venv/bin/python tests/test_voice_video.py | pytest tests/test_voice_video.py
ASR-слои мокнуты (нет сети/моделей), ffmpeg — настоящий (он на хосте).
"""
import os
import subprocess
import sys
# ── изоляция: своя БД в /tmp, прод-стор не трогаем ──
_DST = "/tmp/simbiot-test-voice-video.db"
for p in (_DST, _DST + "-wal", _DST + "-shm"):
if os.path.exists(p):
os.unlink(p)
os.environ["SIMBIOT_DB"] = _DST
sys.path.insert(0, "/root/simbiot-harness")
from harness import es, tg_inbox
import harness.voice as voice
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "", flush=True)
if not cond:
fails.append(name)
raise AssertionError(f"{name} {extra}")
# ── фикстуры: генерируем ffmpeg'ом при запуске (детерминировано, без бинарников в репо) ──
_TMP = "/tmp/simbiot-test-voice-video"
os.makedirs(_TMP, exist_ok=True)
WITH_AUDIO = os.path.join(_TMP, "with_audio.mp4") # testsrc + sine 440Hz
NO_AUDIO = os.path.join(_TMP, "no_audio.mp4") # только видео
def _ff(args):
r = subprocess.run(["ffmpeg", "-y", "-v", "error"] + args,
capture_output=True, timeout=120)
if r.returncode != 0:
raise RuntimeError((r.stderr or b"").decode()[:300])
_ff(["-f", "lavfi", "-i", "testsrc=duration=2:size=128x96:rate=10",
"-f", "lavfi", "-i", "sine=frequency=440:duration=2",
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac",
"-shortest", WITH_AUDIO])
_ff(["-f", "lavfi", "-i", "testsrc=duration=2:size=128x96:rate=10",
"-c:v", "libx264", "-preset", "ultrafast", "-an", NO_AUDIO])
with open(WITH_AUDIO, "rb") as f:
VIDEO_WITH_AUDIO = f.read()
with open(NO_AUDIO, "rb") as f:
VIDEO_NO_AUDIO = f.read()
es.init_db()
# ────────────────────────── 1. extract_audio_track ──────────────────────────
def test_extract_audio_track_from_container():
"""mp4 с дорожкой → ogg/opus байты (заголовок OggS + OpusHead, звук есть)."""
track = voice.extract_audio_track(VIDEO_WITH_AUDIO)
check("контейнер с дорожкой → байты", bool(track), "None/пусто")
assert track[:4] == b"OggS", f"не ogg: {track[:8]!r}"
assert b"OpusHead" in track[:64], "не opus-кодек"
check("ogg/opus формат дорожки", True)
def test_extract_audio_track_no_track():
"""mp4 без аудио → None (ffmpeg ничего не вынул)."""
r = voice.extract_audio_track(VIDEO_NO_AUDIO)
check("контейнер без дорожки → None", r is None, f"получили {type(r)}")
r = voice.extract_audio_track(b"not a video at all")
check("мусор на входе → None (без исключения)", r is None, f"получили {type(r)}")
# ─────────────────────── 2-4. роутинг kind=video ────────────────────────────
def _video_msg(mid, duration):
"""MessageReceived c video-вложением в Telegram-формате."""
return {"update_id": int(mid.split("-")[1]), "message": {
"message_id": int(mid.split("-")[1]) % 100000, "date": 1780000000,
"chat": {"id": 42}, "from": {"id": 1, "username": "alex", "first_name": "Alex"},
"video": {"file_id": f"fid-{mid}", "file_unique_id": f"fuid-{mid}",
"duration": duration}}}
class _Rec:
"""Записываем вызовы ASR-слоёв и подменяем их ответами."""
def __init__(self, fw_text=None, ws_text=None):
self.calls = {"fw": [], "ws": [], "dl": []}
self._orig = {}
self._patch("_download_voice",
lambda fid, fuid="": (self.calls["dl"].append(fid) or VIDEO_WITH_AUDIO, None))
self._patch("_asr_faster_whisper",
lambda audio, fn: (self.calls["fw"].append(audio) or fw_text))
self._patch("_asr_webservice",
lambda audio, fn: (self.calls["ws"].append(audio) or ws_text))
def _patch(self, name, fn):
self._orig[name] = getattr(voice, name)
setattr(voice, name, fn)
def restore(self):
for k, v in self._orig.items():
setattr(voice, k, v)
self._orig = {}
def test_short_video_routes_to_turbo():
"""kind=video ≤60с: сразу faster-whisper turbo, webservice не зовётся."""
tg_inbox.receive_message(_video_msg("m-910001", 5))
rec = _Rec(fw_text="привет из видео")
# детерминированный маркер: сам ogg-заголовок между запусками ffmpeg
# не байт-стабилен — проверяем передачу ровно того, что вернул extract
marker = b"OggS-EXTRACTED-TRACK"
orig_extract = voice.extract_audio_track
voice.extract_audio_track = lambda c: (rec.calls.setdefault("ex", []).append(1) or marker)
try:
text, engine = voice.transcribe_message(
"m-910001", "fid-m-910001", "fuid-m-910001", duration=5, kind="video")
finally:
voice.extract_audio_track = orig_extract
rec.restore()
check("транскрипт получен", text == "привет из видео", f"text={text!r}")
check("движок = large-v3-turbo", engine == "faster-whisper-large-v3-turbo",
f"engine={engine!r}")
check("turbo-ярус вызван ровно 1 раз", len(rec.calls["fw"]) == 1,
f"{len(rec.calls['fw'])}")
check("webservice-ярус пропущен", len(rec.calls["ws"]) == 0,
f"{len(rec.calls['ws'])}")
check("в ASR ушла ИЗВЛЕЧЁННАЯ дорожка (не контейнер)",
rec.calls["fw"] and rec.calls["fw"][0] == marker, "байты != extract()")
check("extract вызван для видео", len(rec.calls.get("ex", [])) == 1)
m = es.run_projections()["inbox"]["m-910001"]
check("проекция: transcript в read model", m["transcript"] == "привет из видео")
check("проекция: text = транскрипт", m["text"] == "привет из видео")
check("проекция: без transcribe_reason", not m.get("transcribe_reason"))
def test_long_video_uses_tier_ladder():
"""kind=video >60с: сначала webservice base (обычная лестница)."""
tg_inbox.receive_message(_video_msg("m-910002", 120))
rec = _Rec(ws_text="длинное видео base")
try:
text, engine = voice.transcribe_message(
"m-910002", "fid-m-910002", "fuid-m-910002", duration=120, kind="video")
finally:
rec.restore()
check("транскрипт из base-яруса", text == "длинное видео base", f"text={text!r}")
check("движок = whisper-asr-base", engine == "whisper-asr-base", f"engine={engine!r}")
check("webservice вызван", len(rec.calls["ws"]) == 1, f"{len(rec.calls['ws'])}")
check("turbo не понадобился", len(rec.calls["fw"]) == 0, f"{len(rec.calls['fw'])}")
def test_video_without_track_marks_failure():
"""Дорожки нет → MessageTranscribed(reason=audio_track_failed), движок None."""
tg_inbox.receive_message(_video_msg("m-910003", 7))
rec = _Rec(fw_text="не должен вызываться")
rec._patch("_download_voice",
lambda fid, fuid="": (self_dl := rec.calls["dl"].append(fid) or VIDEO_NO_AUDIO, None))
try:
text, engine = voice.transcribe_message(
"m-910003", "fid-m-910003", "fuid-m-910003", duration=7, kind="video")
finally:
rec.restore()
check("результат — отказ (None)", text is None, f"text={text!r}")
m = es.run_projections()["inbox"]["m-910003"]
check("reason=audio_track_failed", m.get("transcribe_reason") == "audio_track_failed",
f"reason={m.get('transcribe_reason')!r}")
check("ASR-слои не звались", not rec.calls["fw"] and not rec.calls["ws"])
# ──────────────────────────── 5. e2e на фикстуре ────────────────────────────
def test_e2e_video_message_to_transcript():
"""Полный путь: приём видео-сообщения → транскрипция → идемпотентность."""
mid = tg_inbox.receive_message(_video_msg("m-910004", 12))
rec = _Rec(fw_text="текст из видео e2e")
try:
text, engine = voice.transcribe_message(
mid, "fid-m-910004", "fuid-m-910004", duration=12, kind="video")
finally:
rec.restore()
check("e2e: транскрипт вернулся", text == "текст из видео e2e", f"text={text!r}")
m = es.run_projections()["inbox"][mid]
check("e2e: kind=video в проекции", m["kind"] == "video", f"kind={m['kind']!r}")
check("e2e: движок записан", m["engine"] == "faster-whisper-large-v3-turbo")
check("e2e: минуты увидат текст", m["text"] == "текст из видео e2e", f"text={m['text']!r}")
# идемпотентность: повтор берёт транскрипт из read model — без download/ASR
rec2 = _Rec(fw_text="другой текст (не должен использоваться)")
try:
text2, engine2 = voice.transcribe_message(
mid, "fid-m-910004", "fuid-m-910004", duration=12, kind="video")
finally:
rec2.restore()
check("повтор: транскрипт из кэша read model", text2 == "текст из видео e2e",
f"text={text2!r}")
check("повтор: без повторного download", not rec2.calls["dl"], "качал заново")
check("повтор: без повторного ASR", not rec2.calls["fw"] and not rec2.calls["ws"])
# ────────────────────────────── plain-run режим ──────────────────────────────
if __name__ == "__main__":
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for t in tests:
try:
t()
except AssertionError:
pass
print(f"\n{'='*60}")
print(f"{'ALL OK' if not fails else 'FAILED: ' + ', '.join(fails)} ({len(tests)} тестов)")
sys.exit(1 if fails else 0)

View file

@ -1,290 +0,0 @@
#!/usr/bin/env python3
"""gcal.py — Google Calendar для харнеса: create-event / list / check (t-261008155436).
Команда из TG «сделай звонок на X с Владой» → минутный тик грузит скилл
skills/gcal/SKILL.md → зовёт этот инструмент → событие в календаре Alex
с приглашением гостям, слот только на рабочий день (цикл «3 через 3»).
Автоматизация (service account, JWT → access token, REST v3 — без google-клиентов):
1) Alex: console.cloud.google.com → новый проект → включить Calendar API →
«Service accounts» → создать → ключ JSON → прислать файл.
2) В календаре Google: Настройки → «Настройки для моего календаря» →
«Предоставить доступ» → добавить client_email ключа с ролью
«Вносить изменения» (make changes to events).
3) Положить ключ: /root/simbiot-harness/secrets/gcal-service-account.json
(или env GCAL_SERVICE_ACCOUNT_FILE). Файл 0600.
4) ./gcal.py check — готовность. Готово: скилл создаёт события без человека.
Если ключа нет — работают --dry-run, check, next-workdays (планирование слотов).
"""
from __future__ import annotations
import argparse
import base64
import json
import os
import sys
import time
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
HERE = Path(__file__).resolve().parent
REPO = HERE.parent
DEFAULT_KEY = REPO / "secrets" / "gcal-service-account.json"
TOKEN_CACHE = Path("/tmp/.gcal_access_token.json")
# ── График Alex: цикл «3 через 3» ─────────────────────────────────────────────
# docs/alex-schedule.md: 3 рабочих, 3 выходных. Якорь выведен из m-516173591
# (2026-10-03: «сегодня и завтра рабочие, далее 3 выходных» → 10-02 первый
# рабочий цикла). Проверка: 10-08 = (10-08−10-02)=6 → 6%6=0 → рабочий (совпало
# с «сегодня/завтра рабочие» от 10-08). Сдвиг цикла: env GCAL_WORK_ANCHOR=YYYY-MM-DD
ANCHOR = os.environ.get("GCAL_WORK_ANCHOR", "2026-10-02")
PERIOD = 6
WORK_DAYS = {0, 1, 2} # смещения внутри цикла: 0,1,2 — рабочие; 3,4,5 — выходные
MIN_LEAD_MIN = int(os.environ.get("GCAL_MIN_LEAD_MIN", "120")) # анти-паттерн рекордера: не позже 2ч до начала
def _anchor() -> date:
return datetime.strptime(ANCHOR, "%Y-%m-%d").date()
def is_workday(d: date) -> bool:
return ((d - _anchor()).days % PERIOD) in WORK_DAYS
def next_workdays(n: int = 6, from_day: date | None = None) -> list[date]:
d = from_day or date.today()
out: list[date] = []
while len(out) < n:
if is_workday(d):
out.append(d)
d += timedelta(days=1)
return out
# ── Креды и токен (service account → JWT → access token) ─────────────────────
def _key_path(explicit: str | None) -> Path:
p = Path(explicit or os.environ.get("GCAL_SERVICE_ACCOUNT_FILE") or DEFAULT_KEY)
return p
def load_key(explicit: str | None = None) -> dict | None:
p = _key_path(explicit)
if not p.exists():
return None
return json.loads(p.read_text())
def _b64(o: dict | bytes) -> str:
raw = json.dumps(o).encode() if isinstance(o, dict) else o
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
def get_access_token(key: dict, force: bool = False) -> str:
"""JWT (RS256) → Google OAuth2 → access token (кэш ~50 мин)."""
if not force and TOKEN_CACHE.exists():
try:
c = json.loads(TOKEN_CACHE.read_text())
if c.get("exp", 0) > time.time() + 120 and c.get("iss") == key["client_email"]:
return c["token"]
except Exception:
pass
import httpx # уже в venv харнеса
now = int(time.time())
claim = {
"iss": key["client_email"],
"scope": "https://www.googleapis.com/auth/calendar",
"aud": "https://oauth2.googleapis.com/token",
"iat": now,
"exp": now + 3600,
}
head = {"alg": "RS256", "typ": "JWT"}
signing_input = f"{_b64(head)}.{_b64(claim)}".encode()
from cryptography.hazmat.primitives import hashes, serialization
pk = serialization.load_pem_private_key(key["private_key"].encode(), password=None)
sig = pk.sign(signing_input, hashes.SHA256())
jwt = f"{signing_input.decode()}.{_b64(sig)}"
r = httpx.post(
"https://oauth2.googleapis.com/token",
data={"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": jwt},
timeout=20,
)
if r.status_code != 200:
raise RuntimeError(f"token exchange failed: {r.status_code} {r.text[:200]}")
tok = r.json()
TOKEN_CACHE.write_text(json.dumps({"token": tok["access_token"], "exp": now + tok.get("expires_in", 3600),
"iss": key["client_email"]}))
TOKEN_CACHE.chmod(0o600)
return tok["access_token"]
def _api(method: str, path: str, key: dict, body: dict | None = None, params: dict | None = None) -> dict:
import httpx
tok = get_access_token(key)
r = httpx.request(method, f"https://www.googleapis.com/calendar/v3{path}",
headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"},
json=body, params=params or {}, timeout=25)
if r.status_code >= 400:
raise RuntimeError(f"calendar api {r.status_code}: {r.text[:300]}")
return r.json()
# ── Действия ──────────────────────────────────────────────────────────────────
def cmd_check(args) -> int:
print(f"anchor (цикл 3 через 3): {ANCHOR} (env GCAL_WORK_ANCHOR для сдвига)")
today = date.today()
print(f"сегодня {today}: {'РАБОЧИЙ' if is_workday(today) else 'выходной'}")
print("ближайшие рабочие:", ", ".join(d.isoformat() for d in next_workdays(4)))
key = load_key(args.key)
if not key:
p = _key_path(args.key)
print(f"\nключ: НЕТ ({p})")
print("шаги: Google Cloud → Calendar API → service account → JSON-ключ →")
print(" доступ к календарю Alex для client_email (make changes) →")
print(f" положить в {p} (0600). Затем: gcal.py check && gcal.py list")
return 1
print(f"\nключ: OK ({key.get('client_email', '?')})")
try:
cal = _api("GET", f"/calendars/{args.calendar}", key)
print(f"календарь: {cal.get('summary')} ({cal.get('id')}) — доступ есть")
return 0
except Exception as e:
print(f"доступ к календарю: НЕ УДАЛСЯ — {e}")
print("→ дайте client_email право «Вносить изменения» в настройках календаря")
return 2
def _guard(slot_start: datetime) -> tuple[bool, str]:
d = slot_start.date()
if not is_workday(d):
nxt = next_workdays(1, from_day=d + timedelta(days=1))
sug = ", ".join(x.isoformat() for x in nxt)
return False, f"{d.isoformat()} — выходной по циклу Alex «3 через 3». Ближайшие рабочие: {sug}"
lead = (slot_start - datetime.now(timezone.utc)).total_seconds() / 60
if lead < MIN_LEAD_MIN:
return False, (f"до начала {int(lead)} мин < {MIN_LEAD_MIN} (анти-паттерн рекордера: "
f"t-261008155708 — встречи <2 мин до старта не заходятся). Возьмите слот позже.")
return True, "ok"
def _parse_slot(day: str, hhmm: str, dur: int, tz_name: str) -> datetime:
"""Локальное время дня в tz_name → момент UTC (API Google требует offset)."""
from zoneinfo import ZoneInfo
d = date.fromisoformat(day)
hh, mm = map(int, hhmm.split(":"))
return datetime(d.year, d.month, d.day, hh, mm, tzinfo=ZoneInfo(tz_name)).astimezone(timezone.utc)
def cmd_create(args) -> int:
start = _parse_slot(args.day, args.time, args.duration, args.timezone)
ok, msg = _guard(start)
if not ok and not args.force:
print(f"ОТКАЗ: {msg}")
return 3
if not ok:
print(f"ВНИМАНИЕ (force): {msg}")
end = start + timedelta(minutes=args.duration)
guests = [g.strip() for g in (args.guests or "").split(",") if g.strip()]
event = {
"summary": args.title,
"description": args.description or "Создано харнесом Simbiot (skills/gcal, t-261008155436-a658e9)",
"start": {"dateTime": start.isoformat(), "timeZone": args.timezone},
"end": {"dateTime": end.isoformat(), "timeZone": args.timezone},
"attendees": [{"email": g} for g in guests],
"reminders": {"useDefault": False, "overrides": [
{"method": "popup", "minutes": 30}, {"method": "popup", "minutes": 10},
]},
}
if args.dry_run:
print("DRY-RUN (в Google не писали). Событие:")
print(json.dumps(event, ensure_ascii=False, indent=1))
print(f"guard: {msg}; слот рабочий: {ok}")
return 0
key = load_key(args.key)
if not key:
print("нет сервис-ключа Google — см. gcal.py check (шаги настройки)")
return 1
made = _api("POST", f"/calendars/{args.calendar}/events", key, body=event,
params={"sendUpdates": "all"})
print(f"СОЗДАНО: {made.get('htmlLink')}")
print(f" {made.get('summary')} · {start:%Y-%m-%d %H:%M} UTC ({args.timezone}) · "
f"гости: {', '.join(guests) or '—'}")
return 0
def cmd_list(args) -> int:
key = load_key(args.key)
if not key:
print("нет сервис-ключа Google — см. gcal.py check")
return 1
tmin = datetime.now(timezone.utc).isoformat()
data = _api("GET", f"/calendars/{args.calendar}/events", key,
params={"timeMin": tmin, "maxResults": args.limit, "orderBy": "startTime",
"singleEvents": "true"})
evs = data.get("items", [])
if not evs:
print("ближайших событий нет")
return 0
for e in evs:
when = (e.get("start", {}).get("dateTime") or e.get("start", {}).get("date", ""))[:16]
print(f" {when} {e.get('summary', '(без названия)')}")
return 0
def cmd_next(args) -> int:
for d in next_workdays(args.count):
off = ((d - _anchor()).days % PERIOD)
print(f" {d.isoformat()} рабочий #{off + 1}/3")
return 0
def main() -> int:
ap = argparse.ArgumentParser(description="Google Calendar для харнеса (create-event по команде из TG)")
sub = ap.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("check", help="готовность: ключ, доступ, график")
c.add_argument("--key", help="путь к JSON сервис-аккаунта")
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
c.set_defaults(fn=cmd_check)
c = sub.add_parser("create", help="создать событие (слот только рабочий день)")
c.add_argument("--title", required=True)
c.add_argument("--day", required=True, help="YYYY-MM-DD")
c.add_argument("--time", required=True, help="HH:MM (UTC)")
c.add_argument("--duration", type=int, default=30, help="минуты")
c.add_argument("--guests", default="", help="email через запятую (приглашения)")
c.add_argument("--description", default="")
c.add_argument("--timezone", default=os.environ.get("GCAL_TZ", "UTC"))
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
c.add_argument("--key")
c.add_argument("--dry-run", action="store_true", help="показать событие, в Google не писать")
c.add_argument("--force", action="store_true", help="игнорировать guard (осознанно)")
c.set_defaults(fn=cmd_create)
c = sub.add_parser("list", help="ближайшие события")
c.add_argument("--limit", type=int, default=10)
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
c.add_argument("--key")
c.set_defaults(fn=cmd_list)
c = sub.add_parser("next-workdays", help="ближайшие рабочие дни (цикл 3 через 3)")
c.add_argument("count", nargs="?", type=int, default=6)
c.set_defaults(fn=cmd_next)
a = ap.parse_args()
try:
return a.fn(a)
except Exception as e: # noqa: BLE001 — инструмент отвечает кодом, не трейсбеком
print(f"ОШИБКА: {type(e).__name__}: {e}")
return 4
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,4 +0,0 @@
{
"jdkPath": "/usr/lib/jvm/java-17-openjdk-amd64",
"androidSdkPath": "/opt/android-sdk"
}

View file

@ -1,14 +0,0 @@
{
"permission": { "edit": "allow", "bash": "allow", "webfetch": "allow" },
"share": "disabled",
"provider": {
"simbiot": {
"npm": "@ai-sdk/openai-compatible",
"name": "Simbiot Router (cil.by)",
"options": { "baseURL": "https://router.cil.by/v1" },
"models": {
"gpt-5.2": {}, "claude-sonnet-4.6": {}, "gpt-4.1": {}, "o4-mini": {}
}
}
}
}

View file

@ -1,8 +0,0 @@
{
"permission": {
"edit": "allow",
"bash": "allow",
"webfetch": "allow"
},
"share": "disabled"
}

View file

@ -1,2 +0,0 @@
export OPENAI_API_KEY=sk-9ffbac9384bc446eab3bbe58a4a98dc3f4b2347d341caf51a34ce93149fd8c57f1eff2065f89025e
export OPENAI_BASE_URL=https://router.cil.by/v1