/* global React, ReactDOM, DesignCanvas, DCSection, DCArtboard, DCPostIt */ const { useState } = React; // ---- white-label brand (M1) ----------------------------------------------- // The portal is baked to static HTML with window.__BRAND injected into // at build time from brand.config.json (see scripts/brand-client.mjs). Every // brand-tunable literal below reads from it, falling back to the current EdgeLab // Space values so the raw prototype (opened without an injected __BRAND) still // renders standalone. Swapping brand.config.json white-labels the whole UI. const BRAND = (typeof window !== "undefined" && window.__BRAND) || {}; const BRAND_NAME = BRAND.brandName || "EdgeLab Space"; // Non-breaking-space variants so a multi-word brand name never wraps mid-name — // these reproduce the original `EdgeLab Space` / `EDGELAB SPACE` // literals (U+00A0 serializes back to   in the prerendered HTML). const BRAND_NB = BRAND_NAME.replace(/ /g, "\u00A0"); const BRAND_UPPER = BRAND_NAME.toUpperCase(); const BRAND_UPPER_NB = BRAND_UPPER.replace(/ /g, "\u00A0"); const FUNNEL = BRAND.funnel || {}; // mode:"none" removes every community CTA / promo banner from the UI. const FUNNEL_ON = (FUNNEL.mode || "community") !== "none"; const CTA_TEXT = FUNNEL.ctaText || "Вступить в сообщество"; const FUNNEL_URL = (FUNNEL.targetUrl || "https://edgelab.space").replace(/\/+$/, ""); const FUNNEL_OFFER = FUNNEL.offerText || "Закрытое AI-комьюнити: библиотека скиллов, живой чат, эфиры по средам."; // Home-hero niche texts (white-label M1 step 5) — the niche layer of the home // page reads from config; EdgeLab literals are the fallbacks. const HOME = BRAND.home || {}; const HOME_EYEBROW = HOME.eyebrow || "AI ПОРТАЛ"; const HOME_TITLE = HOME.title || "Статьи и гайды по трендовым AI решениям"; const HOME_DESC = HOME.description || "Разбираю на практике Claude Code, дизайн с AI, базы знаний и автоматизацию – живым языком, без сухих инструкций."; const HOME_CTA_TEXT = HOME.ctaText || "Начать внедрение"; const HOME_CTA_HREF = HOME.ctaHref || "/guides/kak-vnedrit-ai-agentov/"; const HOME_HERO_IMG = HOME.heroImage || "/covers/_home-hero-dashi.webp"; const AUTHOR = BRAND.author || {}; const AUTHOR_NAME = AUTHOR.name || "Даши Ешиев"; const LEGAL_NAME = AUTHOR.legal || "EdgeLab Space"; const LEGAL_ENTITY = AUTHOR.legalEntity || "ИП ЕШИЕВ ЦЫРЕН-ДАШИ БУЛАТОВИЧ · ИНН 032315681243"; // Community-landing apex host — the click-time UTM forwarder rewrites only links // that point here. Derived from the funnel target so white-labels work too. const FUNNEL_HOST = (() => { try { return new URL(FUNNEL_URL).hostname.replace(/^www\./, ""); } catch (_) { return "edgelab.space"; } })(); // Deep-link attribution: Telegram ignores utm_* on t.me links — only ?start= // reaches the bot. Encode source+placement as journal_. The click-time handler // (wireStartAttribution) appends the article slug on /guides// pages → per-article. const startLink = (place) => `${FUNNEL_URL}?start=journal_${place}`; // Per-article attribution (2026-07-27): a bot CTA carries a static ?start=journal_ // (place = hdr/ftr/ban/abt). When the click happens on a /guides// page, append the // slug → ?start=journal__, so the bot can log WHICH article converted. Telegram // passes the whole start payload to /start; utm_* on t.me links is ignored, so we drop it. // Click-time rewrite is prerender/hydration-safe (mutates href on click, no SSR markup change). // No-JS clicks keep the place-level code. Payload stays ASCII [A-Za-z0-9_-], < 64 chars. function wireStartAttribution() { document.addEventListener("click", (e) => { const a = e.target && e.target.closest && e.target.closest("a[href]"); if (!a) return; let u; try { u = new URL(a.href, location.origin); } catch (_) { return; } if (u.hostname.replace(/^www\./, "") !== FUNNEL_HOST) return; // only the bot funnel host const start = u.searchParams.get("start"); const m = start && start.match(/^journal_([a-z]+)$/); // only enrich bare journal_ codes if (!m) return; const path = location.pathname.replace(/\/+$/, ""); const gm = path.match(/\/guides\/([^/]+)/); const page = gm ? gm[1] : (path === "" || /\/index\.html$/.test(path) ? "home" : (path.split("/").pop() || "home").replace(/\.html$/, "")); u.searchParams.set("start", `journal_${m[1]}_${page}`); a.href = u.toString(); }, true); } try { if (typeof window !== "undefined" && window.document) { wireStartAttribution(); } } catch (_) {} // M-theme: persist the chosen theme across pages. Default light; dark sticks. // getStoredTheme seeds React state from localStorage; applyTheme mirrors the // choice onto (so html/body bg is dark too — see CSS) // + saves it; flipTheme toggles. An inline script applies the html class // BEFORE paint to avoid a light flash (FOUC) on stored-dark navigations. function getStoredTheme(fallback) { try { const v = window.localStorage.getItem("gfTheme"); if (v === "dark" || v === "light") return v; } catch (e) { /* private mode / no storage */ } // нет явного выбора → следуем дню/ночи устройства (тёмный режим телефона обычно // включается по расписанию вечером) — авто-выбор темы по времени суток try { if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark"; } catch (e) { /* noop */ } return fallback || "light"; } function applyTheme(t) { try { const d = document.documentElement; if (t === "dark") d.classList.add("theme-dark"); else d.classList.remove("theme-dark"); window.localStorage.setItem("gfTheme", t); } catch (e) { /* noop */ } } function flipTheme(setTheme) { // Source of truth = the flag (drives html/body bg in CSS). // applyTheme is a side effect (DOM class + localStorage) — it MUST run outside the // setState updater. Running it inside made React's double-invoke (StrictMode) toggle // the html class twice → it desynced from React state → dark content over a light // html/body = white corner on overscroll after dark→light→dark. Flip off the DOM. try { const cur = document.documentElement.classList.contains("theme-dark") ? "dark" : "light"; const n = cur === "dark" ? "light" : "dark"; applyTheme(n); setTheme(n); } catch (e) { setTheme((t) => (t === "dark" ? "light" : "dark")); } } /* ---------- logo + header ---------- */ function CopyIcon() { return ( ); } function MoonIcon() { return ( ); } function SunIcon() { return ( ); } function ThemeToggle({ night, onClick }) { return ( ); } function Logo() { // clicking the logo (header/footer/drawer) goes to the home page. The two-part // wordmark is derived from the brand name (first word + the rest), so a // white-label brand renders its own name; EdgeLab Space → «edgelab ▪ space». const parts = BRAND_NAME.split(/\s+/); const a = (parts[0] || BRAND_NAME).toLowerCase(); const b = parts.slice(1).join(" ").toLowerCase(); return ( {a}{b && }{b && {b}} ); } // Author byline under the title (guides + articles), Substack-style author header. // `meta` = optional read-time/date shown after the author («Даши Ешиев · 15 мин чтения»). function Byline({ meta }) { return ( {TG_NAME} {AUTHOR_NAME}{meta ? " · " + meta : ""} ); } const TG = (BRAND.telegram && BRAND.telegram.channelUrl) || "https://t.me/dashi_agent"; const TG_NAME = (BRAND.telegram && BRAND.telegram.channelName) || "Записки AI-энтузиаста"; // funnel docs + landing anchors — готовые ссылки с edgelab.space (footer) const DOC_PRIVACY = (FUNNEL.docs && FUNNEL.docs.privacy) || "https://drive.google.com/file/d/1hUEZbJBCdTQcpBqy_OAd8EupZwY9Xpx1/view"; const DOC_OFFER = (FUNNEL.docs && FUNNEL.docs.offer) || "https://drive.google.com/file/d/1i6XeY6SMcuA4uYYxBVpxEPN_EI_AeIH6/view"; const ELS_STREAMS = `${FUNNEL_URL}/#streams`; const ELS_COMMUNITY = `${FUNNEL_URL}/#community`; function TelegramButton() { return ( Телеграм-канал ); } /* ---------- portal navigation ---------- */ // Absolute clean URLs — work from any page depth / trailing-slash / device-served // path. No .html, no relative ../ juggling (that broke styling on clean URLs). const PAGES = { home: "/", library: "/library/", articles: "/#articles", claude: "/claude-code/", hermes: "/hermes/", codex: "/codex/", about: "/about/", guide: "/guide/" }; const GF_BASE = ""; const GUIDE_HREF = (slug) => `/guides/${slug}/`; const P = (key) => PAGES[key]; const NAV = [["home", "Главная"], ["library", "Гайды"], ["articles", "Статьи"], ["about", "О проекте"]]; function NavLinks({ cur }) { return ( ); } function JoinBtn() { if (!FUNNEL_ON) return null; return ({CTA_TEXT} ); } function MobileMenuPanel({ cur, open, onClose }) { return (
e.stopPropagation()}>
{FUNNEL_ON && }
); } function PortalHead({ cur, mobile, night, onToggle }) { const [menu, setMenu] = useState(false); return (
{!mobile && }
{!mobile && FUNNEL_ON && {CTA_TEXT} } {mobile && }
{mobile && setMenu(false)} />}
); } function Header({ dark, util, search = false, night, onToggle, cur = "claude" }) { return (
{search &&
Поиск по гайдам⌘K
}
{FUNNEL_ON && {CTA_TEXT} }
{util && (
ДОСТУП К ЗАБЛОКИРОВАННОМУ· YouTube· WhatsApp· инструкции 2026
)}
); } /* ---------- banners ---------- */ // VOX: EdgeLab-баннеры про «AI-комьюнити/эфиры» убраны. Мягкий заход в продукт — // только внутри статей (в тексте) + одна кнопка в шапке. function BannerStrip() { return null; } function BannerCard() { return null; } function BannerDark() { return null; } /* ---------- sidebar ---------- */ function Sidebar({ mini, group, items, active }) { const list = items || ["Введение в Claude Code", "Настройка проекта с нуля", "MCP-серверы и память", "Автоматизация видео", "Архитектура памяти агента"]; const act = active == null ? 1 : active; return (
{group || "Гайды"}
{list.map((it, i) => { const label = typeof it === "string" ? it : it.t; const slug = typeof it === "string" ? null : it.slug; const cls = "gp-side-item" + (i === act ? " active" : ""); return slug ? {label} : {label}; })} {/* VOX: убран EdgeLab-блок «Что внутри Space» */}
); } /* ---------- end CTA ---------- */ // VOX: EdgeLab community-CTA в конце статьи убран. Заход в продукт — мягко, внутри текста. function CtaDark() { return null; } function CtaLime() { return null; } /* ---------- rich content blocks ---------- */ function Callout({ children, label }) { return (
{label || "СОВЕТ"}

