Генератор сопроводительных писем с fit-оценкой на коде (не LLM),
гиперперсонализацией из вашего профиля и open-source кодом под MIT.
Нативное окно (WebKit2GTK) + Go-бэкенд, один бинарник, настройки в
~/.config/covercraft/settings.json, контекст кандидата в context/*.md.
Автор: Robert Yusupov — github.com/turkprogrammer · yusupov-tech.ru
| Преимущество | CoverCraft | Конкуренты |
|---|---|---|
| Fit-оценка на коде, не на модели | Вердикт apply/skip/caveats считает детерминированный матчер (internal/fit), ноль LLM-токенов по умолчанию |
Rezi, Jobscan — оценка через LLM или эвристика |
| Гиперперсонализация | Письмо строится из вашего реального профиля (context/*.md), а не generic-шаблонов |
Enhancv, Novoresume — шаблоны с автозаполнением |
| Приватность | Профиль и письма остаются локально; нет облака-посредника | TealHQ, Skillbuild — данные в их cloud |
| Open-source + один бинарь | Go-проект под MIT, go build даёт один файл, нет npm/yarn/pip |
Большинство — SaaS или тяжёлые зависимости |
| Два движка матчинга | Детерминированный (0 токенов) и гибридный (LLM с цитатами + код-верификация) | Нет аналогов |
Подробная история всех версий — в CHANGELOG.md.
┌────────────────────────────────┐
│ GTK-окно + WebKit2GTK 4.1 │ internal/window (CGO, ~70 строк)
│ frontend/index.html (embed) │ terminal UI, vanilla JS
└──────────┬─────────────────────┘
│ fetch() по относительным URL
┌──────────▼─────────────────────┐
│ HTTP на 127.0.0.1:случайный │ internal/server
│ порт: / · /api/settings · │
│ /api/generate · │
│ /api/prompt/compose │
└──────┬──────────────┬──────────┘
│ │
~/.config/… context/*.md + LLM (любой OpenAI-совместимый API:
settings.json Ollama, OpenAI, OpenRouter, vLLM, …)
- UI: один HTML-файл без бандлеров, терминальный стиль (тёмная тема по
умолчанию, светлая через
prefers-color-scheme), моноширинный шрифт. Ходит по относительным URL — порт в JS не нужен. - Настройки: хранит Go, UI читает/пишет через
/api/settings. LocalStorage не используется: случайный порт = новый origin при каждом запуске, настройки бы терялись. - Промпт:
context/*.md(профиль, проекты — всё, что.md, по алфавиту)- текст вакансии → единый user-промпт; системный промпт — из настроек.
Кнопка
[ compose ]в панелиsystem.promptодним LLM-вызовом отбирает нерелевантные текущей вакансии разделы профиля и сокращает user-промпт: они вырезаются черезdropSections. Невырезаемы два раздела: антигаллюцинационный «ФАКТЫ-ОГРАНИЧИТЕЛИ» и «КОНТАКТЫ» (строка контактов живёт в приватномcontext/, вне репозитория). Системный промпт композер НЕ переписывает: полеsystem.prompt— рукописный актив пользователя. Страховки: при смене вакансии старый отбор сбрасывается — вырезать по чужой вакансии нельзя; сомнения трактуются в пользу «не резать».
- текст вакансии → единый user-промпт; системный промпт — из настроек.
Кнопка
- Постпроверка письма (
internal/audit): после генерации письмо автоматически проверяется на типовые сбои LLM — потерянные факты профиля (Yii2/Lumen/PHPUnit/Fraud Engine и метрики), запрещённое в строке стека (фреймворки, PHPUnit/PHPStan, non-tech термины), дубли технологий между буллетами и секцией пробелов, смещённую атрибуцию метрик (в письме говорится «Go-проект», а факты — из PHPUnit), плейсхолдеры и названия ИИ-редакторов. Отдельно проверяются структурные инварианты письма: обязательная секция «Адаптация под ваш стек», строка стека с префиксом «Стек: », строка контактов, финальная строка и запрет самоуничижительного начала буллета («не работал / не применял / опыта нет»). Найденное показывается панелью «⚠ проверка письма» под результатом. - Автоправка (auto-fix): кнопка в панели замечаний отправляет письмо + список претензий + контекст кандидата обратно модели с правилами исправления по каждому типу замечания; исправленный текст проверяется повторно — цикл до нуля замечаний. Модель правит точечно по фактам профиля, а не пишет письмо заново. Если модель вместо правки вывела кусок профиля или промпта, такой ответ отбрасывается: письмо не перезаписывается, вердикт фита не отправляется, цикл останавливается с предупреждением.
- LLM: любой OpenAI-совместимый endpoint. Ollama работает без ключа.
- Стриминг:
/api/generateотвечает Server-Sent Events — письмо печатается вletter.outпо мере генерации модели (эффект «печатающей машинки»); в конце — событие с полным текстом,elapsedMsи предупреждениями аудита. - Счётчик времени:
/api/generateвозвращаетelapsedMs— UI показываетN chars · X.Xsрядом с письмом (сколько отвечала модель).
Модели с «размышлениями» (z-ai/glm-5.x и т.п.) без настройки могут думать
минутами — письмо кажется «висящим», а потом приходит context deadline exceeded. В панели api.config есть селект reasoning_effort:
- none — отключить размышления (рекомендуется для генерации писем);
- low / medium / high — управляемое усилие;
- «не отправлять» — параметр не уходит в запрос (для моделей и провайдеров без поддержки, например Ollama).
Замеры на реальном провайдере: glm-5.3-free без effort — 3355
reasoning-токенов и 6+ минут на письме; с none — в разы быстрее.
Таймаут задаётся в настройках (timeoutSec): по умолчанию 60 s, потолок
900 s, клиент по умолчанию 300 s.
- Ubuntu, Linux x86_64, GTK3, WebKit2GTK 4.1.
- Go 1.26+ для сборки.
# dev-заголовки для сборки (рантайм уже есть в Ubuntu 22.04+):
sudo apt install build-essential pkg-config libwebkit2gtk-4.1-dev
libwebkit2gtk-4.0в Ubuntu 24.04+ больше нет — поэтому вендоритьwebview/webview_goнельзя, окно открывается собственной CGO-обвязкой поверх системного WebKit 4.1 (internal/window).
go build -o covercraft .
./covercraftОсновной путь — готовый бинарник из Releases:
tar xzf covercraft_0.3.5_linux_amd64.tar.gz
./covercraftДля запуска достаточно рантайма libwebkit2gtk-4.1-0 (есть в Ubuntu 24.04+).
Альтернатива — go install (компилирует на вашей машине, нужны dev-заголовки):
sudo apt install build-essential pkg-config libwebkit2gtk-4.1-dev
go install github.com/turkprogrammer/covercraft@latestБинарник окажется в ~/go/bin/. Приложение ищет context/*.md рядом с
бинарником или в текущем каталоге — создайте context/ там, где будете
запускать.
- Запустите. Откроется окно
~$ covercraftв терминальном стиле. - В панели
api.configукажите base URL провайдера (напримерhttps://api.apinex.bond/v1илиhttp://127.0.0.1:11434/v1для Ollama), модель (напримерfree/gemini-3.8-flash,llama3.2), ключ при необходимости, и reasoning_effort для reasoning-моделей. - В
system.prompt— как писать письмо (есть разумный дефолт; свой кастомный промпт вставляется вручную и живёт до перезапуска). Кнопка[ compose ]уберёт нерелевантные вакансии разделы профиля, поле при этом не трогает:[ вернуть прежний ]откатывает отбор,reset defaultвозвращает дефолт и чистит вырезанные разделы. Селекторpresetsнад полем переключает заготовки промптов под разные цели (письмо, ответы рекрутеру, свои):[ new preset ]открывает окно с именем и текстом — видно, что именно сохранится;[ save preset ]обновляет активный текстом поля;[ edit preset ]правит имя и текст активного пресета;[ del preset ]удаляет выбранный. Пресеты лежат в~/.config/covercraft/presets.jsonи переживают перезапуск; выбор копирует текст в поле, а правки в поле сессионные — в шаблон они уходят только по save/edit. После генерации строка «в модель ушло» называет источник системного промпта (пресет / ваш / дефолтный) и его первую строку — какой промпт реально ушёл, видно сразу. - Вставьте вакансию в
vacancy.in, нажмите[ gen ]или Ctrl+Enter. - Письмо появится в
letter.out— счётчик покажет размер и время генерации (N chars · X.Xs); можно отредактировать и нажать[ copy ]. - Письмо можно прервать кнопкой
stopв шапке или клавишейEsc; Esc также очищает результат.
Настройки сохраняются автоматически (после 400 мс тишины) и переживают перезапуск. Esc очищает результат.
После генерации письмо проверяется фит-матчером: требования вакансии
(разобранные моделью) сопоставляются с письмом и профилем, панель показывает
вердикт (apply / apply_with_caveats / skip), скор и расшифровку
закрыто / с оговоркой / не закрыто.
Движок задаётся полем fitEngine в ~/.config/covercraft/settings.json:
""/"deterministic"(по умолчанию) — матчер на правилах: токены, синонимы, отрицания, мосты, концепт-признаки. Ноль дополнительных LLM-вызовов, воспроизводим."llm"— гибрид: модель размечает покрытие каждого must-have цитатами, код верифицирует цитаты и отрицания и считает вердикт. Один дополнительный LLM-вызов на генерацию; при ошибке модели, таймауте или битом JSON — автоматический откат на детерминированный матчер.
Концептные требования (навыки, названные словами, а не технологиями — «опыт интеграции с платёжными процессингами») матчатся по концептам: триггер в формулировке требования + минимум два сигнала-факта в письме или профиле.
- Из коробки работает встроенный набор концептов (
DefaultConcepts). - При
fitEngine: "llm"концепты дополнительно генерируются моделью из вашегоcontext/*.md(один LLM-вызов при первомgenerate) и кэшируются в~/.config/covercraft/concepts.jsonс SHA-256 хэшем профиля. - Кэш инвалидруется автоматически при изменении профиля: хэш не совпал —
перегенерация. Файл — обычный JSON (
name,trigger_terms,signals), можно править вручную: правка работает, пока профиль не менялся. - Нет валидного кэша и недоступна LLM (нет ключа, таймаут, битый JSON) — используется встроенный набор: поведение системы не деградирует.
main.go точка входа: сервер + окно
frontend/index.html весь UI (embed)
internal/settings/ JSON-настройки, 0600, атомарная запись
internal/llm/ OpenAI-совместимый клиент /chat/completions,
reasoning_effort (omitempty), timeout 300s
internal/cover/ сборка user-промпта из context/*.md,
ProfileSections/DropSections — отбор разделов
internal/prompt/ отбор разделов профиля под вакансию
(POST /api/prompt/compose)
internal/fit/ фит-матчер: извлечение требований, покрытия,
вердикт; concepts.go — динамические концепты
(LLM-генерация, кэш ~/.config/covercraft/concepts.json)
internal/server/ HTTP-роутер + швы LLMFunc/LLMStreamFunc для тестов,
/api/generate стримит SSE: дельты + done (letter,
elapsedMs, warnings, fit, наблюдаемость отправки);
streamParams — вход одного запуска письма
internal/window/ CGO: GTK-окно + WebKit2GTK 4.1
internal/audit/ постпроверка письма: запрещённые паттерны,
потерянные факты, дубли, атрибуция метрик
context/ user's context: *.md (gitignored, private)
Полный обзор для AI-агентов — llms.txt. Правила контрибуции — CONTRIBUTING.md.
| Метод | Путь | Что делает |
|---|---|---|
| GET | / |
UI (HTML из embed) |
| GET | /api/settings |
текущие настройки |
| POST | /api/settings |
сохранить настройки |
| POST | /api/generate |
{vacancy, systemPrompt, dropSections, letter} → SSE: delta…, done {letter, elapsedMs, warnings, fit, profileWarning, usedSystemPrompt, appliedDropSections, usedUserPromptBytes, usedProfileBytes} |
| POST | /api/prompt/compose |
{vacancy} → {dropSections, reason, sections, droppedBytes, elapsedMs} — отбор разделов, системный промпт не возвращается |
| GET | /api/presets |
список пресетов и activePresetId |
| POST | /api/presets |
{name, systemPrompt} → создать пресет (имя ≤80 символов; длина промпта не ограничена — как у поля #systemPrompt) |
| PUT | /api/presets |
{id, name?, systemPrompt?} → обновить пресет (пустые поля = не менять) |
| DELETE | /api/presets |
{id} → удалить пресет; удаление активного снимает activePresetId |
| POST | /api/presets/active |
{id} → активировать пресет; {id: ""} — снять привязку (кастомный промпт) |
Поля наблюдаемости нужны, чтобы отличить намерение от факта: композер
прислал dropSections, но профиль могли поправить между вызовами, и часть дропов
тогда ничего не вырезает.
| Поле | Что значит |
|---|---|
appliedDropSections |
Дропы, реально вырезанные из профиля. Может быть короче запрошенного dropSections — UI обязан показать расхождение, а не рапортовать «вырезано N». |
usedUserPromptBytes |
Длина user-промпта, реально отправленного модели. |
usedProfileBytes |
Длина профиля, ушедшего в фит. |
usedSystemPrompt |
Фактически отправленный системный промпт: кастом или дефолт. |
usedUserPromptBytes и usedProfileBytes сериализуются всегда, чтобы клиент
отличал «0 байт» от «поле не пришло». warnings предупреждает о prompt ≥8 КБ и
≥4× от дефолта, а также о неприемлемом размере ответа — когда модель вернула
кусок профиля или промпта вместо письма, в warnings попадает причина, и вердикт
фита при таком ответе не отправляется.
go test ./...Живой интеграционный прогон (TestAFLTLiveEndToEnd, internal/fit) по умолчанию
пропускается: платные вызовы к модели, нужен CC_LIVE=1 и промпт v4 по пути из
CC_V4_PROMPT. Ключ никогда не берётся из исходников — только из
~/.config/covercraft/settings.json.
Юнит-тесты на все пакеты: настройки (roundtrip, битый JSON, права 0600), LLM-клиент (запрос/ответ/ошибки через httptest; reasoning_effort отправляется только когда задан), сборка промпта, HTTP-эндпоинты (включая 400/502 и elapsedMs), embed-фронтенд (копирайт, селект reasoning effort), постпроверка писем (запрещённые паттерны, потерянные факты, дубли, атрибуция метрик) и режим автоправки.
Отдельно стоит детектор эха (isPromptEcho): он ловит модель, выдавшую кусок
профиля или промпта вместо письма, и не должен срабатывать на обычной
содержательной правке письма. Сценарии закрыты в internal/server; на живых
письмах (PHP, SRE, AI-product, fullstack) зафиксированы правые требования,
которые ранее ложно уходили в missing.
- Сервер печатает URL в stderr:
covercraft: http://127.0.0.1:PORT. - Логи запросов — там же (
GET /,POST /api/generate). GDK_BACKEND=x11 ./covercraft— окно через Xwayland (удобно для скриншотов и xprop).
Артефакты релиза собираются в dist/ (не в git). В бинарник вшивается
версия модуля из тега git: собирайте после тега из чистого дерева
(иначе в go version -m уедет +dirty):
Версия ниже берётся из тега — так артефакт всегда совпадает с содержимым репозитория, и пример не устаревает при следующем релизе:
V=0.4.0 # версия релиза
mkdir -p dist
go build -o covercraft .
tar czf dist/covercraft_${V}_linux_amd64.tar.gz covercraft README.md LICENSE
(cd dist && sha256sum covercraft_${V}_linux_amd64.tar.gz \
> covercraft_${V}_linux_amd64.tar.gz.sha256)sha256sum запускается из dist/, иначе в файл суммы попадёт путь с
префиксом dist/ и проверка sha256sum -c из самого каталога не пройдёт.
context/ в архив не входит: он приватен и создаётся каждым
пользователем под себя. Dev-пакеты нужны только для сборки; для запуска
достаточно рантайма libwebkit2gtk-4.1-0 (есть в Ubuntu 24.04+).
MIT — см. LICENSE.