{children}

); } function FlowChart({ vert, nodes, caption }) { const ns = nodes || [ { l: "ПРОМПТ", s: "твой запрос" }, { l: "CLAUDE CODE", s: "агент", accent: true }, { l: "MCP", s: "filesystem · git" }, { l: "РЕЗУЛЬТАТ", s: "правки в коде" }, ]; return (
{ns.map((n, i) => (
{n.l} {n.s}
{i < ns.length - 1 &&
}
))}
{caption || "Схема: путь от запроса до правок в репозитории"}
); } function VideoEmbed({ title }) { return (
YouTube
{title || "Инструкция · VOX"}
Видео с YouTube встраивается в статью, 16:9, грузится по клику
); } function StepsGuide({ steps }) { const list = steps || [ { t: "Установи Claude Code", d: "Один пакет и вход по ключу – пара минут." }, { t: "Опиши проект в CLAUDE.md", d: "Правила, стек и доверенные команды в одном файле." }, { t: "Подключи MCP-серверы", d: "filesystem и git закрывают большинство задач." }, ]; return (
{list.map((s, i) => (
{String(i + 1).padStart(2, "0")}
{s.t}

{s.d}

))}
); } /* ---------- agent-native / SEO blocks (Layer 1) ---------- */ function Tldr({ text }) { return (
Коротко

{text || "Если совсем коротко: я завёл один файл с описанием проекта и парой доверенных команд — этого хватило, чтобы агент перестал гадать. Всё остальное я добавлял по мере надобности."}

); } function TestedBadge({ text }) { return (
{text || "Протестировано на Claude Code v1.2 · 12 июня 2026"}
); } function Faq({ qa }) { const list = qa || [ ["Нужно ли уметь программировать?", "Нет. Claude Code берёт рутину на себя – но базовое понимание проекта помогает направлять агента."], ["Какие MCP-серверы ставить первыми?", "filesystem и git закрывают большинство задач. Остальное добавляй под конкретную задачу, а не заранее."], ["Это работает с Codex?", "Да, подход тот же: правила проекта плюс доверенные команды. Отличаются детали запуска."], ]; return (

Частые вопросы

{list.map(([q, a], i) => (
{q}

{a}

))}
); } function Changelog({ items }) { const list = items || [ ["12.06.2026", "Обновлено под Claude Code v1.2, добавлен раздел про MCP-память."], ["02.05.2026", "Добавлена блок-схема потока и видео-разбор."], ["18.04.2026", "Первая публикация."], ]; return (

История изменений

    {list.map(([d, t], i) => (
  • {d}{t}
  • ))}
); } /* ---------- real guides data (from MDX via scripts/build-guides-data.mjs) ---------- */ /* window.__GF_GUIDES / __GF_GUIDE_BY_SLUG are injected by guides-data.js (loaded before this file). Each entry already carries title/desc/tldr/tested/meta/level/ min/steps/outcome/toc/bodyHtml. We adapt them to the article shape the design engine expects (side nav, docsnav prev/next, dangerouslySetInnerHTML body). */ const GF_GUIDES = (typeof window !== "undefined" && window.__GF_GUIDES) || []; const GF_BY_SLUG = (typeof window !== "undefined" && window.__GF_GUIDE_BY_SLUG) || {}; // Category → display name + the guides that belong to it (preserve source order). const GUIDE_CATS = [ { id: "vpn", name: "Обход блокировок" }, { id: "dostup", name: "Доступ к сервисам" }, { id: "vybor", name: "Выбор VPN" }, ]; function guidesInCat(catId) { return GF_GUIDES.filter((g) => g.cat === catId); } // Build the full article object the design's / renders. function guideToArticle(g) { if (!g) return null; const sibs = guidesInCat(g.cat); const idx = sibs.findIndex((s) => s.slug === g.slug); const prev = idx > 0 ? sibs[idx - 1] : (sibs.length > 1 ? sibs[sibs.length - 1] : g); const next = idx < sibs.length - 1 ? sibs[idx + 1] : (sibs.length > 1 ? sibs[0] : g); return { slug: g.slug, cur: g.cat === "claude" ? "claude" : "library", crumbMid: g.catName, type: g.type || "guide", cover: g.cover || "", title: g.title, desc: g.desc, tldr: g.tldr, tested: g.tested, meta: g.meta, level: g.level, side: { group: `${g.catName} · ${sibs.length}`, items: sibs.map((s) => ({ t: s.title, slug: s.slug })), active: Math.max(0, idx), }, toc: g.toc && g.toc.length ? g.toc : ["Введение"], docsnav: [prev.title, next.title], docsnavSlugs: [prev.slug, next.slug], steps: g.steps || [], outcome: g.outcome || "", bodyHtml: g.bodyHtml || "", // «Похожие материалы» (recirculation block) — resolve related slugs → full guides. related: (g.related || []).map((s) => GF_BY_SLUG[s]).filter(Boolean), }; } const REAL_ARTICLES = {}; for (const g of GF_GUIDES) REAL_ARTICLES[g.slug] = guideToArticle(g); // Pick representative guides for the three category landing pages. function firstInCat(catId, fallbackSlug) { const list = guidesInCat(catId); return (list[0] && list[0].slug) || fallbackSlug; } /* ---------- content body ---------- */ const ARTICLES = { claude: { cur: "claude", crumbMid: "Claude Code", title: "Как я собрал рабочее окружение за один вечер", desc: "История о том, как Claude Code из красивой игрушки в демо превратился в инструмент, которому я доверяю реальную работу. Без волшебства — только то, что у меня действительно сработало.", tldr: "Если совсем коротко: я завёл один файл с описанием проекта и парой доверенных команд — этого хватило, чтобы агент перестал гадать. Всё остальное добавлял по мере надобности.", tested: "Протестировано на Claude Code v1.2 · 12 июня 2026", meta: "ОБНОВЛЕНО 12 ИЮНЯ 2026 · 9 МИН ЧТЕНИЯ", side: { group: "Claude Code · 6", items: ["Введение в Claude Code", "Рабочее окружение за вечер", "MCP-серверы и память", "Доверенные команды", "Большой репозиторий", "Память агента"], active: 1 }, toc: ["Агент и контекст", "Инструменты и привычки", "Доверие шаг за шагом", "Обратная связь"], docsnav: ["Введение в Claude Code", "MCP-серверы и память"], intro: (

С чего всё началось

Прошло чуть больше полугода с тех пор, как Claude Code появился в моей работе — и за это время он научил меня куче вещей. Не он сам, конечно, а сам процесс: я набил столько шишек, что грех не поделиться.

Главное, что я понял: агенты вроде Claude Code или Hermes падают не на интеллекте, а на архитектуре. Чаще всего у меня ломалось не потому, что модель «тупая», а потому что я не дал ей контекст, и инструменты дрались между собой.

Ниже — ключевые уроки, которые я вынес за это время, чтобы вам не пришлось повторять мои ошибки. Давайте разберём по порядку.

), body: (

1. Агент не тупой — ему просто не хватает контекста

Первые недели я делал классическую ошибку: кидал задачу без единого слова о проекте и злился, что агент переспрашивает очевидное. Это всё равно что посадить нового человека за чужой код и уйти пить кофе — а потом удивляться, что он сделал не то.

Однажды вечером я потратил полчаса не на саму задачу, а на то, чтобы по-человечески описать проект: что это, по каким правилам мы тут живём и какие команды можно выполнять без лишних вопросов. Тот же Claude Code, та же модель — но он перестал гадать и начал попадать с первого раза.

Что я понял: модель решает всё меньше. Гораздо важнее контекст, который ты ей даёшь. Если упростить, весь путь от моей просьбы до правок в коде укладывается в простую цепочку:

2. Важнее модели — инструменты и привычки

Когда контекст появился, всплыла вторая проблема: агент хватался не за те инструменты. Лез искать в интернете то, что лежало в соседнем файле, или запускал тяжёлую команду там, где хватило бы пары строк.

Я перестал ждать, что он сам угадает, и начал прямо подсказывать: для этой задачи — вот это, для той — вот то. Звучит банально, но именно это сэкономило мне больше всего времени и нервов.

Не пытайтесь подключить всё сразу. Я начал с самого необходимого и добавлял остальное только тогда, когда реально упирался в задачу — так контекст не превращается в свалку.

3. Доверие строится на маленьких шагах

Сначала я перепроверял за агентом каждую строчку — и, честно, это убивало весь смысл. Доверие пришло не сразу: я давал ему задачи поменьше, смотрел на результат, и постепенно отпускал поводья.

Проще один раз увидеть, чем сто раз прочитать — вот короткое видео, где агент правит код за пару минут, пока я просто наблюдаю.

Что я понял: доверие к агенту — это не вера, а привычка. Она нарабатывается на маленьких задачах, где цена ошибки невелика.

4. Обратная связь — лучший способ его обучить

Самым полезным оказалось не идеально сформулировать задачу с первого раза, а быстро поправлять. Агент сделал — я тут же сказал, что не так, он запомнил это как правило, и следующий результат стал точнее. Маленькая петля, которая работает лучше любых длинных инструкций.

Если свести всё к простому чек-листу, с которого стоит начать, — вот он, проверенный на моих проектах:

Чем всё закончилось

Сейчас Claude Code — это не игрушка из демо, а инструмент, которому я доверяю реальную работу. Он не стал умнее за эти полгода. Умнее стал я — в том, как с ним разговаривать.

Если вы только начинаете, не пытайтесь повторить всё сразу. Возьмите один урок, попробуйте на своём проекте — и дальше пойдёт само.

), faq: [ ["Нужно ли уметь программировать?", "Нет. Claude Code берёт рутину на себя — но базовое понимание проекта помогает направлять агента."], ["Какие MCP-серверы ставить первыми?", "filesystem и git закрывают большинство задач. Остальное добавляй под конкретную задачу, а не заранее."], ["Это работает с Codex?", "Да, подход тот же: правила проекта плюс доверенные команды. Отличаются детали запуска."], ], changelog: [ ["12.06.2026", "Обновлено под Claude Code v1.2, добавлен раздел про MCP-память."], ["02.05.2026", "Добавлена блок-схема потока и видео-разбор."], ["18.04.2026", "Первая публикация."], ], }, hermes: { cur: "hermes", crumbMid: "Hermes", title: "Hermes стал моим личным аналитиком", desc: "Три месяца я учил агента читать рынок и собирать сводки за меня. Честная история о том, что получилось, а что пока нет — без обещаний лёгких денег.", tldr: "Если совсем коротко: я перестал искать «самую умную» модель и занялся процессом — утренние сводки, петля обратной связи и общая память. Это дало куда больше, чем любая смена модели.", tested: "Протестировано на Hermes 0.9 · 10 июня 2026", meta: "ОБНОВЛЕНО 10 ИЮНЯ 2026 · 8 МИН ЧТЕНИЯ", side: { group: "Hermes · 5", items: ["Введение в Hermes", "Личный аналитик", "Утренние сводки", "Петля обратной связи", "Память команды"], active: 1 }, toc: ["С чего начал", "Утренние сводки", "Петля обратной связи", "Что в итоге"], docsnav: ["Введение в Hermes", "Память команды"], intro: (

С чего всё началось

Я не трейдер и не аналитик. Но каждое утро мне нужно было быстро понимать, что происходит в индустрии — и на это уходил час, который я ненавидел. Тогда я решил переложить эту рутину на Hermes.

Первый месяц был провальным: агент выдавал простыни текста, в которых тонула суть. Я чуть не бросил. Помогло не «более умная модель», а то, что я наконец перестроил процесс.

Ниже — три вещи, которые превратили Hermes из болтливого помощника в аналитика, которому я доверяю утро. Разберём по порядку.

), body: (

1. Дело было не в модели

Я перепробовал кучу моделей в надежде, что какая-то окажется «умнее» и сама начнёт выдавать то, что нужно. Спойлер: не начала. Разница между ними оказалась куда меньше, чем разница между «объяснил, что мне нужно» и «не объяснил».

Как только я описал, какие источники читать, на что смотреть и в каком виде давать ответ, — качество подскочило на той же самой модели. Путь от запроса до готовой сводки у меня выглядит так:

Что я понял: гнаться за самой умной моделью — тупик. Сначала наладь процесс, и тогда даже простая модель работает на тебя.

2. Ритуал важнее разовых запросов

Переломный момент — когда сводки перестали быть разовыми. Я завёл привычку: каждое утро в 10:00 Hermes присылает короткий разбор по тем источникам, что мы вместе отобрали. Не «когда вспомню», а по расписанию.

Вот как это выглядит вживую — короткое видео, где из десятка источников рождается сводка на пару абзацев.

Что я понял: ценность не в одном гениальном ответе, а в привычке. Регулярность бьёт интеллект.

3. Петля обратной связи всё меняет

Самое полезное — быстро поправлять. Hermes выдал, я сказал «вот это лишнее, а вот этого не хватает», он запомнил это как правило, и следующая сводка стала точнее. За пару недель ответы стали такими, будто их собирал я сам, только без потраченного часа.

Если свести всё к простому ритуалу, вот он:

Чем всё закончилось

Сейчас я открываю утреннюю сводку как кофе — по привычке. Hermes не предсказывает будущее и не делает меня богаче за ночь. Он просто экономит мне час каждый день и держит в курсе. Для меня этого более чем достаточно.

), faq: [ ["Hermes правда помогает зарабатывать?", "Он помогает быть в курсе и экономит время. Решения по-прежнему за вами — это инструмент, а не оракул."], ["Сколько времени на настройку?", "Базовый ритуал собрал за вечер. Дальше он сам становится точнее за счёт правок."], ["Нужны ли особые знания?", "Нет. Важнее понимать, какие источники вам действительно полезны."], ], changelog: [ ["10.06.2026", "Добавил раздел про петлю обратной связи."], ["28.05.2026", "Перевёл сводки на расписание."], ["15.05.2026", "Первая публикация."], ], }, codex: { cur: "codex", crumbMid: "Codex", title: "Codex на практике: чего я не ожидал", desc: "Сел попробовать на вечер — остался на неделю. Рассказываю, где Codex приятно удивил, а где пришлось перестроить привычки. Личные наблюдения, а не пересказ документации.", tldr: "Если совсем коротко: Codex отлично закрывает рутину — ревью, мелкие правки, автоисправления. Но доступы и песочницу стоит настроить с самого начала, иначе нервов уйдёт больше, чем сэкономишь.", tested: "Протестировано на Codex · 8 июня 2026", meta: "ОБНОВЛЕНО 8 ИЮНЯ 2026 · 7 МИН ЧТЕНИЯ", side: { group: "Codex · 4", items: ["Первый запуск Codex", "Ревью пул-реквестов", "Песочница и доступы", "Codex против Claude Code"], active: 0 }, toc: ["Первое впечатление", "Ревью на автомате", "Песочница", "Что в итоге"], docsnav: ["Первый запуск Codex", "Codex против Claude Code"], intro: (

Первое впечатление

Я подходил к Codex со здоровым скепсисом: ещё один агент, который обещает писать код за меня. Сел попробовать на один вечер — а закрыл ноутбук только через неделю, потому что втянулся.

Удивило не то, что он пишет код, а то, как аккуратно он берёт на себя скучное: ревью, мелкие правки, рутинные проверки. Но без пары настроек в начале я бы намучился.

Ниже — что зашло сразу, а что пришлось перенастроить под себя. Разберём по порядку.

), body: (

1. Лучше всего заходит на рутине

Я не стал сразу поручать Codex большие фичи. Начал с мелочи: причесать функцию, поправить тесты, пройтись по замечаниям. И вот тут он оказался незаменим — то, на что у меня уходил час скуки, он делает за минуты.

Если упростить, его путь от задачи до готового результата выглядит так:

Что я понял: не нужно сразу поручать агенту главное. Отдай ему скучное — и он окупится в первый же день.

2. Ревью на автомате — то, ради чего стоит остаться

Главное открытие — Codex как первый ревьюер. Он проходит по пул-реквесту раньше меня и ловит мелочи, на которые у живого человека уже не хватает внимания. Я прихожу к ревью, когда половина замечаний уже снята.

Вот как это выглядит на практике — короткое видео разбора одного PR.

Что я понял: агент-ревьюер экономит не столько время, сколько внимание команды для действительно важного.

3. Песочницу и доступы — сразу

Единственное, обо что я споткнулся, — доступы. Поначалу дал слишком много, потом испугался и закрутил гайки так, что он перестал быть полезным. Золотая середина нашлась быстро, но лучше продумать её заранее.

Вот короткий чек-лист, с которого я бы советовал начать:

Чем всё закончилось

Codex не заменил мне Claude Code — они хорошо делят обязанности. Codex я держу на рутине и ревью, Claude Code — на задачах, где нужно глубже погрузиться в проект. Вместе они закрывают почти всё, и это оказалось удобнее, чем один агент на всё.

), faq: [ ["Codex заменит Claude Code?", "Скорее дополнит. Мне удобнее держать обоих: один на рутине и ревью, другой на глубоких задачах."], ["С чего безопасно начать?", "С песочницы и минимальных доступов. Дальше расширяешь по мере доверия."], ["Где Codex полезнее всего?", "На ревью пул-реквестов и мелких правках — там он экономит больше всего внимания."], ], changelog: [ ["08.06.2026", "Добавил чек-лист по доступам и песочнице."], ["30.05.2026", "Раздел про ревью пул-реквестов."], ["20.05.2026", "Первая публикация."], ], }, }; // Static «Скопировать страницу» button at the top of the article (copies the page's // markdown twin /guides/.md for pasting into an AI agent). Replaces the old // floating bottom FAB — sits statically next to the title, does NOT follow scroll. function CopyPageButton({ slug }) { const [copied, setCopied] = useState(false); const onClick = () => { const done = () => { setCopied(true); window.clearTimeout(window.__cpT); window.__cpT = window.setTimeout(() => setCopied(false), 2400); }; const doCopy = (text) => { if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done).catch(() => {}); else { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { if (document.execCommand("copy")) done(); } catch (e) {} document.body.removeChild(ta); } }; if (slug) fetch("/guides/" + slug + ".md").then((r) => (r.ok ? r.text() : Promise.reject())).then(doCopy).catch(() => { const a = document.querySelector(".gp-article-body, .m-content"); if (a) doCopy(a.innerText); }); else { const a = document.querySelector(".gp-article-body, .m-content"); if (a) doCopy(a.innerText); } }; return ( ); } function CopyButton() { const [copied, setCopied] = useState(false); return ( ); } // Clean card-description truncation: cut at a sentence end (.!?) near the limit, else // at the last word boundary, then «…». No mid-word cuts, no «....» artefacts. function cardDesc(desc, maxLen = 88) { const s = String(desc || "").replace(/\s+/g, " ").trim(); if (s.length <= maxLen) return s; const slice = s.slice(0, maxLen); const sent = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf("! "), slice.lastIndexOf("? ")); if (sent >= maxLen * 0.55) return slice.slice(0, sent + 1); // clean sentence end const sp = slice.lastIndexOf(" "); return (sp > 0 ? slice.slice(0, sp) : slice).replace(/[\s,.;:–—-]+$/, "") + "…"; } // «Похожие материалы» — recirculation block at the end of an article/guide. Plain // covers (NOT /: those hydrate separately and may snapshot // empty during prerender). Cards link to sibling guides → raise recirculation. function RelatedGuides({ items }) { if (!items || items.length === 0) return null; return (

Читайте дальше

); } function Content({ article, cta, rich }) { const a = article || ARTICLES.claude; const isReal = !!a.bodyHtml; // real MDX-backed guide vs demo JSX article const navHref = (slug) => (slug ? GUIDE_HREF(slug) : undefined); const isArticle = a.type === "article"; return (
{a.cover && (
{a.title}
)}
{isArticle ? "Статьи" : Гайды / {a.crumbMid} / {isReal ? "Гайд" : "Личный опыт"}}

{a.title}

{a.desc}

{!isArticle && rich && a.tldr && } {isReal ? ( {!isArticle && a.steps && a.steps.length > 0 && (

Что понадобится

)}
{!isArticle && a.outcome && {a.outcome}} ) : ( {a.intro} {rich && ( {a.body} )} )} {cta}
); } function Toc({ items }) { const list = items || ["Агент и контекст", "Инструменты и привычки", "Доверие шаг за шагом", "Обратная связь"]; // scroll-spy: highlight the section the reader is currently in. Each real H2 in // the article body has id="sec-" (build-guides-data) matching list index i. const [active, setActive] = React.useState(0); React.useEffect(() => { const heads = Array.prototype.slice.call(document.querySelectorAll(".gp-article-body h2[id]")); if (!heads.length) return; const onScroll = () => { let cur = 0; for (let i = 0; i < heads.length; i++) { if (heads[i].getBoundingClientRect().top <= 90) cur = i; else break; } setActive(cur); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const onClick = (e, i) => { const el = document.getElementById("sec-" + i); if (el) { e.preventDefault(); el.scrollIntoView({ behavior: "smooth", block: "start" }); } }; return (
На странице
{list.map((t, i) => ( onClick(e, i)} className={i === active ? "active" : ""}>{t} ))}
); } function Footer() { return ( ); } /* ---------- full guide page ---------- */ function GuidePage({ dir, initialTheme, banner, cta, mini, social, rich, article, cur }) { const a = article || ARTICLES.claude; const [theme, setTheme] = useState(() => getStoredTheme(initialTheme)); const toggle = () => flipTheme(setTheme); return (
{banner}
); } /* ---------- mobile (variant C) ---------- */ const PAGE_MD = `# Claude Code: настройка проекта с нуля\n\nИсточник: guides.edgelab.su · обновлено 12 июня 2026\n\nКак собрать рабочее окружение Claude Code за вечер — от установки до первого агента.\n\n## Зачем настройка\nClaude Code точнее, когда проект описан явно. Полчаса на настройку окупаются на первой же задаче.\n\n## Шаги\n1. Установи Claude Code\n2. Опиши проект в CLAUDE.md\n3. Подключи MCP filesystem + git\n\n— через ${BRAND_NAME} (${FUNNEL_HOST})`; function MobileC({ initialTheme, scroll, demoScrolled }) { const [theme, setTheme] = useState(getStoredTheme(initialTheme)); const night = theme === "dark"; const toggle = () => flipTheme(setTheme); const scRef = React.useRef(null); const [showFab, setShowFab] = useState(false); const [copied, setCopied] = useState(false); React.useEffect(() => { if (demoScrolled && scRef.current) { scRef.current.scrollTop = 1650; setShowFab(true); } }, [demoScrolled]); const onScroll = (e) => { if (scroll) setShowFab(e.target.scrollTop > 1500); }; const copyPage = () => { try { navigator.clipboard && navigator.clipboard.writeText(PAGE_MD); } catch (e) {} setCopied(true); window.clearTimeout(window.__fabT); window.__fabT = window.setTimeout(() => setCopied(false), 2600); }; return (
{FUNNEL_ON && (
{BRAND_NAME} Вступить →
)}
Гайды / Claude Code

Как я собрал рабочее окружение за один вечер

История о том, как Claude Code из игрушки в демо превратился в инструмент, которому я доверяю реальную работу.

ОБНОВЛЕНО 12 ИЮНЯ 2026 · 9 МИН

С чего всё началось

Полгода назад я считал Claude Code игрушкой: в демо красиво, в реальном проекте — бесполезно. Пока однажды не потратил полчаса на то, чтобы по-человечески объяснить агенту проект.

Лучше один раз увидеть

Короткое видео: агент правит код за пару минут, пока я наблюдаю.

Пошагово

{/* VOX: EdgeLab mobile community-CTA убран */}
{FUNNEL_ON &&
{BRAND_NAME}
{CTA_TEXT} →
}
© 2026 {LEGAL_NAME.toUpperCase()}
{LEGAL_ENTITY}
); } function MobileArticle({ article, initialTheme }) { const a = article || ARTICLES.claude; const [theme, setTheme] = useState(getStoredTheme(initialTheme)); const night = theme === "dark"; return (
flipTheme(setTheme)} /> {/* VOX: EdgeLab mobile-баннер убран */}
{a.cover && (
{a.title}
)}
{a.type === "article" ? "Статьи" : "Гайды / " + a.crumbMid}

{a.title}

{a.desc}

{a.type !== "article" && a.tldr && } {a.bodyHtml ? ( {a.type !== "article" && a.steps && a.steps.length > 0 && (

Что понадобится

)}
{a.type !== "article" && a.outcome && {a.outcome}} ) : ( {a.intro} {a.body} )} {/* VOX: EdgeLab mobile community-CTA убран — заход в продукт внутри текста статьи */}
© 2026 {LEGAL_NAME.toUpperCase()}
{LEGAL_ENTITY}
); } function MobileMenu({ initialTheme }) { const [open, setOpen] = useState(true); const [theme, setTheme] = useState(getStoredTheme(initialTheme)); const night = theme === "dark"; const guides = ["Введение в Claude Code", "Настройка проекта с нуля", "MCP-серверы и память", "Автоматизация видео", "Архитектура памяти агента"]; return (
{FUNNEL_ON && (
{BRAND_NAME} Вступить →
)}
Гайды / Claude Code

Claude Code: настройка проекта с нуля

Рабочее окружение Claude Code за вечер – от установки до первого агента.

setOpen(false)}>
e.stopPropagation()}>
Разделы
{guides.map((g, i) => ( {g} ))}
); } function BlockDemo({ heading, intro, children }) { return (

{heading}

{intro}

{children}
); } /* ============================================================ */ /* ===== real-guides catalog + command palette ===== */ // Facets/counts come from the real 11 guides (window.__GF_GUIDES). Each catalog // card carries the guide's slug so it links straight to guides/.html. const LEVELS = ["Новичок", "Средний", "Продвинутый"]; const CATS = GUIDE_CATS.map((c) => ({ id: c.id, name: c.name, n: GF_GUIDES.filter((g) => g.cat === c.id).length, })); const ALL_GUIDES = GF_GUIDES.map((g) => ({ id: g.id, slug: g.slug, cat: g.cat, catName: g.catName, type: g.type, title: g.title, desc: g.desc, level: g.level, min: g.min, // dates drive the «Сначала новые» catalog sort (sortGuides). publishedAt: g.publishedAt || "", updatedAt: g.updatedAt || "", stub: !!g.stub, })); /* ---------- full-text search (MiniSearch) ---------- */ // id → UI-shaped guide (the ALL_GUIDES entry the cards/palette render from). const GUIDE_BY_ID = (() => { const m = {}; for (const g of ALL_GUIDES) m[g.id] = g; return m; })(); // Lazy singleton MiniSearch index over the real guides. Built once on first // search, then cached. Indexes title/desc/tldr + the full-text `st` field that // build-guides-data.mjs emits (title+desc+tldr+tags+stripped body). Returns null // when MiniSearch is unavailable (SSR / prerender) so callers fall back to a // substring scan. let __gfSearchIndex; // undefined = not built, null = unavailable, else instance function getSearchIndex() { if (__gfSearchIndex !== undefined) return __gfSearchIndex; if (typeof window === "undefined" || !window.MiniSearch) { __gfSearchIndex = null; return null; } const idx = new window.MiniSearch({ idField: "id", fields: ["title", "desc", "tldr", "st"], storeFields: ["id"], searchOptions: { boost: { title: 4, desc: 2, tldr: 2, st: 1 }, prefix: true, fuzzy: 0.2, combineWith: "AND" }, }); idx.addAll(GF_GUIDES); __gfSearchIndex = idx; return idx; } // Single source of search truth used by BOTH CommandPalette and Catalog. // Empty query → all guides (source order). Otherwise MiniSearch ranked results, // mapped back to UI-shaped guide objects. Falls back to a substring scan if the // index is unavailable. function searchGuides(q) { if (!q || !q.trim()) return ALL_GUIDES; const idx = getSearchIndex(); if (!idx) { const ql = q.trim().toLowerCase(); return ALL_GUIDES.filter((g) => g.title.toLowerCase().includes(ql) || (g.desc || "").toLowerCase().includes(ql) || g.catName.toLowerCase().includes(ql)); } return idx.search(q).map((r) => GUIDE_BY_ID[r.id]).filter(Boolean); } // XSS-safe highlighter: split `text` on the query tokens (case-insensitive, // prefix/substring) and wrap matches in . Returns an array // of React nodes — never dangerouslySetInnerHTML, so user input is escaped by // React. Tokens come from whitespace-splitting the query. function highlight(text, q) { const src = String(text == null ? "" : text); const tokens = String(q || "").trim().toLowerCase().split(/\s+/).filter((t) => t.length > 0); if (tokens.length === 0) return [src]; const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // For each token build two prefix/substring patterns: // (a) the token itself, matched anywhere (substring) — the literal case; // (b) the token's STEM (drop the last 1–2 chars) matched at a word start and // greedily extended over word chars — so an inflected form (Russian // endings: «базу/базе» vs query «база») still gets highlighted. Still // prefix-based, still XSS-safe (we only wrap the text we matched). const pats = []; for (const t of tokens) { pats.push(esc(t)); if (t.length >= 4) { const stem = t.slice(0, Math.max(3, t.length - 2)); // word-boundary-ish: stem preceded by a non-word char or string start. pats.push("(?{part}); else nodes.push(part); } return nodes; } function CommandPalette({ onClose, initial }) { const [q, setQ] = useState(initial || ""); const [sel, setSel] = useState(0); const inputRef = React.useRef(null); React.useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []); const results = searchGuides(q).slice(0, 40); const groups = {}; results.forEach((g) => { (groups[g.catName] = groups[g.catName] || []).push(g); }); let idx = -1; return (
e.stopPropagation()}>
{ setQ(e.target.value); setSel(0); }} placeholder="Найти гайд…" /> ESC
{results.length === 0 &&
Ничего не найдено
} {Object.keys(groups).map((gn) => ( ))}
↑↓навигацияоткрытьescзакрыть{results.length} из {ALL_GUIDES.length}
); } // Catalog sort modes. "new" is the default — publishedAt desc, i.e. the toolbar's // long-standing «Сначала новые» label, now actually applied (before this it was a // decorative span and the grid showed source order). Search results keep MiniSearch // relevance order, so sorting applies only with an empty query. const SORTS = [ { id: "new", label: "Сначала новые" }, { id: "time", label: "Быстрые сначала" }, { id: "cat", label: "По категории" }, ]; function sortGuides(list, mode) { const a = list.slice(); // «Скоро» placeholders never lead the date/time views (they'd otherwise top // «новые» by newest date and «быстрые» by min reading time, pushing real // content + the crawlable above-the-fold below stubs). In «cat» they stay in // their category. stub === 1 sorts after stub === 0. const stub = (x, y) => (x.stub ? 1 : 0) - (y.stub ? 1 : 0); if (mode === "time") return a.sort((x, y) => stub(x, y) || x.min - y.min || x.title.localeCompare(y.title, "ru")); if (mode === "cat") return a.sort((x, y) => x.catName.localeCompare(y.catName, "ru") || (y.publishedAt || "").localeCompare(x.publishedAt || "")); return a.sort((x, y) => stub(x, y) || (y.publishedAt || y.updatedAt || "").localeCompare(x.publishedAt || x.updatedAt || "")); } function Catalog({ initialTheme, mobile, initialCmdk }) { const [theme, setTheme] = useState(getStoredTheme(initialTheme)); const night = theme === "dark"; const [cat, setCat] = useState("all"); const [level, setLevel] = useState("all"); const [q, setQ] = useState(""); const [cmdk, setCmdk] = useState(initialCmdk || false); const [sort, setSort] = useState("new"); const [sortOpen, setSortOpen] = useState(false); const sortRef = React.useRef(null); // Full-text search drives the base set; facets (cat + level) filter the // searched results. Empty query → all guides, ordered by the chosen sort. const base = React.useMemo(() => searchGuides(q), [q]); const filtered = base.filter((g) => (cat === "all" || g.cat === cat) && (level === "all" || g.level === level)); const hasQuery = q.trim().length > 0; // Only 11 real guides — show them all (no demo pagination on a small set). // Sort applies to the empty-query catalog; with a query we keep relevance order. const cards = hasQuery ? filtered : sortGuides(filtered, sort); const total = filtered.length; React.useEffect(() => { if (!sortOpen) return; const onDoc = (e) => { if (sortRef.current && !sortRef.current.contains(e.target)) setSortOpen(false); }; document.addEventListener("click", onDoc); return () => document.removeEventListener("click", onDoc); }, [sortOpen]); // Hide the open menu if the user starts a search (the toolbar swaps it out). React.useEffect(() => { if (hasQuery) setSortOpen(false); }, [hasQuery]); // No-results fallback: surface a few first guides so the page is never empty. const popular = ALL_GUIDES.slice(0, 3); // ⌘K keyboard shortcut still opens the modal palette (the inline input is primary). React.useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); setCmdk(true); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); return (
flipTheme(setTheme)} />
ВСЕ ИНСТРУКЦИИ

Гайды по доступу к заблокированному

Как вернуть YouTube, WhatsApp, Instagram и зарубежные сервисы в России и как выбрать рабочий VPN. Найди нужное за секунды — поиском или по категориям.

setQ(e.target.value)} placeholder="Найти гайд по названию, теме или тексту…" aria-label="Поиск по гайдам" autoComplete="off" spellCheck={false} /> {q ? setQ("")}>✕ : ⌘K}
Категории
setCat("all")}>Все гайды{ALL_GUIDES.length}
{CATS.map((c) => (
setCat(c.id)}>{c.name}{c.n}
))}
Уровень
setLevel("all")}>Любой
{LEVELS.map((l) => (
setLevel(l)}>{l}
))}
{total} гайдов{cat !== "all" ? " · " + CATS.find((c) => c.id === cat).name : ""}{hasQuery ? " · по запросу «" + q.trim() + "»" : ""} {!hasQuery && (
{sortOpen && (
{SORTS.map((s) => ( ))}
)}
)}
{total > 0 ? ( ) : (
Ничего не найдено по запросу «{q.trim()}»
Попробуйте другие слова или загляните в популярные гайды:
)}
{cmdk && setCmdk(false)} />}
); } /* ============================================================ */ /* ===== HOME / blog landing (editorial, human voice) ===== */ function PlayGlyph() { return (); } function Slot({ id, ph, r, src, fit, position }) { // Deployed statically (no omelette fill runtime): only render a slot that has a // real cover image. Empty placeholders (dashed «Обложка» boxes) are hidden until // a cover exists, so the home/category/about pages never show empty frames. // `fit` (cover|contain|fill) is forwarded to the image-slot element so a // pre-composed banner can be shown whole (contain) instead of cropped (cover). if (!src) return null; return (); } function HomeSecHead({ title }) { return ( ); } function Home({ initialTheme, mobile, idp = "h" }) { const [theme, setTheme] = useState(getStoredTheme(initialTheme)); const night = theme === "dark"; // Split by content type: «Статьи» = readable articles, «Гайды» = technical guides. const metaShort = (g) => `${g.min} МИН ЧТЕНИЯ`; const card = (g) => ({ t: g.title, d: g.desc, m: metaShort(g), slug: g.slug, cover: g.cover, type: g.type }); const articleList = GF_GUIDES.filter((g) => g.type === "article"); const guideList = GF_GUIDES.filter((g) => g.type !== "article"); // Featured articles pinned to the FRONT of the «Статьи» row — same card style, just first // (prince 2026-06-18: «закрепить вначале, но не выделять отдельным блоком»). Multiple featured // are ordered by the tool roadmap Claude Code → Codex → Hermes (prince 2026-06-18: «вынеси вверх»). const FEAT_ORDER = { claude: 0, codex: 1, hermes: 2 }; const featuredArts = articleList.filter((g) => g.featured).sort((a, b) => (FEAT_ORDER[a.cat] ?? 9) - (FEAT_ORDER[b.cat] ?? 9)); const orderedArticles = [...featuredArts, ...articleList.filter((g) => !g.featured)]; const opinion = orderedArticles.slice(0, 3).map(card); // «Статьи» row, featured (roadmap order) first const featured = guideList[0] || GF_GUIDES[0]; // hero «Начать» CTA target const guideCards = guideList.map(card); // «Гайды» 3-col grid — все гайды (prince 2026-06-17: показывать больше) return (
flipTheme(setTheme)} />
{/* hero */}
{HOME_EYEBROW}

{HOME_TITLE}

{HOME_DESC}

{HOME_CTA_TEXT}
{/* articles: 3 cards, featured pinned first */}
{opinion.map((g, i) => (
Статья

{g.t}

{g.d}

{g.m}
))}
{/* guides: uniform card grid (3-col, balanced). Hidden while empty — returns automatically once a guide (type !== "article") is published. */} {guideCards.length > 0 && (
{guideCards.map((g, i) => (
Гайд

{g.t}

{g.d}

{g.m}
))}
)}
); } /* ===== Category page (Claude Code / Hermes / Codex) ===== */ const CATPAGES = { claude: { cur: "claude", eyebrow: "Claude Code", title: "Claude Code без боли", lead: "Личный опыт работы с агентом, который правит код сам: настройка, привычки, разборы реальных задач — по-человечески, без сухих инструкций.", featTag: "Claude Code · личный опыт", featTitle: "Как я собрал рабочее окружение за один вечер", featDesc: "Никакого волшебства — только то, что реально сработало. Что сломалось по дороге и почему полчаса настройки окупились на первой же задаче.", featMeta: "12 ИЮНЯ · 9 МИН ЧТЕНИЯ", list: [ { t: "Память, которая не забывает", d: "Почему агент наконец перестал переспрашивать одно и то же.", m: "18 МАЯ · 6 МИН" }, { t: "День, когда агент написал свой первый скилл", d: "Маленький момент, после которого всё пошло иначе.", m: "14 МАЯ · 5 МИН" }, { t: "Доверенные команды: меньше вопросов", d: "Как я перестал подтверждать каждый шаг и стал доверять агенту.", m: "9 МАЯ · 6 МИН" }, { t: "Большой репозиторий и контекст", d: "Что помогло агенту не теряться в проекте на сотни файлов.", m: "2 МАЯ · 8 МИН" }, ], articles: [ { t: "Почему я перестал гнаться за самой умной моделью", d: "Оказалось, дело вообще не в модели.", m: "3 ИЮНЯ · 6 МИН" }, { t: "Инструменты решают больше, чем кажется", d: "Один правильный инструмент сэкономил мне неделю.", m: "27 МАЯ · 5 МИН" }, { t: "Мой CLAUDE.md, который я ношу из проекта в проект", d: "Что в нём лежит и почему он так экономит время.", m: "20 МАЯ · 7 МИН" }, ], }, hermes: { cur: "hermes", eyebrow: "Hermes", title: "Hermes — личный аналитик", lead: "История о том, как я три месяца учил агента читать рынок и собирать утренние сводки за меня. Что получилось, а что — нет.", featTag: "Hermes · личный опыт", featTitle: "Hermes стал моим аналитиком", featDesc: "Три месяца я учил агента читать рынок за меня. Рассказываю, что из этого вышло — без прикрас и обещаний лёгких денег.", featMeta: "25 МАЯ · 8 МИН ЧТЕНИЯ", list: [ { t: "Утренние сводки в 10:00", d: "Как я собрал ритуал, который экономит мне час каждый день.", m: "21 МАЯ · 6 МИН" }, { t: "Обратная связь = лучший способ обучить агента", d: "Шесть шагов петли, после которых ответы стали точнее.", m: "16 МАЯ · 7 МИН" }, { t: "Эхо-камера: честно о проблеме", d: "Где агент начинает повторять одно и то же и что я с этим делаю.", m: "10 МАЯ · 5 МИН" }, { t: "Маршрутизация задач между агентами", d: "Кто за что отвечает в моей связке и почему так.", m: "3 МАЯ · 6 МИН" }, ], articles: [ { t: "Открытость победит — и вот почему", d: "Честный взгляд на то, куда движется индустрия.", m: "21 МАЯ · 7 МИН" }, { t: "Как я перестал бояться, что агент ошибётся", d: "Спокойствие приходит, когда у тебя есть петля проверки.", m: "12 МАЯ · 5 МИН" }, { t: "Память команды: один контекст на всех", d: "Зачем агентам общая память и как её не превратить в свалку.", m: "5 МАЯ · 6 МИН" }, ], }, codex: { cur: "codex", eyebrow: "Codex", title: "Codex на практике", lead: "Как я подружил Codex с реальными проектами: запуск, ревью, автоисправления. Личные наблюдения, а не пересказ документации.", featTag: "Codex · личный опыт", featTitle: "Первый запуск Codex: чего я не ожидал", featDesc: "Сел попробовать на вечер — остался на неделю. Рассказываю, где Codex приятно удивил, а где пришлось перестроить привычки.", featMeta: "8 ИЮНЯ · 7 МИН ЧТЕНИЯ", list: [ { t: "Codex в ревью пул-реквестов", d: "Как агент стал моим первым ревьюером и что это дало команде.", m: "1 ИЮНЯ · 6 МИН" }, { t: "Автоисправление багов: где предел", d: "Что Codex чинит сам, а где всё ещё нужен человек.", m: "24 МАЯ · 5 МИН" }, { t: "Песочница и доступы по уму", d: "Как я настроил безопасный запуск без лишней паранойи.", m: "17 МАЯ · 6 МИН" }, { t: "Codex против Claude Code: мой выбор", d: "Когда беру одно, когда другое — без священных войн.", m: "10 МАЯ · 8 МИН" }, ], articles: [ { t: "Почему я держу два агента, а не одного", d: "Разные задачи — разные сильные стороны.", m: "30 МАЯ · 6 МИН" }, { t: "CI и агент: автоматизация без сюрпризов", d: "Как я пустил Codex в пайплайн и спокойно сплю.", m: "23 МАЯ · 7 МИН" }, { t: "Промпты для Codex, которые реально работают", d: "Несколько приёмов, проверенных на своих проектах.", m: "15 МАЯ · 5 МИН" }, ], }, }; function CategoryPage({ cat, initialTheme, mobile, idp }) { const c = CATPAGES[cat] || CATPAGES.claude; const p = idp || cat; const [theme, setTheme] = useState(getStoredTheme(initialTheme)); const night = theme === "dark"; return (
flipTheme(setTheme)} />
{c.eyebrow}

{c.title}

{c.lead}

Читать гайд
{c.eyebrow}
{c.articles.map((g, i) => (
Статья

{g.t}

{g.d}

{g.m}
))}
); } /* ===== About page ===== */ function AboutPage({ initialTheme, mobile }) { const [theme, setTheme] = useState(getStoredTheme(initialTheme)); const night = theme === "dark"; return (
flipTheme(setTheme)} />
О проекте

{BRAND_NAME} — про доступ к заблокированному

Понятные разборы: как в 2026 году вернуть YouTube, WhatsApp, Instagram и другие сервисы в России и как выбрать VPN, который действительно работает. Простым языком, без воды и без обещаний.

Бот «{TG_NAME}»

Зачем этот блог

В 2026 году доступ к привычным сервисам в России стал непредсказуемым: то YouTube не грузится, то отваливается WhatsApp, то перестаёт подключаться VPN. Мы разбираем, что происходит на самом деле и что с этим можно сделать — простым языком, без технических заморочек.

Пишем честно: объясняем причину, показываем рабочие шаги и не обещаем невозможного. Если чего-то не может ни один VPN — так и говорим.

Что внутри

Актуальные инструкции

Под реальную ситуацию 2026 года, а не общие советы пятилетней давности.

Для iPhone и Android

Пошагово и понятно, без строчек кода и лишних терминов.

Честно, без обещаний

Называем и то, что работает, и то, где есть ограничения.

{FUNNEL_ON && ( )}
); } /* ============================================================ */ function App() { return ( } cta={} rich={true} /> } cta={} rich={true} />
Меню слева. По тапу на бургер слева выезжает drawer, фон затемняется. Внутри – разделы и список гайдов. Закрытие по «✕» или тапу по затемнению.
Тёмная тема. Тот же drawer в тёмном стиле – фон #141416, lime-акцент на активном пункте, затемнение фона глубже.
Кнопка для агента. При прокрутке ниже 1500px всплывает кнопка «Скопировать для агента» – копирует всю страницу в Markdown, чтобы отправить агенту. Кликни – покажет подтверждение.
Командная палитра. ⌘K (или клик по строке поиска) открывает палитру: живой поиск по всем 500 гайдам, сгруппированный по категориям. Печатай – список фильтруется.
Держи шаги короткими: одно действие – один пункт. Так гайд легче сканировать и он лучше ранжируется.
); } // Delegated copy-to-clipboard for code blocks + prompt callouts (M1). Buttons // (.copy-btn) are baked into the static guide HTML by build-guides-data.mjs; a // single document-level listener handles all of them (works after re-mount). // Analytics goal: fire when any «Вступить» CTA is clicked. The CTA links carry // utm_medium= (header/banner/cta/footer/about); we read it so GA4 + // Yandex.Metrika see WHICH placement converts. Capture-phase → fires before the // page navigates away. Guarded: no-op during prerender (no gtag/ym) or if blocked. if (typeof document !== "undefined" && !window.__gfCtaBound) { window.__gfCtaBound = true; document.addEventListener("click", (e) => { // Scope to the community-join destination only (edgelab.space). Without the // host filter, an editorial workshop link (edgelab.su, utm_campaign=workshop) // in a guide body would wrongly fire the join goal and pollute placements. const a = e.target.closest && e.target.closest(`a[href*="${FUNNEL_HOST}"][href*="utm_medium="]`); if (!a) return; const placement = (a.href.match(/utm_medium=([^&]+)/) || [])[1] || "unknown"; try { if (typeof window.gtag === "function") window.gtag("event", "join_click", { placement: placement, link_url: a.href }); } catch (err) {} try { if (typeof window.ym === "function") window.ym(109832554, "reachGoal", "join_click", { placement: placement }); } catch (err) {} }, true); } if (typeof document !== "undefined" && !window.__gfCopyBound) { window.__gfCopyBound = true; document.addEventListener("click", (e) => { const btn = e.target.closest && e.target.closest(".copy-btn"); if (!btn) return; const kind = btn.getAttribute("data-copy"); let text = ""; if (kind === "code") { const pre = btn.closest(".code-block") && btn.closest(".code-block").querySelector("pre"); text = pre ? pre.innerText : ""; } else if (kind === "prompt") { // Copy the WHOLE prompt body (paragraphs + ol/ul lists), not just

— // numbered task lists were being silently dropped. Clone, strip the // «ПРОМПТ» eyebrow + the button, then take innerText. const cal = btn.closest(".blk-callout"); if (cal) { const clone = cal.cloneNode(true); clone.querySelectorAll(".gp-eyebrow, .copy-btn").forEach((n) => n.remove()); text = clone.innerText.trim(); } } if (!text) return; const done = () => { if (!btn.__orig) btn.__orig = btn.innerHTML; btn.classList.add("copied"); btn.innerHTML = ''; window.clearTimeout(btn.__t); btn.__t = window.setTimeout(() => { btn.classList.remove("copied"); btn.innerHTML = btn.__orig; }, 2000); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(done).catch(() => {}); } else { // fallback for non-secure contexts const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { if (document.execCommand("copy")) done(); } catch (err) { /* noop */ } document.body.removeChild(ta); } }); } // M1b: fixed «Скопировать страницу» button on guide pages — copies the guide's // markdown twin (/guides/.md) for pasting into an AI agent. React-managed // (single instance, no prerender duplication). Falls back to the rendered // article text if the .md fetch fails. Dark button works on light + dark themes. function PageCopyFab({ slug }) { const [copied, setCopied] = useState(false); if (!slug) return null; const onClick = () => { const done = () => { setCopied(true); window.clearTimeout(window.__pfT); window.__pfT = window.setTimeout(() => setCopied(false), 2600); }; const doCopy = (text) => { if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(done).catch(() => {}); } else { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { if (document.execCommand("copy")) done(); } catch (e) { /* noop */ } document.body.removeChild(ta); } }; fetch("/guides/" + slug + ".md") .then((r) => (r.ok ? r.text() : Promise.reject())) .then(doCopy) .catch(() => { const a = document.querySelector(".gp-article-body, .m-content"); if (a) doCopy(a.innerText); }); }; return ( ); } const __gfRoot = ReactDOM.createRoot(document.getElementById("root")); if (window.__GF_VIEW === "variantC") { // Standalone guide page shows the first real guide. const art = REAL_ARTICLES[GF_GUIDES[0] && GF_GUIDES[0].slug] || guideToArticle(GF_GUIDES[0]); __gfRoot.render(

} cta={} rich={true} />
); } else if (window.__GF_VIEW === "home") { __gfRoot.render(
); } else if (window.__GF_VIEW === "guide") { // Per-guide page: guides/.html sets __GF_SLUG (+ __GF_BASE="../"). const slug = window.__GF_SLUG; const art = REAL_ARTICLES[slug] || REAL_ARTICLES[Object.keys(REAL_ARTICLES)[0]]; __gfRoot.render(
} cta={} rich={true} />
); } else if (window.__GF_VIEW === "category") { // Each category route maps to a representative REAL guide in that category. // Hermes/Codex now have placeholder intro guides (cat=hermes/codex). const CAT_TO_SLUG = { claude: firstInCat("claude", GF_GUIDES[0] && GF_GUIDES[0].slug), hermes: firstInCat("hermes", GF_GUIDES[0] && GF_GUIDES[0].slug), codex: firstInCat("codex", GF_GUIDES[0] && GF_GUIDES[0].slug), }; const slug = CAT_TO_SLUG[window.__GF_CAT] || CAT_TO_SLUG.claude; const art = REAL_ARTICLES[slug] || guideToArticle(GF_GUIDES[0]); __gfRoot.render(
} cta={} rich={true} />
); } else if (window.__GF_VIEW === "about") { __gfRoot.render(
); } else if (window.__GF_VIEW === "library") { __gfRoot.render(
); } else if (window.__GF_VIEW === "mobile") { // Route resolution order: live URL hash (interactive nav in mobile.html) wins; // then __GF_ROUTE (baked into the prerendered build/m/.html so it // re-mounts to the right screen even though dynamic serving strips the hash); // then __GF_CAT; default home. __GF_ROUTE is a reserved route name // (home/library/claude/hermes/codex/about) or a real guide slug — the legacy // "guide" alias below is hash-only and never baked. const v = (window.location.hash || "").replace("#", "") || window.__GF_ROUTE || window.__GF_CAT || "home"; const firstReal = REAL_ARTICLES[GF_GUIDES[0] && GF_GUIDES[0].slug]; const catReal = (cat) => REAL_ARTICLES[firstInCat(cat, GF_GUIDES[0] && GF_GUIDES[0].slug)]; const bySlug = REAL_ARTICLES[v]; const inner = bySlug ? : v === "claude" ? : v === "hermes" ? : v === "codex" ? : v === "guide" ? : v === "about" ? : v === "library" ? : ; // page-copy FAB only on mobile GUIDE/category article views const mGuideSlug = bySlug ? v : (v === "claude" || v === "hermes" || v === "codex") ? firstInCat(v, GF_GUIDES[0] && GF_GUIDES[0].slug) : v === "guide" ? (GF_GUIDES[0] && GF_GUIDES[0].slug) : null; __gfRoot.render(
{inner}
); if (!window.__gfHashWired) { window.__gfHashWired = true; window.addEventListener("hashchange", () => window.location.reload()); } } else { __gfRoot.render(); }