From bfc1b3056b069a71263fb417c469c5e58c5261aa Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:17:47 +0400
Subject: [PATCH 01/21] =?UTF-8?q?spec(sdlc-rename):=20=D0=BF=D0=B5=D1=80?=
=?UTF-8?q?=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?=
=?UTF-8?q?=D0=B5=20jira-hub=20=E2=86=92=20sdlc-hub?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Change sdlc-rename: пакет, CLI, каталог данных, MCP sdlc-jira/sdlc-wiki,
бренд и ключи хранилища веба, скрипт переключения живого хаба (#97).
Co-Authored-By: Claude Opus 5.5
---
openspec/changes/sdlc-rename/.openspec.yaml | 2 +
openspec/changes/sdlc-rename/design.md | 103 ++++++++
openspec/changes/sdlc-rename/proposal.md | 42 +++
.../sdlc-rename/specs/data-model/spec.md | 41 +++
.../changes/sdlc-rename/specs/hub-api/spec.md | 236 +++++++++++++++++
.../changes/sdlc-rename/specs/mcp/spec.md | 243 ++++++++++++++++++
.../sdlc-rename/specs/migration/spec.md | 39 +++
.../changes/sdlc-rename/specs/web-app/spec.md | 169 ++++++++++++
.../changes/sdlc-rename/specs/wiki/spec.md | 33 +++
9 files changed, 908 insertions(+)
create mode 100644 openspec/changes/sdlc-rename/.openspec.yaml
create mode 100644 openspec/changes/sdlc-rename/design.md
create mode 100644 openspec/changes/sdlc-rename/proposal.md
create mode 100644 openspec/changes/sdlc-rename/specs/data-model/spec.md
create mode 100644 openspec/changes/sdlc-rename/specs/hub-api/spec.md
create mode 100644 openspec/changes/sdlc-rename/specs/mcp/spec.md
create mode 100644 openspec/changes/sdlc-rename/specs/migration/spec.md
create mode 100644 openspec/changes/sdlc-rename/specs/web-app/spec.md
create mode 100644 openspec/changes/sdlc-rename/specs/wiki/spec.md
diff --git a/openspec/changes/sdlc-rename/.openspec.yaml b/openspec/changes/sdlc-rename/.openspec.yaml
new file mode 100644
index 0000000..1f5d280
--- /dev/null
+++ b/openspec/changes/sdlc-rename/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-10-24
diff --git a/openspec/changes/sdlc-rename/design.md b/openspec/changes/sdlc-rename/design.md
new file mode 100644
index 0000000..195dea5
--- /dev/null
+++ b/openspec/changes/sdlc-rename/design.md
@@ -0,0 +1,103 @@
+## Context
+
+`main` bb54d0a: схема v7, модули `jira` и `wiki`, агенты jira v14 и wiki v1. Имя `jira-hub` живёт в:
+
+- **коде:** пакет `src/jira_hub/` (≈30 файлов импортируют `jira_hub`), `pyproject.toml` (дистрибутив и скрипт
+ `jira-hub`), `core/paths.py` (`JIRA_HUB_HOME`, `~/.local/share/jira-hub`), `core/cli.py` (`prog`, модуль MCP по
+ умолчанию `jira`), `core/mcp.py` (`mcp_json`: серверы `jira-hub`/`jira-hub-<модуль>`), `FastMCP("jira-hub")` и
+ `FastMCP("jira-hub-wiki")`, подсказки «jira-hub serve», `api.py` (текст «make web в каталоге jira-hub», строка запуска);
+- **тестах:** `JIRA_HUB_HOME` в фикстурах, эталон `tests/fixtures/baseline/mcp_wiki_tools.expected.json`, проверки имён
+ серверов и фрагментов `.mcp.json`, пути `src/jira_hub` в `test_layout`, `test_registry`, `test_web_app`,
+ `test_wiki_projects`;
+- **вебе:** `Хаб Jira`, бренд в `shell/Sidebar.tsx`, ключи `jira-hub-token|sidebar|legend`,
+ `JIRA_HUB_URL` в `vite.config.ts`, `web/scripts/test-hub.sh`, `package.json` (`jira-hub-web`), подсказка входа;
+- **документации:** README (launchd, MCP, выкатка), CLAUDE.md, ARCHITECTURE.md, Makefile, `openspec/specs/` (18
+ требований в 7 спеках);
+- **проектах-потребителях** (не git): `awp-ca` — `.mcp.json`, `enabledMcpjsonServers` в `.claude/settings.local.json`,
+ `CLAUDE.md`, скилл `building-customer-error-report` (`mcp__jira-hub__*`); `stocks_drrk` — `.mcp.json`,
+ `enabledMcpjsonServers`;
+- **живой установке:** launchd `ru.aepif.jira-hub` (`uv run --project …/jira-bridge/jira-hub jira-hub serve`, лог
+ `~/Library/Logs/jira-hub.log`), данные `~/.local/share/jira-hub/`, `.mcp.json` в `~/projects/awp-ca` и
+ `~/projects/stocks_drrk` (серверы `jira-hub`, `jira-hub-wiki`), user scope MCP `jira-hub`, репозиторий Gitea
+ `aep/jira-hub`, checkout `~/projects/jira-bridge/jira-hub` с worktree в `.wt/`;
+- **соседях:** jira-agent и eva-agent упоминают `jira-hub` только в документации, `agent.example.json` и e2e-скрипте
+ eva-agent (`JIRA_HUB_HOME`, `jira-hub` CLI). Протокол агента имени хаба не содержит: агенты ходят через общую папку.
+
+## Goals / Non-Goals
+
+**Goals:** одно имя `sdlc-hub` в коде, CLI, данных, MCP, вебе и документации; живой хаб переключается одним скриптом
+владельца с сухим прогоном; новый код не может молча работать с пустым каталогом данных рядом с живым.
+
+**Non-Goals и Anti-Goals:** см. proposal.md.
+
+## Decisions
+
+1. **Имя — `sdlc-hub`** (решение владельца 23.10.2026). Бренд в вебе — «SDLC Hub».
+2. **Псевдонима `jira-hub` нет.** Старые записи MCP и launchd после переключения должны падать явно, а не работать
+ «почти». Переключение делает скрипт сразу для всех известных потребителей.
+3. **MCP-серверы `sdlc-<модуль>`**, модуль в `sdlc-hub mcp` обязателен. Модуль по умолчанию был нужен только ради
+ неизменных записей `.mcp.json` в hub-modules; при переименовании записи меняются всё равно. Имена инструментов в
+ Claude станут `mcp__sdlc-jira__*` и `mcp__sdlc-wiki__*`.
+4. **Каталог данных не переносится кодом.** Хаб, MCP-серверы открытых сессий Claude и CLI открывают одну базу; перенос
+ «на лету» при старте любого из них рискует WAL и гонкой. Код только отказывает с подсказкой (требование «Каталог
+ данных прежнего имени»), перенос — шаг скрипта при остановленном хабе.
+5. **Скрипт переключения в репозитории** (`scripts/cutover.sh`), сухой прогон по умолчанию. Шагов много, они касаются
+ launchd, двух чужих репозиториев и user scope Claude; руками их легко сделать не в том порядке (I-2 wiki-module).
+ Скрипт одноразовый, после переключения остаётся в истории как образец; тесты — на сухой прогон и проверки шага 1
+ (во временном `HOME`), без launchd.
+6. **Переименование репозитория в Gitea — руками владельца** в интерфейсе (Settings → Repository name). Gitea оставляет
+ перенаправление со старого адреса; `tea` и `origin` после этого работают по новому адресу. Скрипт проверяет, что
+ переименование сделано, и меняет `origin`.
+7. **Checkout переезжает в `jira-bridge/sdlc-hub`**, workspace `jira-bridge` остаётся: от его пути зависят каталог
+ памяти Claude, workspace `CLAUDE.md`, путь общей папки реле (`…/utm/rshb/jira-bridge`) и настройки агентов на
+ виртуалке. Переименование workspace — отдельно, если понадобится.
+8. **Ключи `localStorage` — с разовым переносом** из `jira-hub-*`: браузер владельца не должен снова просить токен и
+ терять свёрнутость панели и легенды. Ключи доски `graph-*` имени хаба не содержат и не меняются.
+9. **История не переписывается**: `docs/superpowers/plans/`, `openspec/changes/archive/` остаются со старыми именами.
+ Раздел `Purpose` спек `hub-api`, `mcp`, `web-app` (дельтами не меняется) правится в PR архива.
+10. **Агенты без релиза.** `src/agent.py` и `src/run.py` jira-agent и eva-agent не трогаются (строки документации — со
+ следующим релизом); README, CLAUDE.md, ARCHITECTURE.md, `agent.example.json` и `scripts/e2e-hub.sh` eva-agent —
+ мелкими PR в своих репозиториях (bounded, без change).
+11. **Порядок с #98 (веб под модули).** Переименование сливается первым; ветка #98 перебазируется на него. Бренд и
+ ключи хранилища меняет этот change, устройство боковой панели — #98.
+
+## Переключение (для владельца)
+
+1. Слить PR этого change; переименовать репозиторий в Gitea `aep/jira-hub` → `aep/sdlc-hub`.
+2. Закрыть или не трогать сессии Claude с MCP хаба (после переключения — `/mcp` reconnect).
+3. `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh` — сухой прогон, проверить вывод.
+4. `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh --apply`.
+5. Просмотреть правки в awp-ca и stocks_drrk (`.mcp.json`, `.claude/`, `CLAUDE.md` awp-ca; копии `*.pre-sdlc` рядом) и
+ удалить копии. Это не git-репозитории — коммитить нечего.
+6. Проверить: `agent_status` через `sdlc-jira`, `status` через `sdlc-wiki`, веб `http://127.0.0.1:8790/app/` без
+ повторного входа.
+
+Откат: остановить `ru.aepif.sdlc-hub`, перенести каталоги обратно, вернуть plist `ru.aepif.jira-hub` из README
+предыдущей версии, `git checkout` прежнего коммита, вернуть записи MCP — порядок в README.
+
+## Risks / Trade-offs
+
+- **Сессии Claude с открытым MCP** держат старый путь к пакету: после переноса checkout их серверы падают до
+ reconnect. Принято: переключение — разовое событие владельца.
+- **codegraph-индексы** в `.codegraph/` checkout и worktree содержат абсолютные пути: после переноса — `codegraph init`
+ заново (шаг в сводке скрипта).
+- **Гонка с #98**: массовый перенос `src/jira_hub` → `src/sdlc_hub` конфликтует с любой веткой, трогающей Python;
+ #98 — только `web/`, поэтому конфликт ограничен брендом и ключами хранилища (решение 11).
+
+## Decisions made without discussion
+
+Владелец 24.10.2026 ответил «делай» на вопросы brainstorming — все решения ниже приняты агентом.
+
+| # | Дата | Решение | Обоснование | Цена ошибки |
+|---|---|---|---|---|
+| 1 | 24.10.2026 | Бренд «SDLC Hub» | имя из решения владельца, латиница как в имени репозитория | правка строки в вебе |
+| 2 | 24.10.2026 | Без псевдонима `jira-hub` | явный отказ лучше тихой работы старых записей | вернуть точку входа в `pyproject.toml` |
+| 3 | 24.10.2026 | MCP `sdlc-jira`/`sdlc-wiki`, модуль обязателен | симметрия модулей, ядро без «jira по умолчанию» | переименовать серверы ещё раз во всех `.mcp.json` |
+| 4 | 24.10.2026 | Каталог данных переносит скрипт, код только отказывает | гонка открытых баз при переносе на лету | ручной `mv` при остановленном хабе |
+| 5 | 24.10.2026 | Скрипт `scripts/cutover.sh` с сухим прогоном | много шагов в чужих конфигурациях, порядок важен | шаги руками по README |
+| 6 | 24.10.2026 | Gitea переименовывает владелец | действие на сервере, вне репозитория | — |
+| 7 | 24.10.2026 | Workspace `jira-bridge` не переименовывается | от пути зависят память Claude, реле, агенты | отдельный change позже |
+| 8 | 24.10.2026 | Перенос ключей `localStorage` | не просить токен заново | повторный вход в веб |
+| 9 | 24.10.2026 | История (планы, архив) не меняется | это запись прошлого | — |
+| 10 | 24.10.2026 | Агенты без релиза, документация — мелкими PR | протокол не меняется | строки документации в `agent.py` со старым именем до следующего релиза |
+| 11 | 24.10.2026 | Сначала переименование, потом #98 | #98 перебазируется на новые пути | перебазирование веток |
diff --git a/openspec/changes/sdlc-rename/proposal.md b/openspec/changes/sdlc-rename/proposal.md
new file mode 100644
index 0000000..fc1dc7f
--- /dev/null
+++ b/openspec/changes/sdlc-rename/proposal.md
@@ -0,0 +1,42 @@
+## Why
+
+Хаб перестал быть только мостом Jira: с hub-modules (#87) он состоит из ядра и модулей, с wiki-module (#91) — второй
+модуль (EVA Wiki) со своим агентом и своим MCP. Имя `jira-hub` теперь вводит в заблуждение: в вебе «Хаб Jira», MCP вики
+называется `jira-hub-wiki`, данные вики лежат в `~/.local/share/jira-hub/`. Решение владельца 23.10.2026 — хаб
+становится **sdlc-hub**; переименование было отложено до готовности модулей. 24.10.2026 владелец решил делать его сейчас,
+вместе с пересборкой веба под модули (jira-hub#98), «раз мы причёсываем визуал».
+
+## What Changes
+
+- **Пакет и CLI.** Пакет `jira_hub` → `sdlc_hub`, дистрибутив `jira-hub` → `sdlc-hub`, команда `jira-hub` → `sdlc-hub`.
+ Псевдонима `jira-hub` нет.
+- **MCP по модулю без модуля по умолчанию.** `sdlc-hub mcp <модуль> [--project <метка>]`; модуль обязателен. Серверы
+ называются `sdlc-<модуль>`: `sdlc-jira`, `sdlc-wiki`. Фрагмент `.mcp.json` из `project_create` — с новыми именами.
+- **Каталог данных.** `~/.local/share/sdlc-hub/`, переменная `SDLC_HUB_HOME`. Если нового каталога нет, а прежний
+ `~/.local/share/jira-hub/` есть, любая команда хаба, открывающая базу, отказывает с текстом, как перенести каталог;
+ сама ничего не переносит.
+- **Веб.** Бренд «SDLC Hub» (заголовок вкладки, боковая и верхняя панели), ключи `localStorage` `sdlc-hub-*` с разовым
+ переносом значений из `jira-hub-*`, подсказка входа `uv run sdlc-hub tokens`, `npm run dev` — `SDLC_HUB_URL`.
+- **Переключение живого хаба.** Скрипт `scripts/cutover.sh` (сухой прогон по умолчанию, `--apply` — выполнить) для
+ владельца: останавливает launchd `ru.aepif.jira-hub`, переносит каталог данных, ставит `ru.aepif.sdlc-hub` с логом
+ `~/Library/Logs/sdlc-hub.log`, переписывает записи MCP в `.mcp.json` известных проектов и в user scope, переносит
+ checkout `jira-bridge/jira-hub` → `jira-bridge/sdlc-hub` и чинит worktree. Репозиторий в Gitea `aep/jira-hub` →
+ `aep/sdlc-hub` владелец переименовывает в интерфейсе Gitea, скрипт затем меняет `origin`.
+- **Документация.** README, CLAUDE.md, ARCHITECTURE.md, текущие спеки (`openspec/specs/`) — на новые имена; workspace
+ `CLAUDE.md` и упоминания в jira-agent/eva-agent (README, CLAUDE.md, ARCHITECTURE.md, `agent.example.json`, e2e-скрипт
+ eva-agent) — отдельными мелкими PR в своих репозиториях.
+
+## Non-Goals
+
+- Пересборка веба под модули — jira-hub#98, отдельный change.
+- Переименование workspace `jira-bridge`, общей папки реле (`relay_dir`), репозиториев агентов, launchd агентов на
+ виртуалке.
+- Правка `src/agent.py` и `src/run.py` агентов: строки документации в них меняются со следующим релизом агента.
+- Правка истории: `docs/superpowers/plans/`, `openspec/changes/archive/`.
+- Изменения протокола агента, HTTP API (кроме текстов), схемы базы, токенов.
+
+## Anti-Goals
+
+- Живой хаб не должен оказаться в состоянии «код новый, данные/launchd старые» без явной ошибки: молчаливо создать пустой
+ `~/.local/share/sdlc-hub/` рядом с живыми данными — провал, даже если тесты зелёные.
+- Агенты на виртуалке не требуют ни перенастройки, ни нового релиза.
diff --git a/openspec/changes/sdlc-rename/specs/data-model/spec.md b/openspec/changes/sdlc-rename/specs/data-model/spec.md
new file mode 100644
index 0000000..bf8dcd9
--- /dev/null
+++ b/openspec/changes/sdlc-rename/specs/data-model/spec.md
@@ -0,0 +1,41 @@
+## MODIFIED Requirements
+
+### Requirement: Настройки
+Таблица `setting` SHALL хранить парами ключ–JSON: `project_key`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
+`transitions_map`, `pacing`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой
+правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или `null`, по умолчанию `null`;
+относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию
+`["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением
+по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование
+«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Оставшиеся от v3 ключи хаб SHALL NOT использовать, кроме
+передачи `board_id` агенту как запасной доски для команд без доски в параметрах (поставленных до миграции);
+`GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через
+`PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json`
+командой `sdlc-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект
+`{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига
+`issue_type_<тип>` и `issue_type_<тип>_id`; `bug` попадает в `issue_types`, только если в конфиге есть хотя бы
+один из ключей `issue_type_bug`, `issue_type_bug_id` (`task` и `epic` — как раньше, всегда).
+
+#### Scenario: Правка настроек поднимает версию
+- **WHEN** через API изменён `pacing`
+- **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию
+
+#### Scenario: Конфиг без ключей ошибки
+- **WHEN** `sdlc-hub init --config` с `issue_type_task`, `issue_type_epic`, без `issue_type_bug`
+- **THEN** `issue_types` содержит `task` и `epic`, `bug` нет; постановка `ensure_issue` с `issue_type = bug` — 422
+
+#### Scenario: Неверный relay_dir
+- **WHEN** `PUT /settings` с `relay_dir = "rshb/jira-bridge"`
+- **THEN** 422, значение не записано, `settings_version` не изменился
+
+#### Scenario: Закрытые статусы по умолчанию
+- **WHEN** хаб открыл базу, в которой нет ключа `closed_statuses`
+- **THEN** `GET /settings` отдаёт `closed_statuses = ["Готово", "Снято"]`; `claim` агенту этого ключа не передаёт
+
+#### Scenario: Запись базовой JQL
+- **WHEN** `PUT /settings {base_jql: "project = AURORA"}`
+- **THEN** 422 «базовая JQL собирается из проектов»; `settings_version` не изменился
+
+#### Scenario: Создание проекта не трогает версию настроек
+- **WHEN** создан проект `STOCKS_DRRK`
+- **THEN** `settings_version` прежний; `GET /settings` отдаёт `base_jql` с обеими метками
diff --git a/openspec/changes/sdlc-rename/specs/hub-api/spec.md b/openspec/changes/sdlc-rename/specs/hub-api/spec.md
new file mode 100644
index 0000000..b998f2d
--- /dev/null
+++ b/openspec/changes/sdlc-rename/specs/hub-api/spec.md
@@ -0,0 +1,236 @@
+## MODIFIED Requirements
+
+### Requirement: Процесс и доступ
+`sdlc-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:` (по умолчанию 8790), база и настройки в
+`~/.local/share/sdlc-hub/` (переопределяется `SDLC_HUB_HOME`). Все маршруты API SHALL требовать
+`Authorization: Bearer `: токен агента модуля открывает только `/agent/*` (маршруты ядра — от имени этого агента,
+маршруты модуля со scope `agent` — только агенту своего модуля, см. `modules`, «Агенты модулей»), клиентский — всё
+остальное. `sdlc-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение —
+раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без
+данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или
+`Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки
+дополнительно отвечает 502. Методы, кроме GET, POST, PUT и PATCH, отвечают 501 стандартной страницей `http.server`, не
+JSON.
+
+#### Scenario: Чужой токен
+- **WHEN** токеном агента `jira` вызван `GET /issues`
+- **THEN** 401
+
+#### Scenario: Неподдерживаемый метод
+- **WHEN** `DELETE /issues/AURORA-1`
+- **THEN** 501, тело не JSON; база не меняется
+
+#### Scenario: Страница без токена
+- **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть
+- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет
+
+#### Scenario: Токены после миграции
+- **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `sdlc-hub tokens`
+- **THEN** вывод содержит клиентский токен, `jira: T` и новый токен `wiki`
+
+### Requirement: Реле через общую папку
+`sdlc-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле
+бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если
+каталог `relay_dir` существует, реле SHALL создать в нём подкаталоги `req/`, `resp/`, `work/`; если не существует —
+записать одно предупреждение в лог и проверять снова.
+
+Обмен:
+- запрос агента — файл `req/.json`: `{id, method, path, body, authorization, deadline}`, где `deadline` — момент UTC
+ (ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать;
+- реле SHALL брать запрос переименованием в `work/.json` и обрабатывать каждый взятый запрос в отдельном потоке;
+- запрос с истёкшим `deadline` SHALL выбрасываться без ответа; `POST /agent/claim` SHALL пересылаться, только если до
+ `deadline` осталось не меньше 35 с, иначе выбрасываться без ответа;
+- остальные запросы реле SHALL пересылать в свой HTTP на `:` процесса `serve` (`127.0.0.1`, если `serve`
+ слушает `0.0.0.0`) тем же методом, путём и телом, с заголовком `Authorization` из файла, и записывать ответ
+ `resp/.json` = `{id, status, body}` (тело — строка ответа HTTP) через временный файл с именем на точку и
+ переименование; затем удалять `work/.json`;
+- если соединение со своим HTTP не удалось или ответ нарушает протокол HTTP, ответ SHALL иметь `status = 502` и текст
+ ошибки в `body`;
+- нечитаемый JSON запроса SHALL перечитываться до 2 с, затем запрос выбрасывается с записью в лог.
+
+При первой подготовке каждой папки после старта реле (в том числе новой папки после смены `relay_dir`) реле SHALL
+удалить файлы из `work/` с записью в лог. Повторная подготовка той же папки (пропал подкаталог, не было доступа) SHALL
+только создавать недостающие подкаталоги, не трогая `work/`. Раз в минуту
+реле SHALL удалять ответы в `resp/` старше 10 мин, запросы в `req/` с `deadline` старше 60 с и файлы с именем на точку
+старше 10 мин. На каждый обработанный запрос реле SHALL писать в лог хаба строку `relay
+<секунды>`. HTTP API, обработчики и проверка токенов при этом не меняются.
+
+#### Scenario: Запрос через папку
+- **WHEN** в `req/` появился `a1.json` с `GET /agent/settings`, агентским токеном и `deadline` через 60 с
+- **THEN** в `resp/` появляется `a1.json` со `status = 200` и телом настроек; `work/a1.json` удалён
+
+#### Scenario: Чужой токен через папку
+- **WHEN** в файле запроса `GET /issues` с агентским токеном
+- **THEN** ответ со `status = 401`, как по HTTP
+
+#### Scenario: Просроченный claim
+- **WHEN** реле взяло `POST /agent/claim`, до `deadline` которого осталось 20 с
+- **THEN** запрос выброшен без ответа и без обращения к хабу; команда в очереди осталась `queued`
+
+#### Scenario: Долгий claim не задерживает другие запросы
+- **WHEN** реле пересылает `claim`, который ждёт команду 30 с, и в это время в `req/` появляется `GET /agent/settings`
+- **THEN** ответ на `settings` пишется, не дожидаясь `claim`
+
+#### Scenario: Папка не подключена
+- **WHEN** `relay_dir` задан, но каталога нет; через минуту каталог появился
+- **THEN** в логе одно предупреждение; после появления каталога реле создаёт подкаталоги и обрабатывает запросы
+
+#### Scenario: Сбой протокола своего HTTP
+- **WHEN** при пересылке запроса свой HTTP оборвал ответ (`http.client.HTTPException`)
+- **THEN** в `resp/` ответ со `status = 502` и текстом ошибки; реле продолжает работу
+
+#### Scenario: Повторная подготовка не трогает взятые запросы
+- **WHEN** реле работает, в `work/` лежит взятый запрос, и пропал подкаталог `resp/`
+- **THEN** реле создаёт `resp/` заново; файл в `work/` не удалён, в логе нет «удалён незавершённый запрос»
+
+### Requirement: Публикация версии агента
+`sdlc-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`;
+неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ,
+ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной для этого модуля. Файл SHALL записываться в
+`/release/<модуль>/agent.py` (каталоги 0700), затем `manifest.json` `{version, sha256, size,
+published_at}`; оба — через временный файл с `fsync` и переименование, манифест последним. Прежний `agent.py` на время
+публикации SHALL переименовываться в `agent.py.prev` и удаляться после записи манифеста; если запись нового файла или
+манифеста не удалась (`OSError`), хаб SHALL вернуть `agent.py.prev` на место `agent.py`, удалить временные файлы и
+завершить команду сообщением «не удалось записать публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя
+версия. Команда SHALL работать без запущенного `serve` и печатать «опубликована v, sha256 <первые 12 символов>…,
+ байт»; при ошибке — сообщение и код 1, опубликованное не меняется. При старте хаб SHALL один раз перенести
+прежнюю публикацию `release/agent.py` и `release/manifest.json` в `release/jira/`, если `release/jira/` ещё нет; ошибка
+переноса — предупреждение в лог, хаб стартует. Хаб SHALL читать манифест модуля агента при каждом `claim`,
+`GET /agent/release` и `GET /agent`; манифест годен, только если это объект, `version` — целое число (не логическое) не
+меньше 1, `sha256` — строка из 64 символов `[0-9a-f]`; негодный или нечитаемый манифест означает «публикации нет» и
+предупреждение в лог — одно на каждый новый текст ошибки. Файл `agent.py`, не декодируемый как UTF-8 при совпавшем
+sha256, означает «публикации нет» (`GET /agent/release` — 404 `no_release`). `GET /agent?module=<модуль>` (без
+параметра — `jira`) и каждый элемент `GET /agents` SHALL дополняться полями `release` (`{version, sha256,
+published_at}` или `null`), `update_state`, `launcher_version`, `agent_reported` (был ли `claim` этого агента с момента
+запуска хаба) и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии
+агента, агент онлайн, `agent_reported = true`, `launcher_version = null`), «откат с v на v: »
+(`rolled_back`), «v отвергнута: » (`rejected`), иначе `null`.
+
+#### Scenario: Публикация новой версии
+- **WHEN** опубликована v5, `sdlc-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6`
+- **THEN** в `release/jira/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6,
+ sha256 …»; код 0
+
+#### Scenario: Публикация eva-agent
+- **WHEN** `sdlc-hub agent publish --module wiki eva-agent/src/agent.py` с `AGENT_VERSION = 1`, опубликована jira v14
+- **THEN** в `release/wiki/` — v1; `claim` агента `wiki` получает `agent_release {version: 1, …}`, `claim` агента `jira` —
+ `{version: 14, …}`
+
+#### Scenario: Неизвестный модуль
+- **WHEN** `sdlc-hub agent publish --module nope agent.py`
+- **THEN** код 2, в stderr «неизвестный модуль nope» и список модулей с агентами; `release/` не изменён
+
+#### Scenario: Версия не выше опубликованной
+- **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6`
+- **THEN** «опубликована v6, новая должна быть больше», код 1; `release/` не изменён
+
+#### Scenario: Нет строки версии
+- **WHEN** публикуется файл без строки `AGENT_VERSION = <целое>` или с двумя такими строками
+- **THEN** сообщение об ошибке, код 1; `release/` не изменён
+
+#### Scenario: Диск полон при записи манифеста
+- **WHEN** опубликована v8, публикуется v9, запись `manifest.json` завершилась `OSError`
+- **THEN** «не удалось записать публикацию: …», код 1, без трейсбека; `agent.py` и манифест — прежние v8,
+ `GET /agent/release` отдаёт v8; временных файлов и `agent.py.prev` в `release/jira/` нет
+
+#### Scenario: Негодный манифест
+- **WHEN** в `manifest.json` `version = 6.9` (или `true`, или `sha256` короче 64 символов), хаб получает 100 `claim`
+- **THEN** в `claim` `agent_release = null`, `GET /agent/release` — 404; в логе одно предупреждение о манифесте
+
+#### Scenario: Агент без запускателя
+- **WHEN** опубликована v6, агент v5 онлайн, в последнем `claim` `launcher_version = null`
+- **THEN** `GET /agent` → `agent_reported = true`, `update_hint` = «агент запущен без run.py — обновится только вручную»
+
+#### Scenario: Хаб перезапущен, агент ещё не выходил на связь
+- **WHEN** опубликована v10, агент v9 под запускателем, хаб перезапущен, `claim` после запуска ещё не было
+- **THEN** `GET /agent` → `agent_reported = false`, `launcher_version = null`, `update_hint = null`; после первого
+ `claim` — `agent_reported = true`, `launcher_version` из `claim`
+
+#### Scenario: Перенос прежней публикации
+- **WHEN** хаб с `release/agent.py` v14 и манифестом без `release/jira/` запущен на новом коде
+- **THEN** публикация v14 лежит в `release/jira/`, `claim` агента `jira` получает `agent_release {version: 14, …}`
+
+### Requirement: Раздача веб-приложения
+Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/sdlc_hub/web/app/`, читая файлы
+с диска на каждый запрос:
+- `GET /app/assets/<путь>` — файл из `assets/` сборки; путь, который после разрешения выходит за `assets/`, не
+ существует или содержит нулевой байт, — 404; `Content-Type` по расширению (`.js` — `text/javascript; charset=utf-8`, `.css` —
+ `text/css; charset=utf-8`, `.svg` — `image/svg+xml`, `.png` — `image/png`, `.ico` — `image/x-icon`, `.woff2` —
+ `font/woff2`, `.map` — `application/json`, прочее — `application/octet-stream`);
+ `Cache-Control: public, max-age=31536000, immutable`;
+- любой другой `GET /app` или `GET /app/…` — `index.html` сборки, `text/html; charset=utf-8`, `Cache-Control: no-cache`;
+- если `index.html` сборки нет — на любой `GET /app…` 503 `text/html; charset=utf-8` с текстом, что приложение не
+ собрано и нужно выполнить `make web` в каталоге sdlc-hub;
+- `GET /` и `GET /links` — 302 с `Location: /app/board`.
+Сборка приложения не хранится в git; её создаёт `make web`.
+
+#### Scenario: Файл сборки
+- **WHEN** в сборке есть `assets/index-abc123.js`, запрошен `GET /app/assets/index-abc123.js`
+- **THEN** 200, `Content-Type: text/javascript; charset=utf-8`, `Cache-Control` с `immutable`, тело — содержимое файла
+
+#### Scenario: Путь приложения
+- **WHEN** запрошен `GET /app/issues/AURORA-1620`
+- **THEN** 200, тело — `index.html` сборки, `Cache-Control: no-cache`
+
+#### Scenario: Выход за каталог сборки
+- **WHEN** запрошен `GET /app/assets/../../api.py` (в том числе в виде `%2e%2e`)
+- **THEN** 404, содержимое файлов вне `assets/` не отдаётся
+
+#### Scenario: Приложение не собрано
+- **WHEN** каталога сборки нет, запрошен `GET /app/board`
+- **THEN** 503, тело — HTML с подсказкой `make web`
+
+#### Scenario: Старый адрес доски
+- **WHEN** запрошен `GET /links` или `GET /`
+- **THEN** 302, `Location: /app/board`
+
+#### Scenario: Нулевой байт в пути
+- **WHEN** запрошен `GET /app/assets/%00` или `GET /app/assets/a%00.js`
+- **THEN** 404 `not_found`, а не 500
+
+## ADDED Requirements
+
+### Requirement: Каталог данных прежнего имени
+Если `SDLC_HUB_HOME` не задан, `~/.local/share/sdlc-hub/` не существует, а `~/.local/share/jira-hub/` существует, любая
+команда `sdlc-hub`, которой нужен каталог данных (`serve`, `mcp`, `tokens`, `init`, `import`, `agent publish`), SHALL
+завершиться с кодом 2 и текстом в stderr: каталог данных лежит под прежним именем, перенести его —
+`scripts/cutover.sh` (или `mv ~/.local/share/jira-hub ~/.local/share/sdlc-hub` при остановленном хабе). Команда MUST NOT
+создавать новый каталог, переносить или менять прежний. Если существуют оба каталога, используется новый, в stderr —
+предупреждение, что прежний можно удалить после проверки.
+
+#### Scenario: Код обновлён, каталог не перенесён
+- **WHEN** есть только `~/.local/share/jira-hub/`, запущен `sdlc-hub serve`
+- **THEN** код выхода 2, в stderr — подсказка о переносе; `~/.local/share/sdlc-hub/` не создан, прежний каталог не тронут
+
+#### Scenario: Задан SDLC_HUB_HOME
+- **WHEN** `SDLC_HUB_HOME=/tmp/x`, есть `~/.local/share/jira-hub/`
+- **THEN** команда работает с `/tmp/x`, проверка прежнего каталога не выполняется
+
+### Requirement: Переключение живого хаба
+`scripts/cutover.sh` SHALL переводить живую установку Mac mini со старого имени на новое. Без `--apply` скрипт SHALL
+только печатать шаги и найденные проблемы, ничего не меняя. С `--apply` — по порядку, с остановкой на первой ошибке:
+1. проверки: основной checkout `jira-bridge/jira-hub` чистый и на `main`; в Gitea доступен `aep/sdlc-hub` (владелец
+ переименовал репозиторий); `~/.local/share/sdlc-hub/` и `jira-bridge/sdlc-hub` не существуют;
+2. остановить launchd `ru.aepif.jira-hub` и удалить его plist;
+3. перенести каталог данных `~/.local/share/jira-hub` → `~/.local/share/sdlc-hub`;
+4. перенести checkout `jira-bridge/jira-hub` → `jira-bridge/sdlc-hub`, `git worktree repair`, `origin` →
+ `aep/sdlc-hub`, `git pull --ff-only`, пересоздать `.venv` (`uv sync`), `make web`;
+5. поставить и запустить launchd `ru.aepif.sdlc-hub` (`uv run --project sdlc-hub serve`, лог
+ `~/Library/Logs/sdlc-hub.log`) и дождаться ответа хаба на порту из настроек;
+6. в проектах из списка (по умолчанию `~/projects/awp-ca`, `~/projects/stocks_drrk`): в `.mcp.json` заменить записи
+ `jira-hub` → `sdlc-jira` (`sdlc-hub mcp jira`) и `jira-hub-wiki` → `sdlc-wiki` (`sdlc-hub mcp wiki`) с новым путём; в
+ `.claude/settings*.json` — имена серверов (`enabledMcpjsonServers` и т. п.) и разрешения `mcp__jira-hub__` →
+ `mcp__sdlc-jira__`, `mcp__jira-hub-wiki__` → `mcp__sdlc-wiki__`; в `CLAUDE.md` и `.claude/skills/**` — те же имена
+ серверов и инструментов. Прочие файлы проектов (заметки, отчёты) не трогаются. Перед правкой файл копируется рядом
+ с суффиксом `.pre-sdlc`; коммиты скрипт не делает;
+7. в user scope Claude Code заменить `jira-hub` на `sdlc-jira` (`claude mcp remove`/`add`).
+Каждый выполненный шаг SHALL печататься; в конце — сводка, что осталось владельцу (`/mcp` reconnect в открытых сессиях,
+`codegraph init` в checkout и worktree, удалить копии `.pre-sdlc` после проверки). Шаги откатываются в обратном порядке по инструкции из README.
+
+#### Scenario: Сухой прогон
+- **WHEN** `bash scripts/cutover.sh` без аргументов
+- **THEN** напечатаны шаги и проверки, файловая система, launchd и конфигурации Claude не изменены
+
+#### Scenario: Репозиторий в Gitea не переименован
+- **WHEN** `bash scripts/cutover.sh --apply`, `aep/sdlc-hub` в Gitea не найден
+- **THEN** скрипт останавливается на шаге 1 с подсказкой, launchd и каталоги не тронуты
diff --git a/openspec/changes/sdlc-rename/specs/mcp/spec.md b/openspec/changes/sdlc-rename/specs/mcp/spec.md
new file mode 100644
index 0000000..e0cd68e
--- /dev/null
+++ b/openspec/changes/sdlc-rename/specs/mcp/spec.md
@@ -0,0 +1,243 @@
+## MODIFIED Requirements
+
+### Requirement: Сервер и регистрация
+`sdlc-hub mcp <модуль> [--project <метка>]` SHALL запускать MCP-сервер модуля хаба по stdio (пакет `mcp`), ходящий в
+HTTP API хаба клиентским токеном из настроек; сервер называется `sdlc-<модуль>` (`sdlc-jira`, `sdlc-wiki`). Модуль
+обязателен: без него — код выхода 2 и список модулей в stderr, сервер не запускается; неизвестный модуль — так же.
+Общая регистрация — в user scope Claude Code как `sdlc-jira` (`uv run --project <путь> sdlc-hub mcp jira`), без
+проекта. Репозиторий проекта SHALL регистрировать серверы модулей в своём `.mcp.json` (project scope) с
+`--project <метка>`; эта запись перекрывает общую. Если хаб недоступен, каждый инструмент SHALL вернуть текст «хаб не
+запущен: sdlc-hub serve», а не исключение.
+
+#### Scenario: Хаб выключен
+- **WHEN** вызван `issues` при остановленном хабе
+- **THEN** ответ инструмента — одна строка с подсказкой, сервер MCP продолжает работать
+
+#### Scenario: Сервер модуля jira
+- **WHEN** запущен `sdlc-hub mcp jira --project AWPCA`
+- **THEN** сервер называется `sdlc-jira`, список инструментов и их описания совпадают с прежним сервером `jira-hub`,
+ сессия привязана к `AWPCA`
+
+#### Scenario: Модуль по умолчанию
+- **WHEN** запущен `sdlc-hub mcp --project AWPCA`
+- **THEN** модуля по умолчанию больше нет: код выхода 2, в stderr — подсказка указать модуль и список модулей (`jira`, `wiki`)
+
+#### Scenario: Неизвестный модуль
+- **WHEN** запущен `sdlc-hub mcp nope`
+- **THEN** код выхода 2, в stderr — «неизвестный модуль nope» и список модулей (`jira`, `wiki`)
+
+### Requirement: Инструменты
+| Инструмент | Параметры | Результат |
+|---|---|---|
+| `projects` | — | таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии текущий проект отмечен |
+| `project_create` | `label`, `name`, `board_id` (необязательно) | `POST /projects` с автором `claude`: проект, id пакета обновления и фрагмент `.mcp.json` для репозитория проекта (см. ниже); 409/422 — текст ошибки |
+| `issues` | фильтры как у `GET /issues` (кроме `project` — его задаёт привязка), `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
+| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (номер «#», автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет»; у комментария со ссылками на вложения — строка «файлы: # <имя>, …»; затем вложения (см. «Вложения в MCP») |
+| `comment` | `key`, `comment_id` (номер из ответа `issue`, «#» в начале допускается) | один комментарий целиком по `GET /issues/{key}`: строка « # <автор>, создан » (и «, изменён », если `updated` есть и отличается от `created`), под ней полный текст с переносами строк; нет такого номера — «у нет комментария #»; есть ссылки на вложения — после текста строка «вложения:» и по строке на каждое: «# <имя> (<размер>)» |
+| `attachment` | `key`, `attachment` (номер вложения «#» или «», иначе — точное имя файла), `wait_seconds` (по умолчанию 120, не больше 600) | скачивает вложение и отдаёт путь к локальной копии (см. «Вложения в MCP») |
+| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки |
+| `batch_create` | `title`, `commands` (список объектов по контракту `commands`; у `ensure_attachment` вместо `file` можно `path`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации |
+| `batch_status` | `id` | статусы команд, ошибки, оценка остатка |
+| `batch_wait` | `id`, `timeout_minutes` (≤ 60) | как `batch_status`, возвращается по завершении или по таймауту |
+| `batches` | `limit` | последние пакеты со статусом |
+| `command_retry` | `id` | новый пакет |
+| `batch_cancel` | `id` | вид пакета; для команд пакета, уже взятых агентом, — строка «команда #N уже у агента и будет выполнена; остановить — `POST /commands/N/cancel` с `force: true`» |
+| `refresh` | `keys` или `full`, `wait` (bool) | без `wait` — строка с id поставленного пакета; с `wait` — итог пакета, как у `batch_wait` |
+| `agent_status` | — | онлайн/офлайн, версия, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек; публикация агента («публикация: v от <дата>» или «публикации нет»), состояние обновления («обновление: v → v»), запускатель («запускатель: v», «без запускателя» или, пока `agent_reported = false`, «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)»), `update_hint`, если он есть |
+| `sync_log` | `limit` | последние `sync_run` |
+| `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` |
+
+Разрез по проекту и поведение без привязки — требование «Привязка сессии к проекту».
+
+Описание инструмента `batch_create` SHALL перечислять параметры каждого типа команды (обязательные, необязательные в
+`[ ]`, формат значения) и собираться из описания параметров в коде хаба (см. `commands`), а не вручную.
+
+`ensure_attachment` в `batch_create` SHALL принимать вместо `file` параметр `path` — абсолютный путь к локальному файлу на
+машине MCP-сервера. До постановки пакета MCP SHALL прочитать файл, загрузить его `POST /files` и заменить `path` на
+`file`; `filename` по умолчанию — имя файла из `path`. Нет файла, нет прав, не файл, больше 10 МБ — ответ инструмента
+с причиной и путём, пакет не ставится. Передан и `path`, и `file` — ответ «у ensure_attachment либо path, либо file».
+
+Описание инструмента `batch_create` SHALL сообщать, что метку проекта сессии хаб дописывает в `ensure_issue` сам, метки
+других проектов запрещены, а команды над задачами вне проекта отклоняются; `board_id` для `ensure_sprint` выставляет хаб.
+
+Ответ `project_create` SHALL содержать фрагмент `.mcp.json`
+`{"mcpServers": {"sdlc-jira": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "sdlc-hub", "mcp", "jira",
+"--project", "<метка>"]}}}`, где корень хаба — корень репозитория, из которого загружен пакет текущего процесса MCP
+(вычисляется от расположения пакета, без зашитого пути), и указание: положить файл в корень репозитория проекта (слить
+с имеющимся `.mcp.json`) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта).
+
+Автор команд из MCP SHALL быть `claude`; из API без автора — `api`.
+
+#### Scenario: Пакет из чата
+- **WHEN** `batch_create {title: "1538 в спринт 2", commands: [ensure_fields{…}, ensure_link{…}, ensure_sprint{…}]}`
+- **THEN** пакет создан с четырьмя командами (три + `refresh`), ответ содержит оценку в минутах
+
+#### Scenario: Проект из чата
+- **WHEN** сессия без привязки, `project_create {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120}`
+- **THEN** проект создан, ответ содержит id пакета обновления и фрагмент `.mcp.json` с `"--project", "STOCKS_DRRK"`
+
+#### Scenario: Ошибка с файлом из чата
+- **WHEN** `batch_create {title: "Ошибка ОСВ 60", commands: [ensure_issue{temp_key: new:1, issue_type: bug, …},
+ ensure_attachment{key: new:1, path: "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}]}`
+- **THEN** файл загружен в хаб; пакет создан с `ensure_attachment {key: new:1, file: , filename: "ОСВ сч. 60 за 2 кв.
+ 2026.xlsx"}` и хвостовым `refresh`
+
+#### Scenario: Файла нет
+- **WHEN** тот же вызов, файла по `path` нет
+- **THEN** ответ «файл не найден: », пакет не создан, в хаб ничего не загружено
+
+#### Scenario: Ожидание результата
+- **WHEN** `batch_wait {id, timeout_minutes: 10}` и пакет завершился через 6 минут
+- **THEN** ответ через 6 минут со статусами всех команд
+
+#### Scenario: Статус агента с довыдачей
+- **WHEN** агент на связи, команда 40 выдана дважды и ещё без отчёта
+- **THEN** ответ `agent_status` содержит «агент онлайн» и строку о команде 40 с числом выдач 2
+
+#### Scenario: Отмена пакета с командой у агента
+- **WHEN** `batch_cancel {id: 9}`, команда 30 пакета в `claimed`
+- **THEN** ответ содержит вид пакета и строку «команда #30 уже у агента и будет выполнена; остановить — …»
+
+#### Scenario: Статус агента после отката
+- **WHEN** опубликована v7, агент v6 под запускателем v1 прислал `update_state = rolled_back` для v7
+- **THEN** ответ `agent_status` содержит «публикация: v7 от …», «обновление: rolled_back v6 → v7», «запускатель: v1» и
+ «откат с v7 на v6: …»
+
+#### Scenario: Статус агента сразу после перезапуска хаба
+- **WHEN** хаб перезапущен, агент ещё не прислал `claim`
+- **THEN** ответ `agent_status` содержит «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска
+ хаба)» и не содержит «без запускателя» и «агент запущен без run.py»
+
+#### Scenario: Задача с семью комментариями
+- **WHEN** `issue {key: "AURORA-9001"}`, у задачи 7 комментариев, у самого нового текст 500 символов
+- **THEN** ответ содержит «комментарии (7):», 5 последних комментариев с номерами «#» (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2»
+
+#### Scenario: Комментарий целиком
+- **WHEN** у задачи `AURORA-9001` комментарий `#101` длиной 800 символов с переносами строк, вызван `comment {key: "AURORA-9001", comment_id: "101"}`
+- **THEN** ответ — «AURORA-9001 #101 <автор>, создан » и полный текст комментария без обрезки, переносы строк сохранены
+
+#### Scenario: Нет такого комментария
+- **WHEN** `comment {key: "AURORA-9001", comment_id: "999"}`, у задачи нет комментария 999
+- **THEN** ответ «у AURORA-9001 нет комментария #999»
+
+### Requirement: Привязка сессии к проекту
+`sdlc-hub mcp <модуль> --project <метка>` SHALL привязывать сервер MCP к проекту. Привязка SHALL проверяться при каждом вызове
+инструмента по `GET /projects`: метки нет — ответ «проект <метка> не заведён в хабе: project_create или исправьте
+--project в .mcp.json» (кроме `projects`, `project_create`).
+
+В привязанной к проекту P сессии:
+- `issues` — `GET /issues?project=P` с остальными фильтрами;
+- `issue`, `comment` — любая задача по ключу;
+- `notes_set` — только своя задача P (метка P; соседи — нет) или временный ключ `new:` (его задача создаётся пакетом
+ проекта), иначе ответ « не в проекте P»;
+- `batch_create`, `command_retry` — с `project = P` (проверки пакета проекта, см. `commands`; повтор команды другого
+ проекта или пакета без проекта — отказ хаба);
+- `batches` — `GET /batches?project=P`; `batch_status`, `batch_wait`, `batch_cancel` — по id, без ограничения;
+- `refresh` — `full` — общий полный прогон всех проектов, `keys` — как раньше.
+
+Без привязки инструменты `issues`, `issue`, `comment`, `notes_set`, `batch_create`, `command_retry`, `refresh` SHALL отвечать
+«сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)» и в хаб не ходить; `projects`,
+`project_create`, `agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`,
+`batch_cancel` работают.
+
+#### Scenario: Сессия без проекта
+- **WHEN** MCP запущен без `--project`, вызван `issues`
+- **THEN** ответ «сессия не привязана к проекту: …», запросов к `GET /issues` нет
+
+#### Scenario: Задачи своего проекта
+- **WHEN** MCP запущен с `--project STOCKS_DRRK`, вызван `issues`
+- **THEN** в таблице только свои задачи `STOCKS_DRRK` и их соседи
+
+#### Scenario: Создание задачи без метки
+- **WHEN** сессия `STOCKS_DRRK`, `batch_create {commands: [ensure_issue {temp_key: new:1, summary: "…"}]}` без `labels`
+- **THEN** пакет поставлен с `project = STOCKS_DRRK`, у `ensure_issue` `labels = ["STOCKS_DRRK"]`
+
+#### Scenario: Опечатка в проекте
+- **WHEN** MCP запущен с `--project STOKS`, такого проекта нет; вызван `issues`
+- **THEN** ответ «проект STOKS не заведён в хабе: …»
+
+### Requirement: Вложения в MCP
+`issue` SHALL после комментариев выводить «вложения (N):» и по строке на каждое вложение (новые сверху): «# <имя>
+(<размер>) — <автор>, », дальше через запятую: «в описании», если на вложение ссылается описание, «в #»
+для каждого ссылающегося комментария, «скачано», если `state = ready`, «больше 10 МБ — только в Jira» для таких файлов;
+без вложений — «вложений нет». Размер — в байтах, КБ или МБ с одним знаком после запятой («512 Б», «234,4 КБ», «2,3 МБ»);
+неизвестный — «размер неизвестен».
+
+`attachment` SHALL:
+1. найти вложение по `GET /issues/{key}`: по номеру, иначе по точному имени (несколько с одним именем — самое новое, в
+ ответ строка «у N вложений «<имя>», взято самое новое #»); не найдено — «у нет вложения «<значение>»;
+ список — issue », в хаб больше не ходить;
+2. `POST /attachments/{id}/fetch` с автором `claude`; 413 — «файл больше 10 МБ (<размер>) — скачайте в Jira:
+ задача , вложение «<имя>»»; 409 и прочие ошибки хаба — их текст;
+3. пока `state = fetching` — опрашивать `GET /attachments/{id}` раз в 2 с, не дольше `wait_seconds`; не дождался —
+ «скачивание ещё идёт (команда #, агент <онлайн/офлайн>); вызовите attachment ещё раз — вторая команда не
+ поставится»; `failed` — «не удалось скачать: »;
+4. при `ready` — `GET /attachments/{id}/content` и записать файл в `<каталог данных хаба>/downloads//<имя>`, где имя —
+ `filename` с заменой `/`, `\`, `:` и управляющих символов на `_` (пустое — `attachment-`); каталог с правами
+ `0700`; запись через временный файл и переименование; если там уже лежит файл того же размера — не перезаписывать;
+5. ответить «<абсолютный путь>» отдельной строкой и под ним «<имя> (<размер>), , задача ».
+
+#### Scenario: Скриншот из комментария
+- **WHEN** `comment {key: "AURORA-1650", comment_id: "101"}` показал «вложения: #31337 скрин 1.png (234,4 КБ)»; вызван
+ `attachment {key: "AURORA-1650", attachment: "#31337"}`, агент онлайн, файла у хаба нет
+- **THEN** поставлена `fetch_attachment`; через несколько секунд ответ — путь
+ `…/sdlc-hub/downloads/31337/скрин 1.png` и строка «скрин 1.png (234,4 КБ), image/png, задача AURORA-1650»; файл по пути
+ читается
+
+#### Scenario: Повторный вызов
+- **WHEN** тот же вызов второй раз, файл у хаба (`ready`) и в `downloads/31337/`
+- **THEN** ответ сразу, команд в очереди не прибавилось
+
+#### Scenario: Агент не успел
+- **WHEN** `attachment {…, wait_seconds: 30}`, агент занят долгим `refresh`
+- **THEN** через 30 с ответ «скачивание ещё идёт (команда #N, агент онлайн); вызовите attachment ещё раз — вторая
+ команда не поставится»
+
+#### Scenario: По имени
+- **WHEN** `attachment {key: "AURORA-1650", attachment: "ОСВ 60.xlsx"}`, вложение с таким именем одно
+- **THEN** скачивается оно; путь оканчивается на `/ОСВ 60.xlsx`
+
+#### Scenario: Вложения в карточке
+- **WHEN** `issue {key: "AURORA-1650"}`, у задачи вложения #31337 «скрин 1.png» (на него ссылается комментарий 101,
+ скачано) и #31338 «запись.mp4» на 25 МБ
+- **THEN** в ответе «вложения (2):», строка #31338 с «больше 10 МБ — только в Jira», строка #31337 с «в #101, скачано»; у
+ комментария 101 строка «файлы: #31337 скрин 1.png»
+
+### Requirement: MCP вики
+`sdlc-hub mcp wiki [--project <метка>]` SHALL запускать по stdio MCP-сервер модуля `wiki` с именем `sdlc-wiki`,
+ходящий в HTTP API хаба клиентским токеном; хаб недоступен — каждый инструмент отвечает «хаб не запущен: sdlc-hub serve».
+Привязка к проекту проверяется так же, как у сервера jira («Привязка сессии к проекту»). Инструменты:
+
+| Инструмент | Параметры | Ответ |
+|---|---|---|
+| `projects` | — | таблица: метка, название, корень вики, страниц, последняя синхронизация; текущий проект отмечен |
+| `project_create` | `label`, `name`, `wiki_root` (необязательно) | `POST /projects` с автором `claude`; проект и фрагмент `.mcp.json` с записью `sdlc-wiki` |
+| `root_set` | `root` | `PATCH /projects/<текущий> {wiki_root}`; новый корень и id пакета синхронизации |
+| `tree` | `code` (необязательно), `depth` (по умолчанию 2) | дерево от корня или от `code`: отступ, код, название, размер |
+| `page` | `code` или `title`, `offset` (по умолчанию 0) | шапка (код, путь в дереве, адрес в Eva, дата синхронизации, вложения с `id`) и Markdown с `offset` до 20 000 символов, в конце — «продолжение: offset=», если текст длиннее; `title` ищется точно, иначе — до 10 кандидатов с кодами |
+| `search` | `query`, `limit` (по умолчанию 10) | страницы: код, название, путь, фрагмент |
+| `attachment` | `id` | ставит скачивание, ждёт до 60 с; готово — путь к копии `downloads/wiki-/<имя>`; иначе состояние и подсказка повторить |
+| `sync` | `wait` (по умолчанию `false`) | ставит синхронизацию (или сообщает о текущей) и id пакета; с `wait` — ждёт до 300 с и отдаёт итог |
+| `status` | — | корень, последняя синхронизация, ошибка, пакет в работе, агент `wiki`: онлайн/офлайн, версия, публикация, обновление, запускатель (как `agent_status` сервера jira) |
+
+Инструменты, кроме `projects` и `project_create`, SHALL требовать привязки к проекту. В привязанной сессии без корня
+(кроме `root_set`, `status`) ответ SHALL быть «у проекта <метка> нет корня вики: root_set DOC-…»; с корнем, но без
+синхронизаций — «вики проекта ещё не синхронизирована: sync». Набор инструментов и их описания у сервера `sdlc-jira`
+(модуль jira) SHALL остаться прежним.
+
+#### Scenario: Поиск из сессии awp-ca
+- **WHEN** сервер запущен с `--project AWPCA`, копия синхронизирована, вызван `search {query: "статус согласован"}`
+- **THEN** ответ — страницы с кодами и фрагментами; запрос в хаб — `GET /wiki/search?project=AWPCA&q=…`
+
+#### Scenario: Длинная страница
+- **WHEN** `page {code: DOC-3}`, Markdown 45 000 символов
+- **THEN** первые 20 000 символов и «продолжение: offset=20000»; `page {code: DOC-3, offset: 40000}` — остаток без строки
+ продолжения
+
+#### Scenario: Корня нет
+- **WHEN** сессия `sfm_kedr` без корня, вызван `tree`
+- **THEN** ответ «у проекта sfm_kedr нет корня вики: root_set DOC-…», запроса `GET /wiki/tree` нет
+
+#### Scenario: Сервер jira не изменился
+- **WHEN** запущен `sdlc-hub mcp jira --project AWPCA`
+- **THEN** имя сервера `sdlc-jira`, список инструментов и описания совпадают с эталоном до change
diff --git a/openspec/changes/sdlc-rename/specs/migration/spec.md b/openspec/changes/sdlc-rename/specs/migration/spec.md
new file mode 100644
index 0000000..7eaf7a7
--- /dev/null
+++ b/openspec/changes/sdlc-rename/specs/migration/spec.md
@@ -0,0 +1,39 @@
+## MODIFIED Requirements
+
+### Requirement: Инициализация настроек
+`sdlc-hub init --config ` SHALL перенести в `setting`: `project_key`, типы задач,
+`epic_name_field_id`, `fields`, `epic_link`, `priority_map`, `labels`, `status_labels`, `kind_labels`, `comment_marker`,
+`pacing` (в новую форму по типам команд), `transitions.map`, `link_types`; сгенерировать два токена; завести проект
+`AWPCA` «АРМ КА» с `board_id` из `sprints.board_id` конфига (если проекта `AWPCA` ещё нет). Поля `auth`, `base_url`,
+`verify_ssl`, `ca_bundle` MUST NOT переноситься (они у агента).
+
+#### Scenario: Повторный init
+- **WHEN** `init` вызван на уже инициализированной базе
+- **THEN** настройки не перезаписываются без `--force`, токены сохраняются; проект `AWPCA` не дублируется
+
+#### Scenario: Первый init
+- **WHEN** `init --config` на пустой базе, в конфиге `sprints.board_id = 4216`
+- **THEN** есть проект `AWPCA` «АРМ КА» с `board_id = 4216`; в `setting` нет `base_jql` и `board_id`
+
+### Requirement: Импорт CSV-пакета
+`sdlc-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.csv` и заполнить
+`issue_note` по ключу Jira: статус по факту, вид работы, что сделано, что осталось, внутренние колонки. Строки
+`new_tasks.csv` без ключа SHALL получать временный ключ `new:<номер строки>`; строки `tasks.csv` и `epics.csv` без ключа
+SHALL пропускаться. `links.csv` SHALL импортироваться как сверочный набор в отчёт импорта (ожидаемые связи против зеркала
+после первого `refresh`), не как команды. Связь, которой в зеркале нет как `source→target` того же типа, но есть как
+`target→source` того же типа, SHALL попадать в `reversed_links`, а не в `missing_links`. Импорт идемпотентен: повтор
+обновляет заметки, не дублирует.
+
+#### Scenario: Импорт и первая синхронизация
+- **WHEN** выполнены `init`, `import`, запущены хаб и агент, `refresh full` завершён
+- **THEN** число задач зеркала ≥ числа строк `tasks.csv` + созданных из `new_tasks.csv`; отчёт импорта перечисляет ключи
+ из CSV, которых нет в зеркале, связи из `links.csv`, которых нет в `issue_link`, и связи, которые есть в обратную сторону
+
+#### Scenario: Связь в обратную сторону
+- **WHEN** в `links.csv` строка «AURORA-1445 → AURORA-1446, связанные», а в зеркале есть только «ASUPR_Связанные»
+ 1446→1445
+- **THEN** связь в `reversed_links`, в `missing_links` её нет
+
+#### Scenario: Эпик без ключа
+- **WHEN** в `epics.csv` строка без значения в колонке «Ключ»
+- **THEN** строка пропущена, заметка `new:*` для неё не создаётся
diff --git a/openspec/changes/sdlc-rename/specs/web-app/spec.md b/openspec/changes/sdlc-rename/specs/web-app/spec.md
new file mode 100644
index 0000000..65e6ca3
--- /dev/null
+++ b/openspec/changes/sdlc-rename/specs/web-app/spec.md
@@ -0,0 +1,169 @@
+## MODIFIED Requirements
+
+### Requirement: Вход по токену
+Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом
+`sdlc-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса.
+Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
+подсказкой `uv run sdlc-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
+Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен»
+SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на
+запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401,
+экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает».
+
+Ключи `localStorage` приложения с префиксом `sdlc-hub-` (`sdlc-hub-token`, `sdlc-hub-sidebar`, `sdlc-hub-legend`) SHALL
+при первом чтении, если ключа нет, брать значение одноимённого ключа с префиксом `jira-hub-`, записывать его под новым
+ключом и удалять прежний.
+
+#### Scenario: Первый вход
+- **WHEN** приложение открыто впервые, токена нет
+- **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён
+
+#### Scenario: Устаревший токен
+- **WHEN** сохранённый токен не подходит и запрос ответил 401
+- **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны
+
+#### Scenario: Токен от прежней доски
+- **WHEN** в `localStorage` нет `sdlc-hub-token`, но есть `jira-hub-token` (сохранён до переименования хаба)
+- **THEN** приложение входит с ним без запроса токена; значение перенесено в `sdlc-hub-token`, `jira-hub-token` удалён
+
+#### Scenario: Запоздавший 401 от прежнего токена
+- **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401
+- **THEN** приложение остаётся на странице, новый токен сохранён
+
+#### Scenario: Хаб отверг проверку токена
+- **WHEN** проверка `GET /settings` ответила 400 с `detail`
+- **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает»
+
+### Requirement: Оболочка и адреса
+Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
+задач), `/app/feed` (лента изменений, требование «Лента изменений») и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
+на страницу «не найдено» со ссылками на разделы. Боковая панель SHALL быть закреплена у левого края окна на всю его
+высоту и MUST NOT прокручиваться вместе со страницей; страница занимает остальную ширину. Сверху вниз панель SHALL
+содержать: название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий
+отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»); разделы «Доска связей», «Задачи» и «Лента» — вертикальным
+списком ссылок со значками (текущий отмечен `aria-current="page"`); внизу — блок данных Jira с действием «Обновить из
+Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с действием «Сменить токен».
+Свёрнутая панель SHALL быть узкой полосой с теми же действиями значками: «Развернуть панель», метка текущего проекта,
+разделы, «Обновить из Jira» с точкой состояния агента, тема, меню; названия — в подсказках и `aria-label`. Свёртка
+SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута). На экранах уже
+640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», названием и кнопкой «Обновить из
+Jira»; «Меню» открывает развёрнутую панель поверх страницы, панель закрывается выбором раздела, клавишей Escape или
+щелчком мимо неё. Горизонтальной прокрутки страницы на любой ширине нет.
+Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном.
+
+#### Scenario: Переход между разделами
+- **WHEN** на доске в боковой панели выбран раздел «Задачи»
+- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
+
+#### Scenario: Раздел «Лента»
+- **WHEN** на доске в боковой панели выбран раздел «Лента»
+- **THEN** адрес — `/app/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели
+
+#### Scenario: Сменить токен
+- **WHEN** в меню боковой панели выбрано «Сменить токен»
+- **THEN** токен очищен, показан экран входа
+
+#### Scenario: Панель на месте при прокрутке
+- **WHEN** доска прокручена до самого низа
+- **THEN** боковая панель видна целиком на прежнем месте: разделы и «Обновить из Jira» доступны без прокрутки вверх
+
+#### Scenario: Свёрнутая панель
+- **WHEN** нажата «Свернуть панель», страница перезагружена
+- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; «Развернуть панель»
+ возвращает полную панель
+
+#### Scenario: Узкий экран
+- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи»
+- **THEN** панель открылась поверх страницы, после выбора закрыта, показан список задач; горизонтальной прокрутки нет
+
+### Requirement: Доска связей
+Страница `/app/board` SHALL воспроизводить поведение доски связей хаба (`links.html` ревизии 3931a88) без изменений,
+кроме перечисленных в этом требовании и в требовании «Источник данных доски»: построение модели из `GET /board`, CSV
+фильтра Jira и JSON доски (`arm-ka-links/1`); дорожки по эпикам со свёрткой и перестановкой, служебные дорожки внизу;
+карточки с перетаскиванием, растяжкой, размером по содержимому и привязкой к сетке; стрелки связей с выравниванием;
+фильтры по спринтам и исполнителям; легенда со скрытием видов работ и типов связей; «соседи по связям», «только
+передачи», «скрыть без связей», «критический путь», «по датам» с колонками дат и их растяжкой; ящик «Проблемы»; таблица
+связей; экспорт и импорт JSON; «Сбросить раскладку». Раскладка, дорожки и фильтры SHALL храниться в
+`localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей.
+Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html`
+на одних и тех же данных и фильтрах.
+
+Шапка доски (закреплена при прокрутке) SHALL содержать заголовок «Доска связей», счётчики (задачи из всех, соседи по
+связям, связи из всех, скрытые без связей), пометку источника, если на доске снимок из файла (с действием вернуться к
+данным хаба), событие доски, кнопку «Проблемы» со счётчиком и меню с действиями «Перечитать», «Загрузить файл…»,
+«Экспорт JSON», «Таблица связей», «Привязка к сетке» и «Сбросить раскладку» (с подтверждением). Под шапкой — панель
+управления: мультиселекты «Спринты» и «Исполнители», переключатели группы «Показать» («Соседи по связям», «Только
+передачи», «Скрыть без связей») и группы «Режим» («Критический путь», «По датам»), каждый с `aria-pressed` и прежней
+подсказкой. Под панелью — легенда, которая сворачивается кнопкой «Легенда»; свёрнутая показывает число скрытых видов
+работ и типов связей; состояние свёртки хранится в `localStorage` под ключом `sdlc-hub-legend`. «Обновить из Jira»,
+тема и «Сменить токен» — в боковой панели приложения, на доске их нет.
+
+У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку»,
+открывающий `/app/issues/{key}` окном поверх доски; ключ карточки по-прежнему открывает задачу в Jira. Пока окно открыто
+и после его закрытия доска SHALL сохранять прокрутку, раскладку, фильтры и строку состояния. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку
+карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке
+захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
+Карточка задачи, статус Jira которой входит в `closed_statuses`, SHALL быть приглушена (полупрозрачна), под
+указателем и с фокусом клавиатуры внутри карточки — в полную яркость; стрелка связи, у которой хотя бы один конец — такая задача, SHALL быть приглушена так же.
+Приглушение — только вид: модель, видимая выборка, критический путь, проблемы, раскладка и экспорт JSON от него не
+зависят; переключателя нет; правило одно для данных хаба, CSV и JSON.
+При возврате на `/app/board` с другой страницы приложения в той же вкладке доска SHALL восстанавливать прежнюю
+прокрутку; после перезагрузки страницы доска открывается сверху.
+
+Дорожки SHALL разделяться линией; фон развёрнутых дорожек один, без чередования, чтобы подсветка колонок «по датам»
+(прошедшие даты, сегодня) была видна на всех дорожках; свёрнутая дорожка выделяется фоном. У карточки задачи, статус
+Jira которой не входит в `closed_statuses`, с плановой датой окончания раньше сегодняшней дата «до …» SHALL быть цветом
+ошибки, с подсказкой и текстом для экранного чтения «просрочено»; это только вид, модель и раскладка от него не зависят.
+
+#### Scenario: Раскладка переживает переход
+- **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными
+- **THEN** карточки стоят на тех же местах, дорожки в том же порядке и свёртке, фильтры те же
+
+#### Scenario: Совпадение со старой доской
+- **WHEN** на одной фикстуре `GET /board` и одном наборе фильтров построены модель, видимая выборка, критический путь
+ и проблемы старой страницей и приложением
+- **THEN** результаты совпадают
+
+#### Scenario: Открыть карточку
+- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку»
+- **THEN** адрес `/app/issues/AURORA-1620`, карточка задачи открыта окном, под ним доска
+
+#### Scenario: Возврат с карточки
+- **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем окно закрыто
+- **THEN** адрес `/app/board`, доска показана с той же прокруткой и раскладкой, без повторной загрузки `GET /board`
+
+#### Scenario: Возврат из другого раздела
+- **WHEN** доска прокручена вниз, выбран раздел «Задачи», затем раздел «Доска связей»
+- **THEN** доска показана с той же прокруткой
+
+#### Scenario: Закрытые на доске приглушены
+- **WHEN** на доске AURORA-1620 «В работе», AURORA-1621 «Готово», AURORA-1622 «Снято», связь AURORA-1620 → AURORA-1621
+- **THEN** карточки AURORA-1621 и AURORA-1622 и стрелка AURORA-1620 → AURORA-1621 приглушены, AURORA-1620 — нет;
+ под указателем AURORA-1621 — в полную яркость; критический путь и ящик «Проблемы» те же, что без приглушения
+
+#### Scenario: Свёрнутая легенда
+- **WHEN** в легенде скрыт вид работы «тестирование», легенда свёрнута, страница перезагружена
+- **THEN** легенда свёрнута, рядом с кнопкой «Легенда» — «скрыто: 1»; карточек вида «тестирование» на доске нет
+
+#### Scenario: Просрочка на доске
+- **WHEN** сегодня 22.10, у AURORA-1620 статус «В работе» и срок 20.10, у AURORA-1621 «Готово» и срок 20.10, включено
+ «По датам»
+- **THEN** дата AURORA-1620 — цветом ошибки с пометкой «просрочено», у AURORA-1621 — обычная; подсветка колонки 20.10
+ видна на всех дорожках, дорожки одного фона
+
+### Requirement: Сборка и разработка
+Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
+`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
+`src/sdlc_hub/web/app/`; каталог сборки не хранится в git. Шрифты (Mulish — текст интерфейса, IBM Plex Mono — ключи
+задач и исходники), иконки и прочие ресурсы страницы SHALL входить в сборку: приложение не обращается к внешним
+адресам. `make check` SHALL запускать `uv run pytest`, проверку типов и линтер фронта и тесты фронта; зелёным считается
+только весь набор. `npm run dev` SHALL поднимать сервер разработки с прокси маршрутов API на хаб из `SDLC_HUB_URL` (по
+умолчанию тестовый `http://127.0.0.1:8791`).
+
+#### Scenario: Сборка с нуля
+- **WHEN** в чистом checkout выполнен `make web`
+- **THEN** в `src/sdlc_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки
+
+#### Scenario: Без интернета
+- **WHEN** приложение открыто на машине без доступа в интернет
+- **THEN** страница не запрашивает ресурсов вне хаба, текст набран Mulish, ключи задач — IBM Plex Mono
diff --git a/openspec/changes/sdlc-rename/specs/wiki/spec.md b/openspec/changes/sdlc-rename/specs/wiki/spec.md
new file mode 100644
index 0000000..e3bcc27
--- /dev/null
+++ b/openspec/changes/sdlc-rename/specs/wiki/spec.md
@@ -0,0 +1,33 @@
+## MODIFIED Requirements
+
+### Requirement: Markdown-копия вики
+Копия проекта SHALL лежать в `/wiki/<метка>/` (каталоги 0700): у каждой страницы — каталог
+`NN_<название>` внутри каталога родителя (`NN` — двузначная позиция среди соседей по `orderno`, затем по коду; у корня
+номера нет; название без символов `<>:"/\|?*` и управляющих, обрезано до 80 символов; совпадение у соседей —
+суффикс `_2`, `_3`), в нём `index.md`: frontmatter `code`, `title`, `url`, `synced_at`, строка `# ` и текст. В
+корне копии — `_tree.json`, список `{code, title, depth, path}` в порядке обхода. Копия SHALL собираться рядом
+(`wiki/.<метка>.new/`) и подменять прежнюю переименованием; незавершённую подмену хаб SHALL доводить при старте.
+
+HTML страницы SHALL переводиться в Markdown (заголовки `#`); ссылка или картинка на файл Eva (по списку `attachments` агента: адрес на хосте Eva
+с путём `/files/…`, относительный адрес — тоже Eva; миниатюра `<адрес>.meta/thumbnail.jpg` означает файл `<адрес>`, а
+ссылки на `static/…` — значки интерфейса — из текста удаляются) SHALL заменяться на
+`[📎 <имя>](attachment:)`, где `id` — строка вложения; ссылка на документ Eva с кодом `DOC-<цифры>` (в Eva — относительный
+`project/Document/DOC-…`) — на `[<текст>](page:<код>)`; прочие ссылки остаются. Сырой HTML в копию не попадает.
+
+#### Scenario: Раскладка
+- **WHEN** корень DOC-1 «АРМ КА», дети DOC-3 «Статусы» (`orderno` 1) и DOC-2 «Роли/права» (`orderno` 2)
+- **THEN** `wiki/AWPCA/АРМ КА/index.md`, `…/01_Статусы/index.md`, `…/02_Роли_права/index.md`; `_tree.json` в порядке
+ DOC-1, DOC-3, DOC-2
+
+#### Scenario: Картинка на странице
+- **WHEN** в HTML DOC-3 ``
+- **THEN** в `index.md` — `[📎 scheme.png](attachment:)`, у вложения `` `state = none`, файл не скачан
+
+#### Scenario: Прикреплённый файл с миниатюрой
+- **WHEN** на странице миниатюра `…//model.xlsx.meta/thumbnail.jpg?version=1`, значок `static/attach-file.png` и
+ ссылка на `…//model.xlsx`
+- **THEN** одно вложение `model.xlsx` с адресом файла; в тексте одна ссылка `[📎 model.xlsx](attachment:)`, значка нет
+
+#### Scenario: Ссылка на другую страницу
+- **WHEN** в HTML `Модель данных`
+- **THEN** в `index.md` — `[Модель данных](page:DOC-019834)`
--
2.47.2
From 10d2eaa51394ed97ed0bad43d266daeba71cc903 Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:21:09 +0400
Subject: [PATCH 02/21] =?UTF-8?q?plan(sdlc-rename):=20=D0=BF=D0=BB=D0=B0?=
=?UTF-8?q?=D0=BD=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?=
=?UTF-8?q?=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE?=
=?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Opus 5.5
---
.../plans/2026-10-24-sdlc-rename.md | 215 ++++++++++++++++++
openspec/changes/sdlc-rename/design.md | 7 +-
.../changes/sdlc-rename/specs/hub-api/spec.md | 5 +-
3 files changed, 223 insertions(+), 4 deletions(-)
create mode 100644 docs/superpowers/plans/2026-10-24-sdlc-rename.md
diff --git a/docs/superpowers/plans/2026-10-24-sdlc-rename.md b/docs/superpowers/plans/2026-10-24-sdlc-rename.md
new file mode 100644
index 0000000..b92c211
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-24-sdlc-rename.md
@@ -0,0 +1,215 @@
+# Переименование jira-hub → sdlc-hub — план реализации
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** одно имя `sdlc-hub` в пакете, CLI, каталоге данных, MCP, вебе и документации хаба плюс скрипт, которым владелец переключает живую установку.
+
+**Architecture:** механическое переименование пакета `src/jira_hub` → `src/sdlc_hub` и всех строк; MCP-серверы `sdlc-<модуль>` с обязательным модулем; защита от «код новый — каталог данных старый» в `core/paths.py` + `core/cli.py`; разовый перенос ключей `localStorage`; одноразовый скрипт `scripts/cutover.py` (stdlib, вне пакета) с обёрткой `scripts/cutover.sh`.
+
+**Tech Stack:** Python 3.13 (uv, pytest), React 19 + TS + Vite (Vitest, Playwright), bash.
+
+**Spec:** `openspec/changes/sdlc-rename/` (proposal.md, design.md, specs/) — issue jira-hub#97.
+
+## Global Constraints
+
+- Имя: пакет `sdlc_hub`, дистрибутив и CLI `sdlc-hub`, без псевдонима `jira-hub` (design, решение 2).
+- Каталог данных `~/.local/share/sdlc-hub/`, переменная `SDLC_HUB_HOME`; прежнее имя `~/.local/share/jira-hub/` встречается в коде только в проверке прежнего каталога (Task 3) и в скрипте переключения (Task 5).
+- MCP-серверы `sdlc-jira`, `sdlc-wiki`; `sdlc-hub mcp <модуль>`, модуль обязателен.
+- Бренд веба — «SDLC Hub»; ключи `localStorage` `sdlc-hub-token`, `sdlc-hub-sidebar`, `sdlc-hub-legend`.
+- Не менять: `docs/superpowers/plans/` (кроме этого плана), `openspec/changes/archive/`, `openspec/specs/` (их меняет архив change), протокол агента, схему базы, HTTP-маршруты, токены, ключи доски `graph-*`.
+- Граница ядра (`tests/test_registry.py`) остаётся зелёной; новые имена в ядре — без названий модулей.
+- Зелёный — только весь набор: `uv run pytest -q` и `cd web && npm run lint && npm test`.
+- Язык комментариев, сообщений и коммитов — русский; коммиты заканчиваются строкой `Co-Authored-By: Claude Opus 5.5 `.
+- Живой хаб (`:8790`, `~/.local/share/jira-hub`), launchd, `~/.claude.json`, `~/projects/awp-ca`, `~/projects/stocks_drrk` в задачах НЕ трогать: тесты — только во временных каталогах.
+
+## Review Focus
+
+1. Старый `jira-hub` в `.mcp.json` проекта и новый код: `sdlc-hub mcp` без модуля должен падать с понятной подсказкой, а не поднимать сервер jira — Task 2, `test_mcp_module_required`.
+2. Новый код при живом каталоге под старым именем: команда не должна создать пустой `~/.local/share/sdlc-hub/` и работать с пустой базой — Task 3, `test_legacy_home_refused` проверяет, что каталог не создан.
+3. Браузер с токеном под `jira-hub-token`: вход без повторного ввода токена; закрытое хранилище (исключение в `localStorage`) не ломает чтение — Task 4, `legacyStorage.test.ts`.
+4. Сухой прогон скрипта не меняет ничего, в том числе в проектах-потребителях — Task 5, `test_dry_run_changes_nothing` сравнивает снимок дерева.
+5. Повторный запуск скрипта после частичного переключения (данные уже перенесены): остановка на проверках шага 1 с понятным текстом, без второго переноса — Task 5, `test_apply_refuses_when_target_exists`.
+
+---
+
+### Task 1: Пакет, CLI и каталог данных под новым именем
+
+Механическое переименование. Имена MCP-серверов (`FastMCP("jira-hub")`, `FastMCP("jira-hub-wiki")`, ключи в `mcp_json`, модуль MCP по умолчанию) в этой задаче НЕ меняются — это Task 2; строка команды `"jira-hub"` в `mcp_json` меняется на `"sdlc-hub"`.
+
+**Files:**
+- Move: `src/jira_hub/` → `src/sdlc_hub/` (`git mv`)
+- Modify: `pyproject.toml` (`name = "sdlc-hub"`, `[project.scripts] sdlc-hub = "sdlc_hub.cli:main"`, `packages = ["src/sdlc_hub"]`), `uv.lock` (`uv lock`), `.gitignore` (`src/sdlc_hub/web/app/`), `Makefile` (комментарий), `web/vite.config.ts` (`outDir: "../src/sdlc_hub/web/app"`), `web/scripts/test-hub.sh` (`SDLC_HUB_HOME`, `uv run sdlc-hub`, защита `"$HOME/.local/share/sdlc-hub"*`), `web/scripts/golden-from-links.ts` — НЕ менять (читает старый коммит по старому пути)
+- Modify: все `import`/`from jira_hub` в `src/` и `tests/`; `core/paths.py` (`SDLC_HUB_HOME`, по умолчанию `~/.local/share/sdlc-hub`); тексты `jira-hub` → `sdlc-hub` в `src/sdlc_hub/__init__.py` (docstring: «sdlc-hub — хаб SDLC: ядро (проекты, очередь команд, агенты, HTTP API, веб) и модули jira и wiki»), `core/cli.py` (`prog="sdlc-hub"`, `description="SDLC-хаб: ядро и модули"`, docstring), `core/api.py` (подсказка `make web` «в каталоге sdlc-hub», строка запуска `sdlc-hub: http://…`), `core/client.py`, `core/mcp.py` (`OFFLINE = "хаб не запущен: sdlc-hub serve"`, команда в `mcp_json`), модули `jira` и `wiki` (тексты подсказок и docstring с `jira-hub tokens`/`jira-hub serve`)
+- Modify tests: `JIRA_HUB_HOME` → `SDLC_HUB_HOME` (`test_agents.py`, `test_cli.py`, `test_mcp.py`, `test_wiki_mcp.py`, `conftest.py` при наличии), пути `src/jira_hub` → `src/sdlc_hub` (`test_layout.py`, `test_registry.py`, `test_web_app.py` — `("sdlc_hub", "web", "app")`, `test_wiki_projects.py`)
+- Test: `tests/test_layout.py`
+
+**Interfaces:**
+- Produces: пакет `sdlc_hub` (те же модули, что были в `jira_hub`), `sdlc_hub.core.paths.home() -> Path` читает `SDLC_HUB_HOME`, по умолчанию `Path.home() / ".local" / "share" / "sdlc-hub"`; CLI `sdlc-hub`.
+
+- [ ] **Step 1: Тест каталога по умолчанию** — в `tests/test_layout.py`:
+
+```python
+def test_default_home(monkeypatch, tmp_path):
+ from sdlc_hub.core import paths
+ monkeypatch.delenv("SDLC_HUB_HOME", raising=False)
+ monkeypatch.setenv("HOME", str(tmp_path))
+ assert paths.home() == tmp_path / ".local" / "share" / "sdlc-hub"
+
+
+def test_env_home(monkeypatch, tmp_path):
+ from sdlc_hub.core import paths
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "x"))
+ assert paths.home() == tmp_path / "x"
+```
+
+- [ ] **Step 2:** `uv run pytest tests/test_layout.py -q` — FAIL (`ModuleNotFoundError: sdlc_hub`).
+- [ ] **Step 3:** `git mv src/jira_hub src/sdlc_hub`; заменить `jira_hub` → `sdlc_hub` и `JIRA_HUB_HOME` → `SDLC_HUB_HOME` по `src/`, `tests/`, `web/vite.config.ts`, `.gitignore`, `Makefile`, `web/scripts/test-hub.sh`; правки `pyproject.toml`; `uv lock && uv sync`; тексты из списка Files.
+- [ ] **Step 4:** `uv run pytest -q` — весь набор PASS; `uv run sdlc-hub --help` печатает `usage: sdlc-hub`; `uv run jira-hub --help` — команды нет.
+- [ ] **Step 5:** проверка остатков: `git grep -n -i "jira_hub\|JIRA_HUB" -- src tests web pyproject.toml Makefile .gitignore ':!web/scripts/golden-from-links.ts'` — пусто; `git grep -n "jira-hub" -- src tests` — только имена MCP-серверов (`FastMCP`, ключи `mcp_json`, их тесты и эталон `tests/fixtures/baseline/mcp_wiki_tools.expected.json`).
+- [ ] **Step 6: Commit** — `git add -A && git commit -m "refactor(sdlc-rename): пакет sdlc_hub, CLI sdlc-hub, каталог данных sdlc-hub"`.
+
+### Task 2: MCP-серверы `sdlc-<модуль>`, модуль обязателен
+
+**Files:**
+- Modify: `src/sdlc_hub/modules/jira/mcp.py` (`FastMCP("sdlc-jira")`), `src/sdlc_hub/modules/wiki/mcp.py` (`FastMCP("sdlc-wiki")`, docstring и описание `project_create`: «с сервером sdlc-wiki»), `src/sdlc_hub/core/mcp.py` (`mcp_json`, `run`), `src/sdlc_hub/core/cli.py` (аргумент `module` в `mcp`)
+- Modify tests: `tests/test_cli.py` (`test_mcp_default_module` → `test_mcp_module_required`), `tests/test_mcp.py:500-520`, `tests/test_project_fields.py:152-157`, `tests/test_wiki_mcp.py:291,305`, эталон `tests/fixtures/baseline/mcp_wiki_tools.expected.json` (описание `project_create`)
+
+**Interfaces:**
+- Consumes: пакет `sdlc_hub` (Task 1).
+- Produces: `core.mcp.mcp_json(label: str, module: str) -> dict` — `{"mcpServers": {f"sdlc-{module}": {"command": "uv", "args": ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp", module, "--project", label]}}}`, `module` без значения по умолчанию; `core.mcp.run(module_name: str | None, project: str | None = None) -> int`.
+
+- [ ] **Step 1: Тесты** — в `tests/test_cli.py` заменить `test_mcp_default_module`:
+
+```python
+def test_mcp_module_required(monkeypatch, capsys):
+ from sdlc_hub.core import mcp as core_mcp
+ from sdlc_hub.modules.jira import mcp as jira_mcp
+ seen = []
+ monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append(core_mcp.PROJECT))
+ assert cli.main(["mcp", "--project", "AWPCA"]) == 2
+ err = capsys.readouterr().err
+ assert "укажите модуль: sdlc-hub mcp <модуль>" in err and "jira, wiki" in err
+ assert seen == []
+ assert cli.main(["mcp", "jira", "--project", "AWPCA"]) == 0
+ assert seen == ["AWPCA"]
+```
+
+ В `test_project_fields.py::test_mcp_json_per_module`: `list(core_mcp.mcp_json("AWPCA", "jira")["mcpServers"]) == ["sdlc-jira"]`, `args[-4:] == ["mcp", "jira", "--project", "AWPCA"]`, `args[3] == "sdlc-hub"`; для `"wiki"` — `["sdlc-wiki"]`, `args[-4:] == ["mcp", "wiki", "--project", "AWPCA"]`. В `test_mcp.py` фрагмент `project_create` сравнивается с `core_mcp.mcp_json("STOCKS_DRRK", "jira")`, ключ `"sdlc-jira"`. В `test_wiki_mcp.py`: `m.mcp_server().name == "sdlc-wiki"`, `"sdlc-wiki" in created`. Имя сервера jira: `jira_mcp.mcp.name == "sdlc-jira"` (добавить в `test_mcp.py`).
+- [ ] **Step 2:** `uv run pytest tests/test_cli.py tests/test_mcp.py tests/test_project_fields.py tests/test_wiki_mcp.py -q` — FAIL.
+- [ ] **Step 3:** реализация: `cli` — `p.add_argument("module", nargs="?", default=None, help="имя модуля (jira, wiki); без модуля или неизвестный — код 2")`; `mcp.run(None, …)` печатает в stderr `укажите модуль: sdlc-hub mcp <модуль> [--project <метка>]; модули: <имена через ", ">` и возвращает 2; ветка неизвестного модуля прежняя. Вызов `mcp_json` в `core/mcp.py:186` уже передаёт `module`. Эталон baseline — заменить `jira-hub-wiki` на `sdlc-wiki` в описании.
+- [ ] **Step 4:** `uv run pytest -q` — весь набор PASS; `git grep -n "jira-hub" -- src tests` — пусто.
+- [ ] **Step 5: Commit** — `feat(sdlc-rename): MCP-серверы sdlc-jira и sdlc-wiki, модуль в sdlc-hub mcp обязателен`.
+
+### Task 3: Отказ при каталоге данных под прежним именем
+
+**Files:**
+- Modify: `src/sdlc_hub/core/paths.py`, `src/sdlc_hub/core/cli.py` (`main`, сразу после `parse_args`)
+- Test: `tests/test_cli.py`
+
+**Interfaces:**
+- Consumes: `paths.home()` (Task 1).
+- Produces: `paths.legacy_home() -> Path` (`Path.home() / ".local" / "share" / "jira-hub"`), `paths.home_problem() -> str | None` (текст отказа: `SDLC_HUB_HOME` не задан, `home()` не существует, `legacy_home()` существует), `paths.home_warning() -> str | None` (`SDLC_HUB_HOME` не задан, существуют оба).
+
+- [ ] **Step 1: Тесты** в `tests/test_cli.py`:
+
+```python
+def _homes(monkeypatch, tmp_path, legacy=True, new=False):
+ monkeypatch.delenv("SDLC_HUB_HOME", raising=False)
+ monkeypatch.setenv("HOME", str(tmp_path))
+ share = tmp_path / ".local" / "share"
+ if legacy: (share / "jira-hub").mkdir(parents=True)
+ if new: (share / "sdlc-hub").mkdir(parents=True)
+ return share
+
+
+@pytest.mark.parametrize("argv", [["serve"], ["tokens"], ["mcp", "jira"], ["agent", "publish", "x.py"]])
+def test_legacy_home_refused(monkeypatch, tmp_path, capsys, argv):
+ share = _homes(monkeypatch, tmp_path)
+ assert cli.main(argv) == 2
+ err = capsys.readouterr().err
+ assert "~/.local/share/jira-hub" in err and "scripts/cutover.sh" in err
+ assert not (share / "sdlc-hub").exists()
+ assert sorted(p.name for p in (share / "jira-hub").iterdir()) == []
+
+
+def test_both_homes_warn(monkeypatch, tmp_path, capsys):
+ _homes(monkeypatch, tmp_path, new=True)
+ cli.main(["tokens"])
+ assert "прежний каталог ~/.local/share/jira-hub можно удалить" in capsys.readouterr().err
+
+
+def test_env_home_skips_legacy_check(monkeypatch, tmp_path, capsys):
+ _homes(monkeypatch, tmp_path)
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "x"))
+ assert cli.main(["tokens"]) == 0
+```
+
+- [ ] **Step 2:** `uv run pytest tests/test_cli.py -q -k "legacy or both_homes or env_home"` — FAIL.
+- [ ] **Step 3:** реализация. Текст отказа (точно):
+ `каталог данных хаба лежит под прежним именем ~/.local/share/jira-hub; перенесите его: bash scripts/cutover.sh (или mv ~/.local/share/jira-hub ~/.local/share/sdlc-hub при остановленном хабе)`.
+ Предупреждение: `есть и ~/.local/share/sdlc-hub, и ~/.local/share/jira-hub: работаю с новым, прежний каталог ~/.local/share/jira-hub можно удалить после проверки`. Проверка в `cli.main` до любой ветки команд; отказ — `print(..., file=sys.stderr)` и `return 2`.
+- [ ] **Step 4:** `uv run pytest -q` — весь набор PASS (прочие тесты задают `SDLC_HUB_HOME` и проверку не задевают).
+- [ ] **Step 5: Commit** — `feat(sdlc-rename): отказ CLI при каталоге данных под прежним именем`.
+
+### Task 4: Веб — бренд, ключи хранилища, подсказки
+
+**Files:**
+- Create: `web/src/api/legacyStorage.ts`, `web/src/api/legacyStorage.test.ts`
+- Modify: `web/index.html` (`SDLC Hub`), `web/src/shell/Sidebar.tsx` (бренд «SDLC Hub», 2 места), `web/src/shell/Sidebar.test.tsx:204`, `web/src/auth/token.ts` (`TOKEN_KEY = "sdlc-hub-token"`, чтение через `readMigrated`, комментарий), `web/src/shell/sidebarPref.ts` (`sdlc-hub-sidebar`), `web/src/modules/jira/board/legendPref.ts` (`sdlc-hub-legend`), экран входа (подсказка `uv run sdlc-hub tokens`) и `web/src/auth/LoginScreen.test.tsx:21`, `web/e2e/screens.spec.ts:92` (`sdlc-hub-sidebar`), `web/vite.config.ts` (`SDLC_HUB_URL`), `web/package.json` и `web/package-lock.json` (`"name": "sdlc-hub-web"`)
+
+**Interfaces:**
+- Produces: `readMigrated(key: string): string | null` в `web/src/api/legacyStorage.ts` — ключ обязан начинаться с `sdlc-hub-`; если `localStorage.getItem(key)` не `null` — вернуть его; иначе прочитать `"jira-hub-" + key.slice("sdlc-hub-".length)`, при наличии — `setItem(key, v)`, `removeItem(old)`, вернуть `v`; любое исключение хранилища — `null`.
+
+- [ ] **Step 1: Тесты** `web/src/api/legacyStorage.test.ts`:
+ - есть только `jira-hub-token = "abc"` → `readMigrated("sdlc-hub-token") === "abc"`, `localStorage.getItem("sdlc-hub-token") === "abc"`, `getItem("jira-hub-token") === null`;
+ - есть оба → возвращается новое, старое не трогается;
+ - нет ни одного → `null`, ничего не записано;
+ - `getItem` бросает (`vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { throw new Error("closed") })`) → `null`, без исключения.
+ - в `Sidebar.test.tsx` — `getByText("SDLC Hub")`; тест `sidebarPref`: при `jira-hub-sidebar = "collapsed"` `readSidebar() === true`.
+- [ ] **Step 2:** `cd web && npx vitest run src/api/legacyStorage.test.ts src/shell` — FAIL.
+- [ ] **Step 3:** реализация по Interfaces; `token.ts`, `sidebarPref.ts`, `legendPref.ts` читают через `readMigrated`, пишут и удаляют по новым ключам.
+- [ ] **Step 4:** `cd web && npm run lint && npm test` — PASS; `git grep -n "jira-hub\|Хаб Jira\|JIRA_HUB" -- web ':!web/scripts/golden-from-links.ts'` — только `legacyStorage.ts` и его тест.
+- [ ] **Step 5: Commit** — `feat(sdlc-rename): бренд SDLC Hub и ключи хранилища sdlc-hub-* с переносом`.
+
+### Task 5: Скрипт переключения живой установки
+
+**Files:**
+- Create: `scripts/cutover.py` (stdlib, Python ≥ 3.9, вне пакета), `scripts/cutover.sh` (`exec python3 "$(dirname "$0")/cutover.py" "$@"`, исполняемый), `tests/test_cutover.py` (загрузка модуля через `importlib.util.spec_from_file_location`)
+
+**Interfaces:**
+- Produces (в `scripts/cutover.py`):
+ - `@dataclass Config`: `bridge: Path` (по умолчанию `~/projects/jira-bridge`), `data_root: Path` (`~/.local/share`), `launch_agents: Path` (`~/Library/LaunchAgents`), `logs: Path` (`~/Library/Logs`), `remote: str` (`https://git.aepif.ru/aep/sdlc-hub.git`), `projects: list[Path]` (`~/projects/awp-ca`, `~/projects/stocks_drrk`), `port: int` (8790); `Config.from_env()` — переопределения `CUTOVER_BRIDGE`, `CUTOVER_DATA_ROOT`, `CUTOVER_LAUNCH_AGENTS`, `CUTOVER_LOGS`, `CUTOVER_REMOTE`, `CUTOVER_PROJECTS` (через `:`), `CUTOVER_PORT`.
+ - `preflight(cfg: Config) -> list[str]` — проблемы шага 1 спеки: старый checkout `bridge/jira-hub` есть, чистый (`git status --porcelain` пуст), ветка `main`, `HEAD == origin/main` (после `git fetch`); `git ls-remote ` успешен; `data_root/sdlc-hub` и `bridge/sdlc-hub` не существуют; `data_root/jira-hub` существует.
+ - `rewrite_mcp_json(path: Path, checkout: Path) -> bool` — ключи `jira-hub` → `sdlc-jira` с `args = ["run", "--project", str(checkout), "sdlc-hub", "mcp", "jira", *хвост --project]`, `jira-hub-wiki` → `sdlc-wiki` с `… "mcp", "wiki", …`; прочие серверы и порядок ключей сохраняются; `False`, если менять нечего.
+ - `rewrite_text(path: Path) -> list[str]` — замены по порядку: `mcp__jira-hub-wiki__`→`mcp__sdlc-wiki__`, `mcp__jira-hub__`→`mcp__sdlc-jira__`, `aep/jira-hub`→`aep/sdlc-hub`, `"jira-hub-wiki"`→`"sdlc-wiki"`, `"jira-hub"`→`"sdlc-jira"`, `` MCP `jira-hub` ``→`` MCP `sdlc-jira` ``, `MCP jira-hub`→`MCP sdlc-jira`; возвращает строки, где после замен осталось `jira-hub` (для сводки «проверить руками»).
+ - `backup(path: Path) -> Path` — копия `path` + `.pre-sdlc` до первой записи.
+ - `plist_text(checkout: Path, log: Path, uv: str) -> str` — как текущий `~/Library/LaunchAgents/ru.aepif.jira-hub.plist`, но `Label = ru.aepif.sdlc-hub`, `ProgramArguments = [uv, run, --project, checkout, sdlc-hub, serve]`, оба лога — `log`.
+ - `steps(cfg: Config) -> list[tuple[str, Callable[[], None]]]` — шаги 2–7 спеки с описаниями; `main(argv: list[str]) -> int` — без `--apply` печатает `preflight` и описания шагов, код 0 (или 1, если есть проблемы); с `--apply` — при проблемах код 1 без действий, иначе шаги по порядку с печатью, первая ошибка — код 1 и текст «остановлено на шаге N: …»; в конце сводка: `/mcp` reconnect, `codegraph init` в checkout и worktree, файлы `*.pre-sdlc`, строки из `rewrite_text` для ручной проверки.
+ - Файлы проектов для правки: `/.mcp.json` (`rewrite_mcp_json`), `/.claude/settings*.json`, `/CLAUDE.md`, `/.claude/skills/**/*` текстовые (`rewrite_text`).
+ - Шаг 5 ждёт TCP-подключения к `127.0.0.1:` до 30 с. Шаг 7: `claude mcp remove -s user jira-hub`, затем `claude mcp add -s user sdlc-jira -- uv run --project sdlc-hub mcp jira`.
+
+- [ ] **Step 1: Тесты** `tests/test_cutover.py` (всё во `tmp_path`, `HOME` подменён, `launchctl`/`claude`/`make`/`uv` не запускаются):
+ - `test_rewrite_mcp_json` — вход: копия формы `~/projects/awp-ca/.mcp.json` (серверы `jira-hub` и `jira-hub-wiki` с `--project AWPCA`) плюс посторонний сервер `other`; выход: ключи `["sdlc-jira", "sdlc-wiki", "other"]` в этом порядке, `args` как в Interfaces, `other` без изменений; повторный вызов → `False`.
+ - `test_rewrite_text_settings` — `{"enabledMcpjsonServers": ["jira-hub", "jira-hub-wiki"], "permissions": {"allow": ["mcp__jira-hub__issue", "mcp__jira-hub-wiki__page"]}}` → `["sdlc-jira", "sdlc-wiki"]`, `["mcp__sdlc-jira__issue", "mcp__sdlc-wiki__page"]`, остатков нет.
+ - `test_rewrite_text_reports_leftovers` — строка «Мост Jira (aep/jira-hub, aep/jira-agent) … через MCP `jira-hub` (user scope)» → «aep/sdlc-hub», «MCP `sdlc-jira`»; строка «каталог jira-hub» попадает в остатки.
+ - `test_plist_text` — `plistlib.loads(plist_text(...).encode())`: `Label == "ru.aepif.sdlc-hub"`, `ProgramArguments[-2:] == ["sdlc-hub", "serve"]`, `StandardOutPath == StandardErrorPath == str(log)`, `KeepAlive is True`.
+ - `test_dry_run_changes_nothing` — подготовить `bridge/jira-hub` (git-репозиторий с `origin` на локальный bare-репозиторий, `main` = `origin/main`), `data_root/jira-hub/hub.sqlite`, проект с `.mcp.json`; `CUTOVER_REMOTE` — путь к bare-репозиторию; снимок `{путь: (mtime_ns, size)}` всего `tmp_path` до и после `main([])` совпадает, код 0.
+ - `test_apply_refuses_without_remote` — `CUTOVER_REMOTE` на несуществующий путь → `main(["--apply"]) == 1`, в выводе «aep/sdlc-hub», снимок не изменился.
+ - `test_apply_refuses_when_target_exists` — `data_root/sdlc-hub` уже есть → `main(["--apply"]) == 1`, текст про существующий каталог, снимок не изменился.
+ - `test_dirty_checkout_refused` — незакоммиченный файл в `bridge/jira-hub` → проблема в `preflight`.
+- [ ] **Step 2:** `uv run pytest tests/test_cutover.py -q` — FAIL.
+- [ ] **Step 3:** реализация по Interfaces. Внешние команды — через `subprocess.run(..., check=True)` в шагах; в сухом прогоне и `preflight` — только чтение (`git status`, `git rev-parse`, `git fetch`, `git ls-remote`).
+- [ ] **Step 4:** `uv run pytest -q` — весь набор PASS; `bash scripts/cutover.sh` с `CUTOVER_*` на временный каталог печатает шаги.
+- [ ] **Step 5: Commit** — `feat(sdlc-rename): скрипт переключения живой установки scripts/cutover.sh`.
+
+### Task 6: Документация
+
+**Files:**
+- Modify: `README.md`, `CLAUDE.md`, `ARCHITECTURE.md`
+
+- [ ] **Step 1:** README: заголовок `# sdlc-hub`, все команды `uv run sdlc-hub …`, каталог `~/.local/share/sdlc-hub/` и `SDLC_HUB_HOME`, MCP — `sdlc-hub mcp jira|wiki`, серверы `sdlc-jira`/`sdlc-wiki`, регистрация user scope `claude mcp add -s user sdlc-jira -- uv run --project /Users/andrey.epifancev/projects/jira-bridge/sdlc-hub sdlc-hub mcp jira`, пути `~/projects/jira-bridge/sdlc-hub`, launchd `ru.aepif.sdlc-hub` с текстом plist из `plist_text` и логом `~/Library/Logs/sdlc-hub.log`, `kickstart … ru.aepif.sdlc-hub`. Новый раздел «Переключение с jira-hub» — шаги из `design.md` («Переключение (для владельца)») и откат: остановить `ru.aepif.sdlc-hub` (`launchctl bootout gui/$(id -u)/ru.aepif.sdlc-hub`), `mv` каталогов данных и checkout обратно, `git worktree repair`, вернуть `origin`, `git checkout` коммита до переименования, plist `ru.aepif.jira-hub` (прежний текст — в этом разделе), вернуть файлы `*.pre-sdlc` в проектах, `claude mcp` — обратно.
+- [ ] **Step 2:** CLAUDE.md: заголовок `# sdlc-hub`, «Что это» — SDLC-хаб с модулями jira и wiki, пути `src/sdlc_hub/…`, `~/.local/share/sdlc-hub/` (`SDLC_HUB_HOME`), команды `uv run sdlc-hub …`, MCP `sdlc-hub mcp <модуль>` (обязателен; серверы `sdlc-jira`, `sdlc-wiki`). ARCHITECTURE.md — те же имена.
+- [ ] **Step 3:** проверка: `git grep -n -i "jira-hub\|jira_hub\|JIRA_HUB\|Хаб Jira" -- . ':!docs/superpowers/plans' ':!openspec' ':!web/scripts/golden-from-links.ts'` — только: `src/sdlc_hub/core/paths.py` (прежний каталог), `tests/test_cli.py`, `scripts/cutover.py`, `tests/test_cutover.py`, `web/src/api/legacyStorage*.ts`, раздел README «Переключение с jira-hub». `uv run pytest -q` и `cd web && npm run lint && npm test` — PASS.
+- [ ] **Step 4: Commit** — `docs(sdlc-rename): README, CLAUDE.md, ARCHITECTURE.md под sdlc-hub`.
+
+Вне ветки хаба (делает контроллер сессии после слияния PR, bounded): мелкие PR в jira-agent и eva-agent — README, CLAUDE.md, ARCHITECTURE.md, `src/agent.example.json`, у eva-agent `scripts/e2e-hub.sh` (`SDLC_HUB_HOME`, `sdlc-hub`, путь хаба); `src/agent.py` и `src/run.py` не трогать (design, решение 10).
diff --git a/openspec/changes/sdlc-rename/design.md b/openspec/changes/sdlc-rename/design.md
index 195dea5..0a76119 100644
--- a/openspec/changes/sdlc-rename/design.md
+++ b/openspec/changes/sdlc-rename/design.md
@@ -41,7 +41,8 @@
4. **Каталог данных не переносится кодом.** Хаб, MCP-серверы открытых сессий Claude и CLI открывают одну базу; перенос
«на лету» при старте любого из них рискует WAL и гонкой. Код только отказывает с подсказкой (требование «Каталог
данных прежнего имени»), перенос — шаг скрипта при остановленном хабе.
-5. **Скрипт переключения в репозитории** (`scripts/cutover.sh`), сухой прогон по умолчанию. Шагов много, они касаются
+5. **Скрипт переключения в репозитории** (`scripts/cutover.sh` — обёртка над `scripts/cutover.py` на stdlib, вне пакета
+ хаба), сухой прогон по умолчанию. Шагов много, они касаются
launchd, двух чужих репозиториев и user scope Claude; руками их легко сделать не в том порядке (I-2 wiki-module).
Скрипт одноразовый, после переключения остаётся в истории как образец; тесты — на сухой прогон и проверки шага 1
(во временном `HOME`), без launchd.
@@ -65,7 +66,9 @@
1. Слить PR этого change; переименовать репозиторий в Gitea `aep/jira-hub` → `aep/sdlc-hub`.
2. Закрыть или не трогать сессии Claude с MCP хаба (после переключения — `/mcp` reconnect).
-3. `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh` — сухой прогон, проверить вывод.
+3. `git -C ~/projects/jira-bridge/jira-hub pull --ff-only` и сразу `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh`
+ — сухой прогон, проверить вывод. Между pull и переключением работающий хаб живёт на загруженном коде, а новые сессии
+ Claude не поднимут MCP — окно в несколько минут.
4. `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh --apply`.
5. Просмотреть правки в awp-ca и stocks_drrk (`.mcp.json`, `.claude/`, `CLAUDE.md` awp-ca; копии `*.pre-sdlc` рядом) и
удалить копии. Это не git-репозитории — коммитить нечего.
diff --git a/openspec/changes/sdlc-rename/specs/hub-api/spec.md b/openspec/changes/sdlc-rename/specs/hub-api/spec.md
index b998f2d..3106f28 100644
--- a/openspec/changes/sdlc-rename/specs/hub-api/spec.md
+++ b/openspec/changes/sdlc-rename/specs/hub-api/spec.md
@@ -209,12 +209,13 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report
### Requirement: Переключение живого хаба
`scripts/cutover.sh` SHALL переводить живую установку Mac mini со старого имени на новое. Без `--apply` скрипт SHALL
только печатать шаги и найденные проблемы, ничего не меняя. С `--apply` — по порядку, с остановкой на первой ошибке:
-1. проверки: основной checkout `jira-bridge/jira-hub` чистый и на `main`; в Gitea доступен `aep/sdlc-hub` (владелец
+1. проверки: основной checkout `jira-bridge/jira-hub` чистый, на `main` и совпадает с `origin/main` (владелец сделал
+ `git pull --ff-only` прямо перед запуском: скрипт приходит этим pull); в Gitea доступен `aep/sdlc-hub` (владелец
переименовал репозиторий); `~/.local/share/sdlc-hub/` и `jira-bridge/sdlc-hub` не существуют;
2. остановить launchd `ru.aepif.jira-hub` и удалить его plist;
3. перенести каталог данных `~/.local/share/jira-hub` → `~/.local/share/sdlc-hub`;
4. перенести checkout `jira-bridge/jira-hub` → `jira-bridge/sdlc-hub`, `git worktree repair`, `origin` →
- `aep/sdlc-hub`, `git pull --ff-only`, пересоздать `.venv` (`uv sync`), `make web`;
+ `aep/sdlc-hub`, пересоздать `.venv` (`uv sync`), `make web`;
5. поставить и запустить launchd `ru.aepif.sdlc-hub` (`uv run --project sdlc-hub serve`, лог
`~/Library/Logs/sdlc-hub.log`) и дождаться ответа хаба на порту из настроек;
6. в проектах из списка (по умолчанию `~/projects/awp-ca`, `~/projects/stocks_drrk`): в `.mcp.json` заменить записи
--
2.47.2
From 13ba7f4d606cbfb7ef33c65d7f8941604f0b83b1 Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:23:19 +0400
Subject: [PATCH 03/21] =?UTF-8?q?spec(sdlc-rename):=20R-1=20=D0=B2=20desig?=
=?UTF-8?q?n.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 5.5
---
openspec/changes/sdlc-rename/design.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/openspec/changes/sdlc-rename/design.md b/openspec/changes/sdlc-rename/design.md
index 0a76119..121a2e4 100644
--- a/openspec/changes/sdlc-rename/design.md
+++ b/openspec/changes/sdlc-rename/design.md
@@ -104,3 +104,4 @@
| 9 | 24.10.2026 | История (планы, архив) не меняется | это запись прошлого | — |
| 10 | 24.10.2026 | Агенты без релиза, документация — мелкими PR | протокол не меняется | строки документации в `agent.py` со старым именем до следующего релиза |
| 11 | 24.10.2026 | Сначала переименование, потом #98 | #98 перебазируется на новые пути | перебазирование веток |
+| 12 | 24.10.2026 | R-1: трейлер коммитов `Co-Authored-By: Claude Sonnet 5.5` вместо `Opus 5.5` из плана | указание системы сессии новее плана | правка трейлера |
--
2.47.2
From a88bb1e6e82619ba7b8b3aeb92a864025e48b399 Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:27:17 +0400
Subject: [PATCH 04/21] =?UTF-8?q?refactor(sdlc-rename):=20=D0=BF=D0=B0?=
=?UTF-8?q?=D0=BA=D0=B5=D1=82=20sdlc=5Fhub,=20CLI=20sdlc-hub,=20=D0=BA?=
=?UTF-8?q?=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=20=D0=B4=D0=B0=D0=BD=D0=BD?=
=?UTF-8?q?=D1=8B=D1=85=20sdlc-hub?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 5.5
---
.gitignore | 2 +-
Makefile | 2 +-
pyproject.toml | 6 +--
src/jira_hub/__init__.py | 2 -
src/sdlc_hub/__init__.py | 2 +
src/{jira_hub => sdlc_hub}/cli.py | 2 +-
src/{jira_hub => sdlc_hub}/core/__init__.py | 0
src/{jira_hub => sdlc_hub}/core/api.py | 4 +-
src/{jira_hub => sdlc_hub}/core/backup.py | 0
src/{jira_hub => sdlc_hub}/core/cli.py | 4 +-
src/{jira_hub => sdlc_hub}/core/client.py | 2 +-
src/{jira_hub => sdlc_hub}/core/db.py | 0
src/{jira_hub => sdlc_hub}/core/errors.py | 0
src/{jira_hub => sdlc_hub}/core/files.py | 0
src/{jira_hub => sdlc_hub}/core/http.py | 0
src/{jira_hub => sdlc_hub}/core/mcp.py | 4 +-
src/{jira_hub => sdlc_hub}/core/modules.py | 0
src/{jira_hub => sdlc_hub}/core/paths.py | 2 +-
src/{jira_hub => sdlc_hub}/core/projects.py | 0
src/{jira_hub => sdlc_hub}/core/queue.py | 0
src/{jira_hub => sdlc_hub}/core/registry.py | 0
src/{jira_hub => sdlc_hub}/core/relay.py | 0
src/{jira_hub => sdlc_hub}/core/release.py | 0
src/{jira_hub => sdlc_hub}/core/settings.py | 0
.../modules/__init__.py | 0
.../modules/jira/__init__.py | 0
.../modules/jira/api.py | 0
.../modules/jira/attachments.py | 0
.../modules/jira/cli.py | 2 +-
.../modules/jira/commands.py | 0
.../modules/jira/fetch.py | 0
.../modules/jira/history.py | 0
.../modules/jira/hooks.py | 0
.../modules/jira/importer.py | 0
.../modules/jira/mcp.py | 0
.../modules/jira/mirror.py | 0
.../modules/jira/project_fields.py | 0
.../modules/jira/projects.py | 0
.../modules/jira/refresh.py | 0
.../modules/jira/schema.py | 0
.../modules/jira/settings.py | 0
.../modules/wiki/__init__.py | 0
.../modules/wiki/api.py | 0
.../modules/wiki/attachments.py | 0
.../modules/wiki/commands.py | 0
.../modules/wiki/convert.py | 0
.../modules/wiki/mcp.py | 0
.../modules/wiki/paths.py | 0
.../modules/wiki/projects.py | 0
.../modules/wiki/roots.py | 0
.../modules/wiki/schema.py | 0
.../modules/wiki/search.py | 0
.../modules/wiki/settings.py | 0
.../modules/wiki/stage.py | 0
.../modules/wiki/sync.py | 0
.../modules/wiki/tick.py | 0
tests/conftest.py | 12 ++---
tests/test_agents.py | 12 ++---
tests/test_api.py | 12 ++---
tests/test_attachments.py | 4 +-
tests/test_baseline.py | 6 +--
tests/test_cli.py | 16 +++---
tests/test_commands.py | 4 +-
tests/test_db.py | 6 +--
tests/test_e2e_hub.py | 4 +-
tests/test_files.py | 2 +-
tests/test_history.py | 8 +--
tests/test_importer.py | 4 +-
tests/test_layout.py | 23 +++++++--
tests/test_mcp.py | 16 +++---
tests/test_mirror.py | 4 +-
tests/test_project_fields.py | 16 +++---
tests/test_projects.py | 6 +--
tests/test_queue.py | 16 +++---
tests/test_queue_core.py | 8 +--
tests/test_registry.py | 14 +++---
tests/test_relay.py | 4 +-
tests/test_release.py | 2 +-
tests/test_settings.py | 6 +--
tests/test_web_app.py | 4 +-
tests/test_wiki_api.py | 4 +-
tests/test_wiki_commands.py | 8 +--
tests/test_wiki_convert.py | 2 +-
tests/test_wiki_mcp.py | 10 ++--
tests/test_wiki_projects.py | 6 +--
tests/test_wiki_sync.py | 8 +--
uv.lock | 50 +++++++++----------
web/playwright.config.ts | 2 +-
web/scripts/seed-hub.py | 6 +--
web/scripts/test-hub.sh | 20 ++++----
web/vite.config.ts | 4 +-
91 files changed, 188 insertions(+), 175 deletions(-)
delete mode 100644 src/jira_hub/__init__.py
create mode 100644 src/sdlc_hub/__init__.py
rename src/{jira_hub => sdlc_hub}/cli.py (87%)
rename src/{jira_hub => sdlc_hub}/core/__init__.py (100%)
rename src/{jira_hub => sdlc_hub}/core/api.py (99%)
rename src/{jira_hub => sdlc_hub}/core/backup.py (100%)
rename src/{jira_hub => sdlc_hub}/core/cli.py (96%)
rename src/{jira_hub => sdlc_hub}/core/client.py (99%)
rename src/{jira_hub => sdlc_hub}/core/db.py (100%)
rename src/{jira_hub => sdlc_hub}/core/errors.py (100%)
rename src/{jira_hub => sdlc_hub}/core/files.py (100%)
rename src/{jira_hub => sdlc_hub}/core/http.py (100%)
rename src/{jira_hub => sdlc_hub}/core/mcp.py (98%)
rename src/{jira_hub => sdlc_hub}/core/modules.py (100%)
rename src/{jira_hub => sdlc_hub}/core/paths.py (90%)
rename src/{jira_hub => sdlc_hub}/core/projects.py (100%)
rename src/{jira_hub => sdlc_hub}/core/queue.py (100%)
rename src/{jira_hub => sdlc_hub}/core/registry.py (100%)
rename src/{jira_hub => sdlc_hub}/core/relay.py (100%)
rename src/{jira_hub => sdlc_hub}/core/release.py (100%)
rename src/{jira_hub => sdlc_hub}/core/settings.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/__init__.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/__init__.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/api.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/attachments.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/cli.py (91%)
rename src/{jira_hub => sdlc_hub}/modules/jira/commands.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/fetch.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/history.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/hooks.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/importer.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/mcp.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/mirror.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/project_fields.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/projects.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/refresh.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/schema.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/jira/settings.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/__init__.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/api.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/attachments.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/commands.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/convert.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/mcp.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/paths.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/projects.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/roots.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/schema.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/search.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/settings.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/stage.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/sync.py (100%)
rename src/{jira_hub => sdlc_hub}/modules/wiki/tick.py (100%)
diff --git a/.gitignore b/.gitignore
index 4a0f716..75e47d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,7 @@ __pycache__/
*.sqlite*
.pytest_cache/
.wt/
-src/jira_hub/web/app/
+src/sdlc_hub/web/app/
web/test-results/
web/playwright-report/
web/.e2e-hub/
diff --git a/Makefile b/Makefile
index d4f6bd6..eb4b16b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
.PHONY: web check
-# сборка веб-приложения в src/jira_hub/web/app/ (каталог не в git; хаб читает его на каждый запрос)
+# сборка веб-приложения в src/sdlc_hub/web/app/ (каталог не в git; хаб читает его на каждый запрос)
web:
cd web && npm ci && npm run build
diff --git a/pyproject.toml b/pyproject.toml
index 7ef8e0f..75bd7b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
[project]
-name = "jira-hub"
+name = "sdlc-hub"
version = "0.1.0"
description = "Мост Jira для АРМ КА: зеркало задач, очередь команд, HTTP API, MCP"
requires-python = ">=3.13"
@@ -10,7 +10,7 @@ dependencies = [
]
[project.scripts]
-jira-hub = "jira_hub.cli:main"
+sdlc-hub = "sdlc_hub.cli:main"
[dependency-groups]
dev = ["pytest>=8"]
@@ -20,7 +20,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
-packages = ["src/jira_hub"]
+packages = ["src/sdlc_hub"]
[tool.pytest.ini_options]
testpaths = ["tests"]
diff --git a/src/jira_hub/__init__.py b/src/jira_hub/__init__.py
deleted file mode 100644
index 908045b..0000000
--- a/src/jira_hub/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""jira-hub — мост Jira для АРМ КА: зеркало задач, очередь команд, HTTP API, MCP."""
-__version__ = "0.1.0"
diff --git a/src/sdlc_hub/__init__.py b/src/sdlc_hub/__init__.py
new file mode 100644
index 0000000..a0796a0
--- /dev/null
+++ b/src/sdlc_hub/__init__.py
@@ -0,0 +1,2 @@
+"""sdlc-hub — хаб SDLC: ядро (проекты, очередь команд, агенты, HTTP API, веб) и модули jira и wiki."""
+__version__ = "0.1.0"
diff --git a/src/jira_hub/cli.py b/src/sdlc_hub/cli.py
similarity index 87%
rename from src/jira_hub/cli.py
rename to src/sdlc_hub/cli.py
index 271dd5f..a1001f4 100644
--- a/src/jira_hub/cli.py
+++ b/src/sdlc_hub/cli.py
@@ -1 +1 @@
-from .core.cli import main # noqa: F401 точка входа jira-hub
+from .core.cli import main # noqa: F401 точка входа sdlc-hub
diff --git a/src/jira_hub/core/__init__.py b/src/sdlc_hub/core/__init__.py
similarity index 100%
rename from src/jira_hub/core/__init__.py
rename to src/sdlc_hub/core/__init__.py
diff --git a/src/jira_hub/core/api.py b/src/sdlc_hub/core/api.py
similarity index 99%
rename from src/jira_hub/core/api.py
rename to src/sdlc_hub/core/api.py
index f20f74e..5374b04 100644
--- a/src/jira_hub/core/api.py
+++ b/src/sdlc_hub/core/api.py
@@ -28,7 +28,7 @@ ASSET_TYPES = {".js": "text/javascript; charset=utf-8", ".css": "text/css; chars
".png": "image/png", ".ico": "image/x-icon", ".woff2": "font/woff2", ".map": "application/json"}
ASSET_CACHE = {"Cache-Control": "public, max-age=31536000, immutable"} # имена файлов сборки содержат хэш (R-3)
NOT_BUILT = ("Хаб Jira
Веб-приложение не собрано: выполните "
- "make web в каталоге jira-hub и перезапустите хаб.
").encode("utf-8")
+ "make web в каталоге sdlc-hub и перезапустите хаб.
").encode("utf-8")
SOCKET_TIMEOUT = 60 # чтение запроса; ожидание долгого claim идёт уже после чтения тела и сокет не держит
SWEEP_EVERY = 24 * 3600 # уборка files/ не чаще раза в сутки (hub-api «Хранилище файлов вложений»)
@@ -610,7 +610,7 @@ def start(hub: Hub, host="0.0.0.0", port=8790) -> tuple[ThreadingHTTPServer, thr
def serve(hub: Hub, host="0.0.0.0", port=8790) -> None:
srv, stop = start(hub, host, port)
- print(f"jira-hub: http://{host}:{srv.server_port}, база {hub.db_path}", flush=True)
+ print(f"sdlc-hub: http://{host}:{srv.server_port}, база {hub.db_path}", flush=True)
try:
srv.serve_forever()
except KeyboardInterrupt:
diff --git a/src/jira_hub/core/backup.py b/src/sdlc_hub/core/backup.py
similarity index 100%
rename from src/jira_hub/core/backup.py
rename to src/sdlc_hub/core/backup.py
diff --git a/src/jira_hub/core/cli.py b/src/sdlc_hub/core/cli.py
similarity index 96%
rename from src/jira_hub/core/cli.py
rename to src/sdlc_hub/core/cli.py
index 15469b9..49a3447 100644
--- a/src/jira_hub/core/cli.py
+++ b/src/sdlc_hub/core/cli.py
@@ -1,4 +1,4 @@
-"""Команды jira-hub: init (настройки из config.example.json), serve (HTTP), mcp [модуль] (stdio), tokens,
+"""Команды sdlc-hub: init (настройки из config.example.json), serve (HTTP), mcp [модуль] (stdio), tokens,
agent publish (версия агента для самообновления) и подкоманды модулей (jira: import)."""
import argparse
import json
@@ -9,7 +9,7 @@ from .paths import secure_dir
def main(argv=None) -> int:
- ap = argparse.ArgumentParser(prog="jira-hub", description="Мост Jira для АРМ КА: хаб")
+ ap = argparse.ArgumentParser(prog="sdlc-hub", description="SDLC-хаб: ядро и модули")
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("init", help="перенести настройки проекта из config.example.json, создать токены")
p.add_argument("--config", required=True)
diff --git a/src/jira_hub/core/client.py b/src/sdlc_hub/core/client.py
similarity index 99%
rename from src/jira_hub/core/client.py
rename to src/sdlc_hub/core/client.py
index 0348f7f..873f88f 100644
--- a/src/jira_hub/core/client.py
+++ b/src/sdlc_hub/core/client.py
@@ -14,7 +14,7 @@ class HubError(Exception):
class HubUnavailable(Exception):
- """Хаб не отвечает — обычно не запущен (jira-hub serve)."""
+ """Хаб не отвечает — обычно не запущен (sdlc-hub serve)."""
class HubClient:
diff --git a/src/jira_hub/core/db.py b/src/sdlc_hub/core/db.py
similarity index 100%
rename from src/jira_hub/core/db.py
rename to src/sdlc_hub/core/db.py
diff --git a/src/jira_hub/core/errors.py b/src/sdlc_hub/core/errors.py
similarity index 100%
rename from src/jira_hub/core/errors.py
rename to src/sdlc_hub/core/errors.py
diff --git a/src/jira_hub/core/files.py b/src/sdlc_hub/core/files.py
similarity index 100%
rename from src/jira_hub/core/files.py
rename to src/sdlc_hub/core/files.py
diff --git a/src/jira_hub/core/http.py b/src/sdlc_hub/core/http.py
similarity index 100%
rename from src/jira_hub/core/http.py
rename to src/sdlc_hub/core/http.py
diff --git a/src/jira_hub/core/mcp.py b/src/sdlc_hub/core/mcp.py
similarity index 98%
rename from src/jira_hub/core/mcp.py
rename to src/sdlc_hub/core/mcp.py
index 746f36f..287986e 100644
--- a/src/jira_hub/core/mcp.py
+++ b/src/sdlc_hub/core/mcp.py
@@ -14,7 +14,7 @@ from pathlib import Path
from . import paths, registry
from .client import HubClient, HubError, HubUnavailable
-OFFLINE = "хаб не запущен: jira-hub serve"
+OFFLINE = "хаб не запущен: sdlc-hub serve"
AUTHOR = "claude"
NOT_BOUND = "сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)"
HUB_ROOT = Path(__file__).resolve().parents[3] # корень репозитория хаба, из которого загружен пакет (решение 19)
@@ -28,7 +28,7 @@ def unknown_project(label) -> str:
def mcp_json(label: str, module: str = registry.DEFAULT_AGENT) -> dict:
"""Фрагмент .mcp.json для репозитория проекта: у модуля по умолчанию прежний (сервер jira-hub), у прочих модулей —
сервер jira-hub-<модуль>."""
- run = ["run", "--project", str(HUB_ROOT), "jira-hub", "mcp"]
+ run = ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp"]
if module == registry.DEFAULT_AGENT:
return {"mcpServers": {"jira-hub": {"command": "uv", "args": [*run, "--project", label]}}}
return {"mcpServers": {f"jira-hub-{module}": {"command": "uv", "args": [*run, module, "--project", label]}}}
diff --git a/src/jira_hub/core/modules.py b/src/sdlc_hub/core/modules.py
similarity index 100%
rename from src/jira_hub/core/modules.py
rename to src/sdlc_hub/core/modules.py
diff --git a/src/jira_hub/core/paths.py b/src/sdlc_hub/core/paths.py
similarity index 90%
rename from src/jira_hub/core/paths.py
rename to src/sdlc_hub/core/paths.py
index ac4250b..10ea4d1 100644
--- a/src/jira_hub/core/paths.py
+++ b/src/sdlc_hub/core/paths.py
@@ -7,7 +7,7 @@ log = logging.getLogger(__name__)
def home() -> Path:
- return Path(os.environ.get("JIRA_HUB_HOME", str(Path.home() / ".local" / "share" / "jira-hub")))
+ return Path(os.environ.get("SDLC_HUB_HOME", str(Path.home() / ".local" / "share" / "sdlc-hub")))
def db_path() -> Path:
diff --git a/src/jira_hub/core/projects.py b/src/sdlc_hub/core/projects.py
similarity index 100%
rename from src/jira_hub/core/projects.py
rename to src/sdlc_hub/core/projects.py
diff --git a/src/jira_hub/core/queue.py b/src/sdlc_hub/core/queue.py
similarity index 100%
rename from src/jira_hub/core/queue.py
rename to src/sdlc_hub/core/queue.py
diff --git a/src/jira_hub/core/registry.py b/src/sdlc_hub/core/registry.py
similarity index 100%
rename from src/jira_hub/core/registry.py
rename to src/sdlc_hub/core/registry.py
diff --git a/src/jira_hub/core/relay.py b/src/sdlc_hub/core/relay.py
similarity index 100%
rename from src/jira_hub/core/relay.py
rename to src/sdlc_hub/core/relay.py
diff --git a/src/jira_hub/core/release.py b/src/sdlc_hub/core/release.py
similarity index 100%
rename from src/jira_hub/core/release.py
rename to src/sdlc_hub/core/release.py
diff --git a/src/jira_hub/core/settings.py b/src/sdlc_hub/core/settings.py
similarity index 100%
rename from src/jira_hub/core/settings.py
rename to src/sdlc_hub/core/settings.py
diff --git a/src/jira_hub/modules/__init__.py b/src/sdlc_hub/modules/__init__.py
similarity index 100%
rename from src/jira_hub/modules/__init__.py
rename to src/sdlc_hub/modules/__init__.py
diff --git a/src/jira_hub/modules/jira/__init__.py b/src/sdlc_hub/modules/jira/__init__.py
similarity index 100%
rename from src/jira_hub/modules/jira/__init__.py
rename to src/sdlc_hub/modules/jira/__init__.py
diff --git a/src/jira_hub/modules/jira/api.py b/src/sdlc_hub/modules/jira/api.py
similarity index 100%
rename from src/jira_hub/modules/jira/api.py
rename to src/sdlc_hub/modules/jira/api.py
diff --git a/src/jira_hub/modules/jira/attachments.py b/src/sdlc_hub/modules/jira/attachments.py
similarity index 100%
rename from src/jira_hub/modules/jira/attachments.py
rename to src/sdlc_hub/modules/jira/attachments.py
diff --git a/src/jira_hub/modules/jira/cli.py b/src/sdlc_hub/modules/jira/cli.py
similarity index 91%
rename from src/jira_hub/modules/jira/cli.py
rename to src/sdlc_hub/modules/jira/cli.py
index f3aa3e5..89d5bb2 100644
--- a/src/jira_hub/modules/jira/cli.py
+++ b/src/sdlc_hub/modules/jira/cli.py
@@ -1,4 +1,4 @@
-"""Подкоманды модуля jira для jira-hub: import (CSV-пакет admin/jira-sync в заметки)."""
+"""Подкоманды модуля jira для sdlc-hub: import (CSV-пакет admin/jira-sync в заметки)."""
import json
from ...core import db, paths, settings
diff --git a/src/jira_hub/modules/jira/commands.py b/src/sdlc_hub/modules/jira/commands.py
similarity index 100%
rename from src/jira_hub/modules/jira/commands.py
rename to src/sdlc_hub/modules/jira/commands.py
diff --git a/src/jira_hub/modules/jira/fetch.py b/src/sdlc_hub/modules/jira/fetch.py
similarity index 100%
rename from src/jira_hub/modules/jira/fetch.py
rename to src/sdlc_hub/modules/jira/fetch.py
diff --git a/src/jira_hub/modules/jira/history.py b/src/sdlc_hub/modules/jira/history.py
similarity index 100%
rename from src/jira_hub/modules/jira/history.py
rename to src/sdlc_hub/modules/jira/history.py
diff --git a/src/jira_hub/modules/jira/hooks.py b/src/sdlc_hub/modules/jira/hooks.py
similarity index 100%
rename from src/jira_hub/modules/jira/hooks.py
rename to src/sdlc_hub/modules/jira/hooks.py
diff --git a/src/jira_hub/modules/jira/importer.py b/src/sdlc_hub/modules/jira/importer.py
similarity index 100%
rename from src/jira_hub/modules/jira/importer.py
rename to src/sdlc_hub/modules/jira/importer.py
diff --git a/src/jira_hub/modules/jira/mcp.py b/src/sdlc_hub/modules/jira/mcp.py
similarity index 100%
rename from src/jira_hub/modules/jira/mcp.py
rename to src/sdlc_hub/modules/jira/mcp.py
diff --git a/src/jira_hub/modules/jira/mirror.py b/src/sdlc_hub/modules/jira/mirror.py
similarity index 100%
rename from src/jira_hub/modules/jira/mirror.py
rename to src/sdlc_hub/modules/jira/mirror.py
diff --git a/src/jira_hub/modules/jira/project_fields.py b/src/sdlc_hub/modules/jira/project_fields.py
similarity index 100%
rename from src/jira_hub/modules/jira/project_fields.py
rename to src/sdlc_hub/modules/jira/project_fields.py
diff --git a/src/jira_hub/modules/jira/projects.py b/src/sdlc_hub/modules/jira/projects.py
similarity index 100%
rename from src/jira_hub/modules/jira/projects.py
rename to src/sdlc_hub/modules/jira/projects.py
diff --git a/src/jira_hub/modules/jira/refresh.py b/src/sdlc_hub/modules/jira/refresh.py
similarity index 100%
rename from src/jira_hub/modules/jira/refresh.py
rename to src/sdlc_hub/modules/jira/refresh.py
diff --git a/src/jira_hub/modules/jira/schema.py b/src/sdlc_hub/modules/jira/schema.py
similarity index 100%
rename from src/jira_hub/modules/jira/schema.py
rename to src/sdlc_hub/modules/jira/schema.py
diff --git a/src/jira_hub/modules/jira/settings.py b/src/sdlc_hub/modules/jira/settings.py
similarity index 100%
rename from src/jira_hub/modules/jira/settings.py
rename to src/sdlc_hub/modules/jira/settings.py
diff --git a/src/jira_hub/modules/wiki/__init__.py b/src/sdlc_hub/modules/wiki/__init__.py
similarity index 100%
rename from src/jira_hub/modules/wiki/__init__.py
rename to src/sdlc_hub/modules/wiki/__init__.py
diff --git a/src/jira_hub/modules/wiki/api.py b/src/sdlc_hub/modules/wiki/api.py
similarity index 100%
rename from src/jira_hub/modules/wiki/api.py
rename to src/sdlc_hub/modules/wiki/api.py
diff --git a/src/jira_hub/modules/wiki/attachments.py b/src/sdlc_hub/modules/wiki/attachments.py
similarity index 100%
rename from src/jira_hub/modules/wiki/attachments.py
rename to src/sdlc_hub/modules/wiki/attachments.py
diff --git a/src/jira_hub/modules/wiki/commands.py b/src/sdlc_hub/modules/wiki/commands.py
similarity index 100%
rename from src/jira_hub/modules/wiki/commands.py
rename to src/sdlc_hub/modules/wiki/commands.py
diff --git a/src/jira_hub/modules/wiki/convert.py b/src/sdlc_hub/modules/wiki/convert.py
similarity index 100%
rename from src/jira_hub/modules/wiki/convert.py
rename to src/sdlc_hub/modules/wiki/convert.py
diff --git a/src/jira_hub/modules/wiki/mcp.py b/src/sdlc_hub/modules/wiki/mcp.py
similarity index 100%
rename from src/jira_hub/modules/wiki/mcp.py
rename to src/sdlc_hub/modules/wiki/mcp.py
diff --git a/src/jira_hub/modules/wiki/paths.py b/src/sdlc_hub/modules/wiki/paths.py
similarity index 100%
rename from src/jira_hub/modules/wiki/paths.py
rename to src/sdlc_hub/modules/wiki/paths.py
diff --git a/src/jira_hub/modules/wiki/projects.py b/src/sdlc_hub/modules/wiki/projects.py
similarity index 100%
rename from src/jira_hub/modules/wiki/projects.py
rename to src/sdlc_hub/modules/wiki/projects.py
diff --git a/src/jira_hub/modules/wiki/roots.py b/src/sdlc_hub/modules/wiki/roots.py
similarity index 100%
rename from src/jira_hub/modules/wiki/roots.py
rename to src/sdlc_hub/modules/wiki/roots.py
diff --git a/src/jira_hub/modules/wiki/schema.py b/src/sdlc_hub/modules/wiki/schema.py
similarity index 100%
rename from src/jira_hub/modules/wiki/schema.py
rename to src/sdlc_hub/modules/wiki/schema.py
diff --git a/src/jira_hub/modules/wiki/search.py b/src/sdlc_hub/modules/wiki/search.py
similarity index 100%
rename from src/jira_hub/modules/wiki/search.py
rename to src/sdlc_hub/modules/wiki/search.py
diff --git a/src/jira_hub/modules/wiki/settings.py b/src/sdlc_hub/modules/wiki/settings.py
similarity index 100%
rename from src/jira_hub/modules/wiki/settings.py
rename to src/sdlc_hub/modules/wiki/settings.py
diff --git a/src/jira_hub/modules/wiki/stage.py b/src/sdlc_hub/modules/wiki/stage.py
similarity index 100%
rename from src/jira_hub/modules/wiki/stage.py
rename to src/sdlc_hub/modules/wiki/stage.py
diff --git a/src/jira_hub/modules/wiki/sync.py b/src/sdlc_hub/modules/wiki/sync.py
similarity index 100%
rename from src/jira_hub/modules/wiki/sync.py
rename to src/sdlc_hub/modules/wiki/sync.py
diff --git a/src/jira_hub/modules/wiki/tick.py b/src/sdlc_hub/modules/wiki/tick.py
similarity index 100%
rename from src/jira_hub/modules/wiki/tick.py
rename to src/sdlc_hub/modules/wiki/tick.py
diff --git a/tests/conftest.py b/tests/conftest.py
index ca94e8a..55ca5a5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,12 +5,12 @@ import threading
import pytest
-from jira_hub.core import api, db, projects, registry, settings
-from jira_hub.core.client import HubClient
-from jira_hub.core.modules import CommandHooks, Module
-from jira_hub.modules.jira import MODULE as JIRA_MODULE
-from jira_hub.modules.jira import project_fields
-from jira_hub.modules.jira import projects as jira_projects
+from sdlc_hub.core import api, db, projects, registry, settings
+from sdlc_hub.core.client import HubClient
+from sdlc_hub.core.modules import CommandHooks, Module
+from sdlc_hub.modules.jira import MODULE as JIRA_MODULE
+from sdlc_hub.modules.jira import project_fields
+from sdlc_hub.modules.jira import projects as jira_projects
@pytest.fixture
diff --git a/tests/test_agents.py b/tests/test_agents.py
index 1840ed9..9a6d2f6 100644
--- a/tests/test_agents.py
+++ b/tests/test_agents.py
@@ -3,8 +3,8 @@ import os
import pytest
-from jira_hub.core import api, cli, db, queue, release, settings
-from jira_hub.core.client import HubError
+from sdlc_hub.core import api, cli, db, queue, release, settings
+from sdlc_hub.core.client import HubError
from v6db import make_v6_db
@@ -91,7 +91,7 @@ def test_agent_query_cannot_be_forged(hub_server_two_modules):
def test_tokens_cli_lists_agents(tmp_path, monkeypatch, capsys):
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path))
assert cli.main(["tokens"]) == 0
lines = capsys.readouterr().out.splitlines()
assert lines[0].startswith("клиентский токен: ") and lines[1].startswith("агент jira: ")
@@ -223,7 +223,7 @@ def test_release_dirs_are_private(tmp_path):
def test_publish_unknown_module(tmp_path, monkeypatch, capsys):
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home"))
src = tmp_path / "agent.py"
src.write_text("AGENT_VERSION = 1\n", encoding="utf-8")
assert cli.main(["agent", "publish", "--module", "nope", str(src)]) == 2
@@ -231,7 +231,7 @@ def test_publish_unknown_module(tmp_path, monkeypatch, capsys):
def test_publish_module_option(tmp_path, monkeypatch, capsys):
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home"))
src = tmp_path / "agent.py"
src.write_text("AGENT_VERSION = 1\n", encoding="utf-8")
assert cli.main(["agent", "publish", "--module", "jira", str(src)]) == 0
@@ -293,7 +293,7 @@ def test_v14_agent_over_http_on_v6_base(tmp_path):
import threading
from pathlib import Path
- from jira_hub.core.client import HubClient
+ from sdlc_hub.core.client import HubClient
make_v6_db(tmp_path / "hub.sqlite")
old = tmp_path / "release" # прежняя раскладка v14: release/agent.py
diff --git a/tests/test_api.py b/tests/test_api.py
index a4123b5..7b64479 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -11,10 +11,10 @@ from datetime import timedelta
import pytest
-from jira_hub.core import api, db, files, queue, release, settings
-from jira_hub.core.client import HubClient, HubError
-from jira_hub.modules.jira import api as jira_api
-from jira_hub.modules.jira.commands import CommandError
+from sdlc_hub.core import api, db, files, queue, release, settings
+from sdlc_hub.core.client import HubClient, HubError
+from sdlc_hub.modules.jira import api as jira_api
+from sdlc_hub.modules.jira.commands import CommandError
@pytest.fixture
@@ -158,7 +158,7 @@ def test_backup_created_on_first_write(hub, tmp_path):
def test_client_unavailable():
- from jira_hub.core.client import HubUnavailable
+ from sdlc_hub.core.client import HubUnavailable
with pytest.raises(HubUnavailable):
HubClient("http://127.0.0.1:9", "t", timeout=1).get("/issues")
@@ -1059,7 +1059,7 @@ ATT_NAME = "скрин 1.png"
def _seed_attachment(hub, mime="image/png", size=3, name=ATT_NAME):
conn = hub["hub"].conn()
try:
- from jira_hub.modules.jira import mirror
+ from sdlc_hub.modules.jira import mirror
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "labels": [], "attachments": [
{"id": "31337", "filename": name, "size": size, "mime_type": mime, "created": "2026-10-20T10:00:00.000+0300"}]}], [])
finally:
diff --git a/tests/test_attachments.py b/tests/test_attachments.py
index a66812a..8018dea 100644
--- a/tests/test_attachments.py
+++ b/tests/test_attachments.py
@@ -2,8 +2,8 @@ import json
import pytest
-from jira_hub.core import db, files
-from jira_hub.modules.jira import attachments, mirror
+from sdlc_hub.core import db, files
+from sdlc_hub.modules.jira import attachments, mirror
def test_fmt_size():
diff --git a/tests/test_baseline.py b/tests/test_baseline.py
index 4cbdae4..f14e913 100644
--- a/tests/test_baseline.py
+++ b/tests/test_baseline.py
@@ -5,8 +5,8 @@ import json
import os
from pathlib import Path
-from jira_hub.core import api, db, settings
-from jira_hub.modules.jira import mcp as jira_mcp
+from sdlc_hub.core import api, db, settings
+from sdlc_hub.modules.jira import mcp as jira_mcp
FIXTURES = Path(__file__).parent / "fixtures" / "baseline"
@@ -27,7 +27,7 @@ def test_mcp_tools():
def test_mcp_wiki_tools():
- from jira_hub.modules.wiki import mcp as wiki_mcp
+ from sdlc_hub.modules.wiki import mcp as wiki_mcp
tools = asyncio.run(wiki_mcp.mcp.list_tools())
_check("mcp_wiki_tools", [{"name": t.name, "description": t.description, "inputSchema": t.inputSchema}
for t in tools])
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ebe21ab..686cc8a 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2,13 +2,13 @@ import json
import os
from pathlib import Path
-from jira_hub.core import api, cli, paths, release, settings
+from sdlc_hub.core import api, cli, paths, release, settings
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
def test_init_import_tokens(tmp_path, monkeypatch, capsys, open_db):
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home"))
assert cli.main(["init", "--config", str(FIXTURE)]) == 0
out = capsys.readouterr().out
assert "transitions_map" in out and "агент jira: " in out
@@ -31,7 +31,7 @@ def test_init_import_tokens(tmp_path, monkeypatch, capsys, open_db):
def test_agent_publish(tmp_path, monkeypatch, capsys):
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home"))
src = tmp_path / "agent.py"
src.write_text("AGENT_VERSION = 6\n", encoding="utf-8")
assert cli.main(["agent", "publish", str(src)]) == 0
@@ -44,7 +44,7 @@ def test_agent_publish(tmp_path, monkeypatch, capsys):
def test_agent_publish_write_failure(tmp_path, monkeypatch, capsys):
"""Диск полон при записи манифеста: сообщение и код 1 без трейсбека, опубликованной остаётся v8 (M-1 #25)."""
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home"))
src = tmp_path / "agent.py"
src.write_text("AGENT_VERSION = 8\n", encoding="utf-8")
assert cli.main(["agent", "publish", str(src)]) == 0
@@ -67,8 +67,8 @@ def test_agent_publish_write_failure(tmp_path, monkeypatch, capsys):
def test_mcp_default_module(monkeypatch):
- from jira_hub.core import mcp as core_mcp
- from jira_hub.modules.jira import mcp as jira_mcp
+ from sdlc_hub.core import mcp as core_mcp
+ from sdlc_hub.modules.jira import mcp as jira_mcp
seen = []
monkeypatch.setattr(core_mcp, "PROJECT", None)
monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append((transport, core_mcp.PROJECT)))
@@ -82,7 +82,7 @@ def test_mcp_default_module(monkeypatch):
def test_mcp_unknown_module(monkeypatch, capsys):
- from jira_hub.modules.jira import mcp as jira_mcp
+ from sdlc_hub.modules.jira import mcp as jira_mcp
seen = []
monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append(transport))
assert cli.main(["mcp", "nope"]) == 2
@@ -93,7 +93,7 @@ def test_mcp_unknown_module(monkeypatch, capsys):
def test_agent_publish_migrates_old_layout(tmp_path, monkeypatch, capsys):
"""M-3: корневые agent.py/manifest.json прежней раскладки переезжают в release/jira/ до проверки «только вверх»."""
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path))
rel = tmp_path / "release"
rel.mkdir()
old = tmp_path / "old.py"
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 0e30b7c..b047460 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -1,7 +1,7 @@
import pytest
-from jira_hub.modules.jira import commands, mirror
-from jira_hub.modules.jira.commands import CommandError
+from sdlc_hub.modules.jira import commands, mirror
+from sdlc_hub.modules.jira.commands import CommandError
NOW = "strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now')"
diff --git a/tests/test_db.py b/tests/test_db.py
index 52872d7..92ecfe9 100644
--- a/tests/test_db.py
+++ b/tests/test_db.py
@@ -7,8 +7,8 @@ import threading
import pytest
-from jira_hub.core import api, backup, db, paths, projects, queue, settings
-from jira_hub.modules.jira import history
+from sdlc_hub.core import api, backup, db, paths, projects, queue, settings
+from sdlc_hub.modules.jira import history
def test_connect_creates_schema(open_db):
@@ -155,7 +155,7 @@ def test_data_dir_is_private(tmp_path):
def test_existing_backups_dir_made_private_on_start(tmp_path):
- from jira_hub.core import api
+ from sdlc_hub.core import api
backups = tmp_path / "backups"
backups.mkdir()
backups.chmod(0o755)
diff --git a/tests/test_e2e_hub.py b/tests/test_e2e_hub.py
index 4953ae1..0a2ddb6 100644
--- a/tests/test_e2e_hub.py
+++ b/tests/test_e2e_hub.py
@@ -2,8 +2,8 @@ import threading
import pytest
-from jira_hub.core import api, settings
-from jira_hub.core.client import HubClient, HubError
+from sdlc_hub.core import api, settings
+from sdlc_hub.core.client import HubClient, HubError
@pytest.fixture
diff --git a/tests/test_files.py b/tests/test_files.py
index bbdf65a..07f536f 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -7,7 +7,7 @@ import time
import pytest
-from jira_hub.core import db, files, settings
+from sdlc_hub.core import db, files, settings
ABC = hashlib.sha256(b"abc").hexdigest() # ba7816bf…
DAY = 86400
diff --git a/tests/test_history.py b/tests/test_history.py
index 5ab7e6b..dcb57c2 100644
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -1,6 +1,6 @@
import pytest
-from jira_hub.modules.jira import history
+from sdlc_hub.modules.jira import history
ITEM = {"history_id": "5001", "seq": 0, "key": "X", "created": "2026-10-22T09:00:00.000+0300", "author": "Петров Пётр",
"field_id": "status", "field_name": "status", "from": "3", "from_string": "В работе", "to": "10001",
@@ -29,7 +29,7 @@ def test_record_keeps_first_version(conn):
def test_record_uses_field_settings(conn):
- from jira_hub.core import settings
+ from sdlc_hub.core import settings
settings.set_(conn, "field_ids", {"planned_end": "customfield_10201"})
history.record_changes(conn, [{**ITEM, "field_id": "customfield_10201", "field_name": "Плановое окончание"}])
assert conn.execute("SELECT field FROM issue_change").fetchone()["field"] == "planned_end"
@@ -92,7 +92,7 @@ def test_feed_closed_in_two_hours(conn): # сце
def test_feed_comment(conn): # «Комментарий в ленте»
- from jira_hub.modules.jira import mirror
+ from sdlc_hub.modules.jira import mirror
mirror.apply_state(conn, [{"key": "X", "labels": ["AWPCA"], "comments": [
{"id": "7", "author": "Иванов", "body": "Готово к проверке", "created": "2026-10-22T13:30:00.000+0300"}]}], [])
got = history.feed(conn, "2026-10-22T10:00:00+00:00", "2026-10-22T12:00:00+00:00", history.DEFAULT_KINDS)
@@ -163,7 +163,7 @@ def test_parse_kinds_unknown():
def test_issue_changes(conn): # «Задача с историей»
- from jira_hub.modules.jira import mirror
+ from sdlc_hub.modules.jira import mirror
_issue(conn, "X", ["AWPCA"])
_change(conn, "X", "1", "2026-10-21T11:00:00Z", "status", "a", "b")
_change(conn, "X", "2", "2026-10-22T11:00:00Z", "description", "c", "d", field_id="description")
diff --git a/tests/test_importer.py b/tests/test_importer.py
index 949919b..f26b5ac 100644
--- a/tests/test_importer.py
+++ b/tests/test_importer.py
@@ -2,8 +2,8 @@ import csv
import pytest
-from jira_hub.core import settings
-from jira_hub.modules.jira import importer, mirror
+from sdlc_hub.core import settings
+from sdlc_hub.modules.jira import importer, mirror
@pytest.fixture
diff --git a/tests/test_layout.py b/tests/test_layout.py
index d3bf289..79131d5 100644
--- a/tests/test_layout.py
+++ b/tests/test_layout.py
@@ -3,21 +3,34 @@ import subprocess
import sys
from pathlib import Path
-import jira_hub
+import sdlc_hub
def test_fresh_imports():
r = subprocess.run(
- [sys.executable, "-c", "import jira_hub.core.api, jira_hub.core.mcp, jira_hub.modules.jira.mcp, jira_hub.cli"],
+ [sys.executable, "-c", "import sdlc_hub.core.api, sdlc_hub.core.mcp, sdlc_hub.modules.jira.mcp, sdlc_hub.cli"],
capture_output=True, text=True)
assert r.returncode == 0, r.stderr
def test_web_app_path():
- from jira_hub.core import api
- assert api.WEB_APP == Path(jira_hub.__file__).parent / "web" / "app"
+ from sdlc_hub.core import api
+ assert api.WEB_APP == Path(sdlc_hub.__file__).parent / "web" / "app"
def test_hub_root():
- from jira_hub.core import mcp
+ from sdlc_hub.core import mcp
assert (mcp.HUB_ROOT / "pyproject.toml").is_file()
+
+
+def test_default_home(monkeypatch, tmp_path):
+ from sdlc_hub.core import paths
+ monkeypatch.delenv("SDLC_HUB_HOME", raising=False)
+ monkeypatch.setenv("HOME", str(tmp_path))
+ assert paths.home() == tmp_path / ".local" / "share" / "sdlc-hub"
+
+
+def test_env_home(monkeypatch, tmp_path):
+ from sdlc_hub.core import paths
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "x"))
+ assert paths.home() == tmp_path / "x"
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index 67b000c..1d17711 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -9,11 +9,11 @@ from types import SimpleNamespace
import pytest
-from jira_hub.core import api, files, settings
-from jira_hub.core import mcp as core_mcp
-from jira_hub.modules.jira import mcp as jmcp
-from jira_hub.modules.jira import commands
-from jira_hub.core.client import HubClient
+from sdlc_hub.core import api, files, settings
+from sdlc_hub.core import mcp as core_mcp
+from sdlc_hub.modules.jira import mcp as jmcp
+from sdlc_hub.modules.jira import commands
+from sdlc_hub.core.client import HubClient
@pytest.fixture
@@ -700,7 +700,7 @@ class FakeHub:
@pytest.fixture
def downloads(tmp_path, monkeypatch):
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home"))
monkeypatch.setattr(core_mcp, "PROJECT", "AWPCA")
monkeypatch.setattr(jmcp, "POLL_SECONDS", 0.05)
return tmp_path / "home" / "downloads"
@@ -819,7 +819,7 @@ def test_attachment_failed(downloads, monkeypatch):
def test_attachment_413(downloads, monkeypatch):
- from jira_hub.core.client import HubError
+ from sdlc_hub.core.client import HubError
att = _att("5", "запись.mp4", 26214400)
hub = FakeHub(_detail(key=ATT_KEY, attachments=[att]), fetch=HubError(413, "too_large", "большой"))
_use(monkeypatch, hub)
@@ -828,7 +828,7 @@ def test_attachment_413(downloads, monkeypatch):
def test_attachment_409_passes_hub_text(downloads, monkeypatch):
- from jira_hub.core.client import HubError
+ from sdlc_hub.core.client import HubError
hub = FakeHub(_detail(key=ATT_KEY, attachments=[_att("5", "a.png", 1)]),
fetch=HubError(409, "agent_too_old", "агент v9 не умеет скачивать вложения — нужен v14"))
_use(monkeypatch, hub)
diff --git a/tests/test_mirror.py b/tests/test_mirror.py
index fe33e17..96c2aff 100644
--- a/tests/test_mirror.py
+++ b/tests/test_mirror.py
@@ -1,7 +1,7 @@
import pytest
-from jira_hub.core import db
-from jira_hub.modules.jira import mirror
+from sdlc_hub.core import db
+from sdlc_hub.modules.jira import mirror
@pytest.fixture
diff --git a/tests/test_project_fields.py b/tests/test_project_fields.py
index a83b9c4..09d3e2c 100644
--- a/tests/test_project_fields.py
+++ b/tests/test_project_fields.py
@@ -1,10 +1,10 @@
"""Поля проекта от модулей; доски — в модуле jira (wiki-module, решение 8, спеки modules/data-model/hub-api)."""
import pytest
-from jira_hub.core import db, mcp as core_mcp, registry
-from jira_hub.core.client import HubError
-from jira_hub.core.modules import Module, ProjectField
-from jira_hub.modules.jira import mirror
+from sdlc_hub.core import db, mcp as core_mcp, registry
+from sdlc_hub.core.client import HubError
+from sdlc_hub.core.modules import Module, ProjectField
+from sdlc_hub.modules.jira import mirror
from v6db import make_v6_db
@@ -80,7 +80,7 @@ def test_editable_field_needs_updater():
def test_migrate_v6_idempotent(tmp_path):
- from jira_hub.modules.jira import projects as jira_projects
+ from sdlc_hub.modules.jira import projects as jira_projects
make_v6_db(tmp_path / "hub.sqlite")
conn = db.connect(tmp_path / "hub.sqlite")
try:
@@ -168,9 +168,9 @@ def two_modules(monkeypatch, open_db):
"""Реестр [jira, stub]: stub объявляет изменяемое поле root, хранит его в setting и ставит пакеты-пустышки."""
import json
import re
- from jira_hub.core import api, queue
- from jira_hub.core.http import ApiError
- from jira_hub.modules.jira import MODULE as JIRA
+ from sdlc_hub.core import api, queue
+ from sdlc_hub.core.http import ApiError
+ from sdlc_hub.modules.jira import MODULE as JIRA
def check_root(v):
if not isinstance(v, str) or not v:
diff --git a/tests/test_projects.py b/tests/test_projects.py
index dee6f9c..e06691c 100644
--- a/tests/test_projects.py
+++ b/tests/test_projects.py
@@ -1,8 +1,8 @@
import pytest
-from jira_hub.core import api, projects, settings
-from jira_hub.modules.jira import mirror
-from jira_hub.modules.jira import projects as jira_projects
+from sdlc_hub.core import api, projects, settings
+from sdlc_hub.modules.jira import mirror
+from sdlc_hub.modules.jira import projects as jira_projects
@pytest.fixture
diff --git a/tests/test_queue.py b/tests/test_queue.py
index 30211e2..e587a36 100644
--- a/tests/test_queue.py
+++ b/tests/test_queue.py
@@ -2,12 +2,12 @@ import hashlib
import pytest
-from jira_hub.core import api, db, files, queue, settings
-from jira_hub.modules.jira import attachments, commands, mirror, refresh
-from jira_hub.modules.jira import fetch as jira_fetch
-from jira_hub.modules.jira import projects as jira_projects
-from jira_hub.modules.jira.commands import CommandError
-from jira_hub.core.queue import ClaimedError
+from sdlc_hub.core import api, db, files, queue, settings
+from sdlc_hub.modules.jira import attachments, commands, mirror, refresh
+from sdlc_hub.modules.jira import fetch as jira_fetch
+from sdlc_hub.modules.jira import projects as jira_projects
+from sdlc_hub.modules.jira.commands import CommandError
+from sdlc_hub.core.queue import ClaimedError
LT = {"связанные": "ASUPR_Связанные"}
@@ -210,7 +210,7 @@ def test_no_backup_for_refresh_batch(conn):
def test_make_backup_rotates(tmp_path, open_db):
- from jira_hub.core import backup
+ from sdlc_hub.core import backup
conn = open_db(tmp_path / "hub.sqlite")
for i in range(4):
backup.make_backup(conn, tmp_path / "backups", f"b{i}", keep=2)
@@ -221,7 +221,7 @@ def test_make_backup_rotates(tmp_path, open_db):
def test_rotation_keeps_schema_migration_copy(tmp_path, open_db):
"""Копия базы перед миграцией схемы не участвует в ротации копий пакетов (M-1 финального ревью)."""
- from jira_hub.core import backup
+ from sdlc_hub.core import backup
conn = open_db(tmp_path / "hub.sqlite")
backup.make_backup(conn, tmp_path / "backups", "schema-v1", keep=0)
for i in range(4):
diff --git a/tests/test_queue_core.py b/tests/test_queue_core.py
index e882ccb..a8cefc5 100644
--- a/tests/test_queue_core.py
+++ b/tests/test_queue_core.py
@@ -5,10 +5,10 @@ from dataclasses import replace
import pytest
-from jira_hub.core import files, queue, registry
-from jira_hub.core.errors import CommandError
-from jira_hub.core.modules import CommandHooks, Module
-from jira_hub.modules import jira
+from sdlc_hub.core import files, queue, registry
+from sdlc_hub.core.errors import CommandError
+from sdlc_hub.core.modules import CommandHooks, Module
+from sdlc_hub.modules import jira
SHA = "a" * 64
diff --git a/tests/test_registry.py b/tests/test_registry.py
index c9f9ad5..5a4534d 100644
--- a/tests/test_registry.py
+++ b/tests/test_registry.py
@@ -5,10 +5,10 @@ from pathlib import Path
import pytest
-from jira_hub.core import registry
-from jira_hub.core.modules import CommandHooks, Module
+from sdlc_hub.core import registry
+from sdlc_hub.core.modules import CommandHooks, Module
-CORE = Path(__file__).resolve().parent.parent / "src" / "jira_hub" / "core"
+CORE = Path(__file__).resolve().parent.parent / "src" / "sdlc_hub" / "core"
# временно: R-1, список только сокращается; после Task 6 пуст
PENDING: set[str] = set()
@@ -50,7 +50,7 @@ def test_route_conflict():
def test_routes_assembled():
- from jira_hub.core import api
+ from sdlc_hub.core import api
pairs = lambda routes: {(m, p) for m, p, _, _ in routes} # noqa: E731
mods = tuple(r for m in registry.modules() for r in m.routes)
assert pairs(api.ROUTES) == pairs(api.CORE_ROUTES) | pairs(mods)
@@ -65,18 +65,18 @@ def test_module_of(monkeypatch):
def _module_imports(tree, top_level_only):
- """Импорты пакета jira_hub.modules (абсолютные и относительные из core/)."""
+ """Импорты пакета sdlc_hub.modules (абсолютные и относительные из core/)."""
found = []
for n in (tree.body if top_level_only else ast.walk(tree)):
if isinstance(n, ast.Import):
names = [a.name for a in n.names]
elif isinstance(n, ast.ImportFrom):
- prefix = {0: "", 1: "jira_hub.core.", 2: "jira_hub."}[n.level]
+ prefix = {0: "", 1: "sdlc_hub.core.", 2: "sdlc_hub."}[n.level]
base = (prefix + (n.module or "")).rstrip(".")
names = [base] + [base + "." + a.name for a in n.names]
else:
continue
- if any(x == "jira_hub.modules" or x.startswith("jira_hub.modules.") for x in names):
+ if any(x == "sdlc_hub.modules" or x.startswith("sdlc_hub.modules.") for x in names):
found.append(n)
return found
diff --git a/tests/test_relay.py b/tests/test_relay.py
index a351d4e..1d4792d 100644
--- a/tests/test_relay.py
+++ b/tests/test_relay.py
@@ -8,8 +8,8 @@ from datetime import datetime, timedelta, timezone
import pytest
-from jira_hub.core import api, relay, settings
-from jira_hub.core.client import HubClient
+from sdlc_hub.core import api, relay, settings
+from sdlc_hub.core.client import HubClient
@pytest.fixture
diff --git a/tests/test_release.py b/tests/test_release.py
index 8044cd5..d98abe4 100644
--- a/tests/test_release.py
+++ b/tests/test_release.py
@@ -6,7 +6,7 @@ from pathlib import Path
import pytest
-from jira_hub.core import release
+from sdlc_hub.core import release
def agent_file(tmp_path, version, body="print('x')\n", name="agent.py"):
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 70a82fb..337bf24 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -3,9 +3,9 @@ from pathlib import Path
import pytest
-from jira_hub.core import queue, settings
-from jira_hub.modules.jira import settings as jira_settings
-from jira_hub.modules.jira.commands import CommandError
+from sdlc_hub.core import queue, settings
+from sdlc_hub.modules.jira import settings as jira_settings
+from sdlc_hub.modules.jira.commands import CommandError
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
diff --git a/tests/test_web_app.py b/tests/test_web_app.py
index c4db10b..7f55cb5 100644
--- a/tests/test_web_app.py
+++ b/tests/test_web_app.py
@@ -5,7 +5,7 @@ from urllib.parse import urlparse
import pytest
-from jira_hub.core import api
+from sdlc_hub.core import api
def _get(base, path):
@@ -100,7 +100,7 @@ def test_other_paths_are_not_app(app_hub):
def test_default_web_dir_is_package_build(tmp_path):
h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
assert h.web_dir == api.WEB_APP
- assert api.WEB_APP.parts[-3:] == ("jira_hub", "web", "app")
+ assert api.WEB_APP.parts[-3:] == ("sdlc_hub", "web", "app")
@pytest.mark.parametrize("path", ["/", "/links"])
diff --git a/tests/test_wiki_api.py b/tests/test_wiki_api.py
index a1b2151..055061d 100644
--- a/tests/test_wiki_api.py
+++ b/tests/test_wiki_api.py
@@ -8,8 +8,8 @@ from urllib.parse import quote
import pytest
-from jira_hub.core import files
-from jira_hub.core.client import HubError
+from sdlc_hub.core import files
+from sdlc_hub.core.client import HubError
from test_wiki_convert import FILE_REF, FILE_URL, THUMB_REF, _html
from test_wiki_sync import CODES, TREE, api, first_sync, page # noqa: F401 — api — фикстура
diff --git a/tests/test_wiki_commands.py b/tests/test_wiki_commands.py
index 3804f95..70497be 100644
--- a/tests/test_wiki_commands.py
+++ b/tests/test_wiki_commands.py
@@ -3,10 +3,10 @@ from datetime import timedelta
import pytest
-from jira_hub.core import db, queue, settings
-from jira_hub.core.errors import CommandError
-from jira_hub.modules.jira import refresh
-from jira_hub.modules.wiki import commands, settings as wiki_settings, tick
+from sdlc_hub.core import db, queue, settings
+from sdlc_hub.core.errors import CommandError
+from sdlc_hub.modules.jira import refresh
+from sdlc_hub.modules.wiki import commands, settings as wiki_settings, tick
@pytest.fixture
diff --git a/tests/test_wiki_convert.py b/tests/test_wiki_convert.py
index 632d60c..ab3759e 100644
--- a/tests/test_wiki_convert.py
+++ b/tests/test_wiki_convert.py
@@ -2,7 +2,7 @@
import re
from pathlib import Path
-from jira_hub.modules.wiki import convert
+from sdlc_hub.modules.wiki import convert
FIX = Path(__file__).parent / "fixtures" / "wiki"
FILE_URL = "https://eva.example.test/files/obj/CmfDocument/CmfDocument%3Aa08/CmfDocument%3A3f2a9c1e-0b7d-4e8a-9c21-5d6e7f8a9b0c/model.xlsx?version=1"
diff --git a/tests/test_wiki_mcp.py b/tests/test_wiki_mcp.py
index ada9cb4..d316fd7 100644
--- a/tests/test_wiki_mcp.py
+++ b/tests/test_wiki_mcp.py
@@ -3,10 +3,10 @@ import asyncio
import pytest
-from jira_hub.core import mcp as core_mcp
-from jira_hub.core import registry
-from jira_hub.core.client import HubError
-from jira_hub.modules.wiki import mcp as wmcp
+from sdlc_hub.core import mcp as core_mcp
+from sdlc_hub.core import registry
+from sdlc_hub.core.client import HubError
+from sdlc_hub.modules.wiki import mcp as wmcp
PROJECTS = {"projects": [
{"label": "AWPCA", "name": "АРМ КА", "wiki_root": "DOC-1", "wiki_root_title": "Корень", "wiki_pages": 3,
@@ -50,7 +50,7 @@ class Fake:
@pytest.fixture
def hub(monkeypatch, tmp_path):
- monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home"))
monkeypatch.setattr(wmcp, "POLL_SECONDS", 0.01)
monkeypatch.setattr(core_mcp, "PROJECT", "AWPCA")
diff --git a/tests/test_wiki_projects.py b/tests/test_wiki_projects.py
index 48d3f6c..d8a053b 100644
--- a/tests/test_wiki_projects.py
+++ b/tests/test_wiki_projects.py
@@ -3,8 +3,8 @@ import json
import pytest
-from jira_hub.core.client import HubError
-from jira_hub.modules.wiki.projects import parse_root
+from sdlc_hub.core.client import HubError
+from sdlc_hub.modules.wiki.projects import parse_root
def test_parse_root():
@@ -138,7 +138,7 @@ def test_wiki_mcp_imports_no_db_or_queue():
"""M-5: mcp.py и roots.py вики не импортируют core.db и core.queue ни прямо, ни через wiki.projects."""
import ast
from pathlib import Path
- base = Path(__file__).parent.parent / "src" / "jira_hub" / "modules" / "wiki"
+ base = Path(__file__).parent.parent / "src" / "sdlc_hub" / "modules" / "wiki"
for name in ("mcp.py", "roots.py"):
tree = ast.parse((base / name).read_text(encoding="utf-8"))
mods = []
diff --git a/tests/test_wiki_sync.py b/tests/test_wiki_sync.py
index 6469759..8f4c1f0 100644
--- a/tests/test_wiki_sync.py
+++ b/tests/test_wiki_sync.py
@@ -8,9 +8,9 @@ from types import SimpleNamespace
import pytest
-from jira_hub.core import db, queue, settings
-from jira_hub.core.client import HubClient, HubError
-from jira_hub.modules.wiki import commands, convert, projects, search, stage, sync, tick
+from sdlc_hub.core import db, queue, settings
+from sdlc_hub.core.client import HubClient, HubError
+from sdlc_hub.modules.wiki import commands, convert, projects, search, stage, sync, tick
def page(code, title, parent=None, orderno=None, html=None, attachments=None, links=None):
@@ -625,6 +625,6 @@ def test_lone_surrogate_in_page_does_not_break_sync(api):
def test_dir_name_escapes_leading_dot():
"""M-14: метка с ведущей точкой не даёт каталог вида .X.new.5 / .X.old, который уборка при старте сочла бы своим."""
- from jira_hub.modules.wiki import paths
+ from sdlc_hub.modules.wiki import paths
assert paths.dir_name(".X.new.5") == "%2EX.new.5" and paths.dir_name(".") == "%2E" and paths.dir_name("..") == "%2E."
assert not paths.dir_name(".X.old").startswith(".") and paths.dir_name("A.B") == "A.B"
diff --git a/uv.lock b/uv.lock
index 8119aaa..3ad5894 100644
--- a/uv.lock
+++ b/uv.lock
@@ -277,31 +277,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
]
-[[package]]
-name = "jira-hub"
-version = "0.1.0"
-source = { editable = "." }
-dependencies = [
- { name = "beautifulsoup4" },
- { name = "markdownify" },
- { name = "mcp" },
-]
-
-[package.dev-dependencies]
-dev = [
- { name = "pytest" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "beautifulsoup4", specifier = ">=4.12" },
- { name = "markdownify", specifier = ">=1.2.3,<2" },
- { name = "mcp", specifier = ">=1.2,<2" },
-]
-
-[package.metadata.requires-dev]
-dev = [{ name = "pytest", specifier = ">=8" }]
-
[[package]]
name = "jsonschema"
version = "4.26.0"
@@ -677,6 +652,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" },
]
+[[package]]
+name = "sdlc-hub"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "markdownify" },
+ { name = "mcp" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "beautifulsoup4", specifier = ">=4.12" },
+ { name = "markdownify", specifier = ">=1.2.3,<2" },
+ { name = "mcp", specifier = ">=1.2,<2" },
+]
+
+[package.metadata.requires-dev]
+dev = [{ name = "pytest", specifier = ">=8" }]
+
[[package]]
name = "six"
version = "1.17.0"
diff --git a/web/playwright.config.ts b/web/playwright.config.ts
index 61997b0..4ce221d 100644
--- a/web/playwright.config.ts
+++ b/web/playwright.config.ts
@@ -17,6 +17,6 @@ export default defineConfig({
stdout: "pipe",
stderr: "pipe",
timeout: 120_000,
- env: { JIRA_HUB_HOME: HUB_HOME, PORT: String(HUB_PORT) },
+ env: { SDLC_HUB_HOME: HUB_HOME, PORT: String(HUB_PORT) },
},
});
diff --git a/web/scripts/seed-hub.py b/web/scripts/seed-hub.py
index e91ab7d..b18ebee 100644
--- a/web/scripts/seed-hub.py
+++ b/web/scripts/seed-hub.py
@@ -5,9 +5,9 @@ import json
import sys
from pathlib import Path
-from jira_hub.core import db, paths, settings
-from jira_hub.modules.jira import mirror
-from jira_hub.modules.wiki import paths as wiki_paths, search as wiki_search, sync as wiki_sync
+from sdlc_hub.core import db, paths, settings
+from sdlc_hub.modules.jira import mirror
+from sdlc_hub.modules.wiki import paths as wiki_paths, search as wiki_search, sync as wiki_sync
snap = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
extras_path = Path(__file__).with_name("seed-extras.json")
diff --git a/web/scripts/test-hub.sh b/web/scripts/test-hub.sh
index 264c493..e1ae05c 100644
--- a/web/scripts/test-hub.sh
+++ b/web/scripts/test-hub.sh
@@ -1,23 +1,23 @@
#!/usr/bin/env bash
-# Тестовый хаб для разработки фронта и e2e: свой JIRA_HUB_HOME (по умолчанию web/.e2e-hub, очищается), порт 8791,
-# зеркало — фикстура hub-basic. Живой хаб (:8790, ~/.local/share/jira-hub) не трогается.
+# Тестовый хаб для разработки фронта и e2e: свой SDLC_HUB_HOME (по умолчанию web/.e2e-hub, очищается), порт 8791,
+# зеркало — фикстура hub-basic. Живой хаб (:8790, ~/.local/share/sdlc-hub) не трогается.
set -euo pipefail
WEB=$(cd "$(dirname "$0")/.." && pwd)
ROOT=$(cd "$WEB/.." && pwd)
-export JIRA_HUB_HOME="${JIRA_HUB_HOME:-$WEB/.e2e-hub}"
+export SDLC_HUB_HOME="${SDLC_HUB_HOME:-$WEB/.e2e-hub}"
PORT="${PORT:-8791}" # держать равным HUB_PORT в playwright.config.ts
-case "$JIRA_HUB_HOME" in "$HOME/.local/share/jira-hub"*) echo "отказ: это каталог живого хаба" >&2; exit 2;; esac
+case "$SDLC_HUB_HOME" in "$HOME/.local/share/sdlc-hub"*) echo "отказ: это каталог живого хаба" >&2; exit 2;; esac
# чистим только каталог внутри web/.e2e-hub (T15.5): чужой путь из переменной окружения не удаляем
-case "$JIRA_HUB_HOME" in
- *..*) echo "отказ: JIRA_HUB_HOME=$JIRA_HUB_HOME содержит «..», очистка не выполняется" >&2; exit 2;; # F-1
+case "$SDLC_HUB_HOME" in
+ *..*) echo "отказ: SDLC_HUB_HOME=$SDLC_HUB_HOME содержит «..», очистка не выполняется" >&2; exit 2;; # F-1
"$WEB/.e2e-hub"|"$WEB/.e2e-hub/"*) ;;
- *) echo "отказ: JIRA_HUB_HOME=$JIRA_HUB_HOME вне $WEB/.e2e-hub, очистка не выполняется" >&2; exit 2;;
+ *) echo "отказ: SDLC_HUB_HOME=$SDLC_HUB_HOME вне $WEB/.e2e-hub, очистка не выполняется" >&2; exit 2;;
esac
if lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
echo "отказ: порт $PORT занят — остановите прежний тестовый хаб (lsof -iTCP:$PORT -sTCP:LISTEN)" >&2; exit 3
fi
-rm -rf "$JIRA_HUB_HOME"
+rm -rf "$SDLC_HUB_HOME"
cd "$ROOT"
-uv run jira-hub init --config tests/fixtures/config.example.json --force >/dev/null
+uv run sdlc-hub init --config tests/fixtures/config.example.json --force >/dev/null
uv run python web/scripts/seed-hub.py web/src/modules/jira/board/__fixtures__/hub-basic.board.json
-exec uv run jira-hub serve --host 127.0.0.1 --port "$PORT"
+exec uv run sdlc-hub serve --host 127.0.0.1 --port "$PORT"
diff --git a/web/vite.config.ts b/web/vite.config.ts
index e87bd03..8021a41 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -2,13 +2,13 @@ import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
// сервер разработки: приложение под /app/, маршруты API — прокси на хаб (по умолчанию тестовый :8791, R-5)
-const hub = process.env.JIRA_HUB_URL ?? "http://127.0.0.1:8791"; // порт держать равным HUB_PORT в playwright.config.ts
+const hub = process.env.SDLC_HUB_URL ?? "http://127.0.0.1:8791"; // порт держать равным HUB_PORT в playwright.config.ts
const API = ["/board", "/issues", "/settings", "/agent", "/refresh", "/batches", "/commands", "/sync-runs"];
export default defineConfig({
base: "/app/",
plugins: [react()],
- build: { outDir: "../src/jira_hub/web/app", emptyOutDir: true },
+ build: { outDir: "../src/sdlc_hub/web/app", emptyOutDir: true },
server: { host: "127.0.0.1", port: 5173, proxy: Object.fromEntries(API.map((p) => [p, { target: hub }])) },
test: {
environment: "jsdom",
--
2.47.2
From e34d61a4835669f6a443b9ecdb7f483b3b0ede47 Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:33:18 +0400
Subject: [PATCH 05/21] =?UTF-8?q?spec(sdlc-rename):=20R-2=20=D0=B2=20desig?=
=?UTF-8?q?n.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 5.5
---
openspec/changes/sdlc-rename/design.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/openspec/changes/sdlc-rename/design.md b/openspec/changes/sdlc-rename/design.md
index 121a2e4..7004073 100644
--- a/openspec/changes/sdlc-rename/design.md
+++ b/openspec/changes/sdlc-rename/design.md
@@ -105,3 +105,4 @@
| 10 | 24.10.2026 | Агенты без релиза, документация — мелкими PR | протокол не меняется | строки документации в `agent.py` со старым именем до следующего релиза |
| 11 | 24.10.2026 | Сначала переименование, потом #98 | #98 перебазируется на новые пути | перебазирование веток |
| 12 | 24.10.2026 | R-1: трейлер коммитов `Co-Authored-By: Claude Sonnet 5.5` вместо `Opus 5.5` из плана | указание системы сессии новее плана | правка трейлера |
+| 13 | 24.10.2026 | R-2: правки web/playwright.config.ts, web/scripts/seed-hub.py и JIRA_HUB_URL→SDLC_HUB_URL (vite.config.ts) сделаны уже в Task 1 | без них e2e, test-hub.sh и seed-скрипт ломаются между задачами | откат трёх файлов |
--
2.47.2
From 08460dc281efffb883d150803c32f2eb2e02b050 Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:45:40 +0400
Subject: [PATCH 06/21] =?UTF-8?q?feat(sdlc-rename):=20MCP-=D1=81=D0=B5?=
=?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80=D1=8B=20sdlc-jira=20=D0=B8=20sdlc-wiki?=
=?UTF-8?q?,=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D0=B2=20sdlc-hub=20?=
=?UTF-8?q?mcp=20=D0=BE=D0=B1=D1=8F=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D0=B5?=
=?UTF-8?q?=D0=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 5.5
---
src/sdlc_hub/core/cli.py | 4 ++--
src/sdlc_hub/core/mcp.py | 17 +++++++++--------
src/sdlc_hub/modules/jira/mcp.py | 2 +-
src/sdlc_hub/modules/wiki/mcp.py | 6 +++---
.../baseline/mcp_wiki_tools.expected.json | 2 +-
tests/test_cli.py | 17 ++++++++---------
tests/test_mcp.py | 10 +++++++---
tests/test_project_fields.py | 9 +++++----
tests/test_wiki_mcp.py | 4 ++--
9 files changed, 38 insertions(+), 33 deletions(-)
diff --git a/src/sdlc_hub/core/cli.py b/src/sdlc_hub/core/cli.py
index 49a3447..761b364 100644
--- a/src/sdlc_hub/core/cli.py
+++ b/src/sdlc_hub/core/cli.py
@@ -17,8 +17,8 @@ def main(argv=None) -> int:
p = sub.add_parser("serve", help="запустить HTTP API")
p.add_argument("--host", default="0.0.0.0")
p.add_argument("--port", type=int)
- p = sub.add_parser("mcp", help="MCP-сервер модуля по stdio (по умолчанию jira)")
- p.add_argument("module", nargs="?", default="jira", help="имя модуля; неизвестное — код 2 (проверка в mcp.run)")
+ p = sub.add_parser("mcp", help="MCP-сервер модуля по stdio ")
+ p.add_argument("module", nargs="?", default=None, help="имя модуля (jira, wiki); без модуля или неизвестный — код 2")
p.add_argument("--project", help="метка проекта, к которому привязана сессия")
for m in registry.modules():
if m.cli_register:
diff --git a/src/sdlc_hub/core/mcp.py b/src/sdlc_hub/core/mcp.py
index 287986e..56bb161 100644
--- a/src/sdlc_hub/core/mcp.py
+++ b/src/sdlc_hub/core/mcp.py
@@ -25,13 +25,10 @@ def unknown_project(label) -> str:
return f"проект {label} не заведён в хабе: project_create или исправьте --project в .mcp.json"
-def mcp_json(label: str, module: str = registry.DEFAULT_AGENT) -> dict:
- """Фрагмент .mcp.json для репозитория проекта: у модуля по умолчанию прежний (сервер jira-hub), у прочих модулей —
- сервер jira-hub-<модуль>."""
- run = ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp"]
- if module == registry.DEFAULT_AGENT:
- return {"mcpServers": {"jira-hub": {"command": "uv", "args": [*run, "--project", label]}}}
- return {"mcpServers": {f"jira-hub-{module}": {"command": "uv", "args": [*run, module, "--project", label]}}}
+def mcp_json(label: str, module: str) -> dict:
+ """Фрагмент .mcp.json для репозитория проекта: сервер sdlc-<модуль>, запуск `sdlc-hub mcp <модуль>`."""
+ run = ["run", "--project", str(HUB_ROOT), "sdlc-hub", "mcp", module, "--project", label]
+ return {"mcpServers": {f"sdlc-{module}": {"command": "uv", "args": run}}}
def client() -> HubClient:
@@ -199,9 +196,13 @@ def register_core_tools(mcp, *, projects_doc: str, columns: list[tuple[str, Call
mcp.tool()(project_create)
-def run(module_name: str, project: str | None = None) -> int:
+def run(module_name: str | None, project: str | None = None) -> int:
"""Запустить MCP-сервер модуля по stdio. Неизвестный модуль: список модулей в stderr, код 2, сервер не стартует."""
global PROJECT
+ if module_name is None:
+ names = ", ".join(x.name for x in registry.modules() if x.mcp_server)
+ print(f"укажите модуль: sdlc-hub mcp <модуль> [--project <метка>]; модули: {names}", file=sys.stderr)
+ return 2
m = registry.module(module_name)
if m is None or m.mcp_server is None:
names = ", ".join(x.name for x in registry.modules() if x.mcp_server)
diff --git a/src/sdlc_hub/modules/jira/mcp.py b/src/sdlc_hub/modules/jira/mcp.py
index d516816..ca51fc1 100644
--- a/src/sdlc_hub/modules/jira/mcp.py
+++ b/src/sdlc_hub/modules/jira/mcp.py
@@ -19,7 +19,7 @@ from .commands import describe, is_temp
POLL_SECONDS = 5
BOTH_FILE_AND_PATH = "у ensure_attachment либо path, либо file"
-mcp = FastMCP("jira-hub")
+mcp = FastMCP("sdlc-jira")
fmt_agent = core.fmt_agent
diff --git a/src/sdlc_hub/modules/wiki/mcp.py b/src/sdlc_hub/modules/wiki/mcp.py
index a99edda..18e5a89 100644
--- a/src/sdlc_hub/modules/wiki/mcp.py
+++ b/src/sdlc_hub/modules/wiki/mcp.py
@@ -1,4 +1,4 @@
-"""MCP-инструменты модуля wiki (stdio, FastMCP, сервер jira-hub-wiki) — инструменты сессии Claude поверх HTTP API хаба
+"""MCP-инструменты модуля wiki (stdio, FastMCP, сервер sdlc-wiki) — инструменты сессии Claude поверх HTTP API хаба
(specs/mcp/spec.md «MCP вики»). Охрана, привязка к проекту и projects/project_create — в core/mcp.py. Markdown страниц и
имена вложений приходят из Eva и считаются недоверенными: страница выдаётся текстом, имя файла сводится к basename."""
import asyncio
@@ -16,7 +16,7 @@ ATTACHMENT_WAIT = 60
SYNC_WAIT = 300
CHUNK = 20000
CANDIDATES = 10
-mcp = FastMCP("jira-hub-wiki")
+mcp = FastMCP("sdlc-wiki")
def _project() -> dict:
@@ -242,7 +242,7 @@ def root_set(root: str) -> str:
@core.guard
def project_create(label: str, name: str, wiki_root: str = "") -> str:
"""Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит
- синхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером jira-hub-wiki."""
+ синхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером sdlc-wiki."""
try:
code = wiki_roots.parse_root(wiki_root)
except ValueError as e:
diff --git a/tests/fixtures/baseline/mcp_wiki_tools.expected.json b/tests/fixtures/baseline/mcp_wiki_tools.expected.json
index 25149f2..8c3c1de 100644
--- a/tests/fixtures/baseline/mcp_wiki_tools.expected.json
+++ b/tests/fixtures/baseline/mcp_wiki_tools.expected.json
@@ -134,7 +134,7 @@
"name": "projects"
},
{
- "description": "Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит\nсинхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером jira-hub-wiki.",
+ "description": "Завести проект (метка, название, необязательно корень вики: код DOC-… или ссылка Eva). С корнем хаб ставит\nсинхронизацию; в ответе — фрагмент .mcp.json для репозитория проекта с сервером sdlc-wiki.",
"inputSchema": {
"properties": {
"label": {
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 686cc8a..51bdcf1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -66,19 +66,18 @@ def test_agent_publish_write_failure(tmp_path, monkeypatch, capsys):
assert sorted(p.name for p in release.module_dir(paths.release_root(), "jira").iterdir()) == ["agent.py", "manifest.json"]
-def test_mcp_default_module(monkeypatch):
+def test_mcp_module_required(monkeypatch, capsys):
from sdlc_hub.core import mcp as core_mcp
from sdlc_hub.modules.jira import mcp as jira_mcp
seen = []
- monkeypatch.setattr(core_mcp, "PROJECT", None)
- monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append((transport, core_mcp.PROJECT)))
- assert cli.main(["mcp", "--project", "AWPCA"]) == 0
+ monkeypatch.setattr(core_mcp, "PROJECT", None) # run() пишет глобал; monkeypatch вернёт прежнее значение
+ monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append(core_mcp.PROJECT))
+ assert cli.main(["mcp", "--project", "AWPCA"]) == 2
+ err = capsys.readouterr().err
+ assert "укажите модуль: sdlc-hub mcp <модуль>" in err and "jira, wiki" in err
+ assert seen == []
assert cli.main(["mcp", "jira", "--project", "AWPCA"]) == 0
- assert seen == [("stdio", "AWPCA"), ("stdio", "AWPCA")]
- # без --project сессия не привязана к проекту
- monkeypatch.setattr(core_mcp, "PROJECT", "STALE")
- assert cli.main(["mcp"]) == 0
- assert seen[-1] == ("stdio", None)
+ assert seen == ["AWPCA"]
def test_mcp_unknown_module(monkeypatch, capsys):
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index 1d17711..35cfaeb 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -503,16 +503,20 @@ def test_project_create(live, monkeypatch): # сце
out = jmcp.project_create("STOCKS_DRRK", "Запасы ДРРК", 5120)
assert "STOCKS_DRRK" in out and re.search(r"пакет[^#]*#\d+", out) and "/mcp" in out
frag = json.loads(out[out.index("{"):])
- assert frag == core_mcp.mcp_json("STOCKS_DRRK")
- assert frag["mcpServers"]["jira-hub"]["args"][-2:] == ["--project", "STOCKS_DRRK"]
+ assert frag == core_mcp.mcp_json("STOCKS_DRRK", "jira")
+ assert frag["mcpServers"]["sdlc-jira"]["args"][-2:] == ["--project", "STOCKS_DRRK"]
assert live["client"].get("/batches")["batches"][0]["author"] == "claude"
assert jmcp.project_create("awpca", "x").startswith("ошибка хаба 409")
assert jmcp.project_create("BAD LABEL", "x").startswith("ошибка хаба 422")
+def test_server_name():
+ assert jmcp.mcp.name == "sdlc-jira"
+
+
def test_hub_root_is_repo():
assert (core_mcp.HUB_ROOT / "pyproject.toml").is_file()
- assert core_mcp.mcp_json("X")["mcpServers"]["jira-hub"]["args"][:3] == ["run", "--project", str(core_mcp.HUB_ROOT)]
+ assert core_mcp.mcp_json("X", "jira")["mcpServers"]["sdlc-jira"]["args"][:3] == ["run", "--project", str(core_mcp.HUB_ROOT)]
def test_fmt_projects():
diff --git a/tests/test_project_fields.py b/tests/test_project_fields.py
index 09d3e2c..b24a563 100644
--- a/tests/test_project_fields.py
+++ b/tests/test_project_fields.py
@@ -150,11 +150,12 @@ def test_field_clashes_with_core(name):
def test_mcp_json_per_module():
- first = core_mcp.mcp_json("AWPCA")["mcpServers"]
- assert list(first) == ["jira-hub"] and first["jira-hub"]["args"][-3:] == ["mcp", "--project", "AWPCA"]
+ first = core_mcp.mcp_json("AWPCA", "jira")["mcpServers"]
+ assert list(first) == ["sdlc-jira"] and first["sdlc-jira"]["args"][-4:] == ["mcp", "jira", "--project", "AWPCA"]
+ assert first["sdlc-jira"]["args"][3] == "sdlc-hub"
other = core_mcp.mcp_json("AWPCA", "wiki")["mcpServers"]
- assert list(other) == ["jira-hub-wiki"]
- assert other["jira-hub-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
+ assert list(other) == ["sdlc-wiki"]
+ assert other["sdlc-wiki"]["args"][-4:] == ["mcp", "wiki", "--project", "AWPCA"]
def test_fmt_projects_columns():
diff --git a/tests/test_wiki_mcp.py b/tests/test_wiki_mcp.py
index d316fd7..5cf1106 100644
--- a/tests/test_wiki_mcp.py
+++ b/tests/test_wiki_mcp.py
@@ -288,7 +288,7 @@ def test_jira_server_unchanged():
def test_run_wiki_module():
m = registry.module("wiki")
- assert m.mcp_server().name == "jira-hub-wiki"
+ assert m.mcp_server().name == "sdlc-wiki"
def test_wiki_tool_names_order():
@@ -302,7 +302,7 @@ def test_projects_table_and_create(hub):
text = str(out)
assert "AWPCA" in text and "DOC-1" in text and "(текущий)" in text
created = wmcp.project_create("n", "Н", "DOC-5")
- assert "jira-hub-wiki" in created
+ assert "sdlc-wiki" in created
assert fake.calls[-1][2]["wiki_root"] == "DOC-5"
--
2.47.2
From d643007c0b0498e7d11c49e99671a66004550ac4 Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:52:08 +0400
Subject: [PATCH 07/21] =?UTF-8?q?feat(sdlc-rename):=20=D0=BE=D1=82=D0=BA?=
=?UTF-8?q?=D0=B0=D0=B7=20CLI=20=D0=BF=D1=80=D0=B8=20=D0=BA=D0=B0=D1=82?=
=?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=B3=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?=
=?UTF-8?q?=D1=85=20=D0=BF=D0=BE=D0=B4=20=D0=BF=D1=80=D0=B5=D0=B6=D0=BD?=
=?UTF-8?q?=D0=B8=D0=BC=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5=D0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 5.5
---
src/sdlc_hub/core/cli.py | 6 ++++++
src/sdlc_hub/core/paths.py | 21 +++++++++++++++++++++
tests/conftest.py | 6 ++++++
tests/test_cli.py | 33 +++++++++++++++++++++++++++++++++
4 files changed, 66 insertions(+)
diff --git a/src/sdlc_hub/core/cli.py b/src/sdlc_hub/core/cli.py
index 761b364..9693ee3 100644
--- a/src/sdlc_hub/core/cli.py
+++ b/src/sdlc_hub/core/cli.py
@@ -31,6 +31,12 @@ def main(argv=None) -> int:
p.add_argument("path")
args = ap.parse_args(argv)
+ if (problem := paths.home_problem()):
+ print(problem, file=sys.stderr)
+ return 2
+ if (warning := paths.home_warning()):
+ print(warning, file=sys.stderr)
+
if args.cmd == "agent":
from . import release
names = [m.name for m in registry.agent_modules()]
diff --git a/src/sdlc_hub/core/paths.py b/src/sdlc_hub/core/paths.py
index 10ea4d1..59fb68f 100644
--- a/src/sdlc_hub/core/paths.py
+++ b/src/sdlc_hub/core/paths.py
@@ -10,6 +10,27 @@ def home() -> Path:
return Path(os.environ.get("SDLC_HUB_HOME", str(Path.home() / ".local" / "share" / "sdlc-hub")))
+def legacy_home() -> Path:
+ """Каталог данных под прежним именем (до переименования в sdlc-hub)."""
+ return Path.home() / ".local" / "share" / "jira-hub"
+
+
+def home_problem() -> "str | None":
+ """Текст отказа, если данные лежат только под прежним именем; при заданном SDLC_HUB_HOME проверки нет."""
+ if os.environ.get("SDLC_HUB_HOME") or home().exists() or not legacy_home().exists():
+ return None
+ return ("каталог данных хаба лежит под прежним именем ~/.local/share/jira-hub; перенесите его: "
+ "bash scripts/cutover.sh (или mv ~/.local/share/jira-hub ~/.local/share/sdlc-hub при остановленном хабе)")
+
+
+def home_warning() -> "str | None":
+ """Предупреждение, если существуют оба каталога (SDLC_HUB_HOME не задан)."""
+ if os.environ.get("SDLC_HUB_HOME") or not (home().exists() and legacy_home().exists()):
+ return None
+ return ("есть и ~/.local/share/sdlc-hub, и ~/.local/share/jira-hub: работаю с новым, "
+ "прежний каталог ~/.local/share/jira-hub можно удалить после проверки")
+
+
def db_path() -> Path:
return home() / "hub.sqlite"
diff --git a/tests/conftest.py b/tests/conftest.py
index 55ca5a5..d3ac10d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,6 +13,12 @@ from sdlc_hub.modules.jira import project_fields
from sdlc_hub.modules.jira import projects as jira_projects
+@pytest.fixture(autouse=True)
+def _isolated_home(monkeypatch, tmp_path_factory):
+ """HOME — во временный каталог: на результат не влияют реальные ~/.local/share/{jira,sdlc}-hub разработчика."""
+ monkeypatch.setenv("HOME", str(tmp_path_factory.mktemp("home")))
+
+
@pytest.fixture
def open_db():
"""Фабрика соединений: open_db() — база в памяти, open_db(path) — файл. Все закрываются после теста."""
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 51bdcf1..168f641 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2,6 +2,8 @@ import json
import os
from pathlib import Path
+import pytest
+
from sdlc_hub.core import api, cli, paths, release, settings
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
@@ -103,3 +105,34 @@ def test_agent_publish_migrates_old_layout(tmp_path, monkeypatch, capsys):
assert cli.main(["agent", "publish", str(new)]) == 1 # v13 < v14: «только вверх» видит прежнюю публикацию
assert not (rel / "agent.py").exists() and not (rel / "manifest.json").exists()
assert json.loads((rel / "jira" / "manifest.json").read_text(encoding="utf-8"))["version"] == 14
+
+
+def _homes(monkeypatch, tmp_path, legacy=True, new=False):
+ monkeypatch.delenv("SDLC_HUB_HOME", raising=False)
+ monkeypatch.setenv("HOME", str(tmp_path))
+ share = tmp_path / ".local" / "share"
+ if legacy: (share / "jira-hub").mkdir(parents=True)
+ if new: (share / "sdlc-hub").mkdir(parents=True)
+ return share
+
+
+@pytest.mark.parametrize("argv", [["serve"], ["tokens"], ["mcp", "jira"], ["agent", "publish", "x.py"]])
+def test_legacy_home_refused(monkeypatch, tmp_path, capsys, argv):
+ share = _homes(monkeypatch, tmp_path)
+ assert cli.main(argv) == 2
+ err = capsys.readouterr().err
+ assert "~/.local/share/jira-hub" in err and "scripts/cutover.sh" in err
+ assert not (share / "sdlc-hub").exists()
+ assert sorted(p.name for p in (share / "jira-hub").iterdir()) == []
+
+
+def test_both_homes_warn(monkeypatch, tmp_path, capsys):
+ _homes(monkeypatch, tmp_path, new=True)
+ cli.main(["tokens"])
+ assert "прежний каталог ~/.local/share/jira-hub можно удалить" in capsys.readouterr().err
+
+
+def test_env_home_skips_legacy_check(monkeypatch, tmp_path, capsys):
+ _homes(monkeypatch, tmp_path)
+ monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "x"))
+ assert cli.main(["tokens"]) == 0
--
2.47.2
From 528c579aa8c1c3a5a08e49c1ec42d5c22ac935b9 Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:55:11 +0400
Subject: [PATCH 08/21] =?UTF-8?q?spec(sdlc-rename):=20R-3=20=D0=B2=20desig?=
=?UTF-8?q?n.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 5.5
---
openspec/changes/sdlc-rename/design.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/openspec/changes/sdlc-rename/design.md b/openspec/changes/sdlc-rename/design.md
index 7004073..ec3d724 100644
--- a/openspec/changes/sdlc-rename/design.md
+++ b/openspec/changes/sdlc-rename/design.md
@@ -106,3 +106,4 @@
| 11 | 24.10.2026 | Сначала переименование, потом #98 | #98 перебазируется на новые пути | перебазирование веток |
| 12 | 24.10.2026 | R-1: трейлер коммитов `Co-Authored-By: Claude Sonnet 5.5` вместо `Opus 5.5` из плана | указание системы сессии новее плана | правка трейлера |
| 13 | 24.10.2026 | R-2: правки web/playwright.config.ts, web/scripts/seed-hub.py и JIRA_HUB_URL→SDLC_HUB_URL (vite.config.ts) сделаны уже в Task 1 | без них e2e, test-hub.sh и seed-скрипт ломаются между задачами | откат трёх файлов |
+| 14 | 24.10.2026 | R-3: autouse-фикстура `_isolated_home` в tests/conftest.py подменяет HOME временным каталогом | реальный ~/.local/share/jira-hub на машине владельца иначе влиял бы на набор тестов | удаление фикстуры |
--
2.47.2
From ef9ec9f46d8952e56774c90b1030ab0730cc7fca Mon Sep 17 00:00:00 2001
From: Andrey Epifancev
Date: Sat, 24 Oct 2026 11:56:58 +0400
Subject: [PATCH 09/21] =?UTF-8?q?feat(sdlc-rename):=20=D0=B1=D1=80=D0=B5?=
=?UTF-8?q?=D0=BD=D0=B4=20SDLC=20Hub=20=D0=B8=20=D0=BA=D0=BB=D1=8E=D1=87?=
=?UTF-8?q?=D0=B8=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8=D1=89=D0=B0?=
=?UTF-8?q?=20sdlc-hub-*=20=D1=81=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE?=
=?UTF-8?q?=D1=81=D0=BE=D0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 5.5
---
web/e2e/screens.spec.ts | 2 +-
web/index.html | 2 +-
web/package-lock.json | 4 +--
web/package.json | 2 +-
web/src/api/legacyStorage.test.ts | 31 +++++++++++++++++++
web/src/api/legacyStorage.ts | 18 +++++++++++
web/src/auth/LoginScreen.test.tsx | 6 ++--
web/src/auth/LoginScreen.tsx | 8 ++---
web/src/auth/token.test.ts | 6 ++--
web/src/auth/token.ts | 8 +++--
web/src/modules/jira/board/BoardPage.test.tsx | 2 +-
web/src/modules/jira/board/BoardPage.tsx | 2 +-
web/src/modules/jira/board/legendPref.ts | 6 ++--
web/src/modules/jira/board/status.ts | 2 +-
web/src/modules/jira/board/useBoardSource.ts | 2 +-
.../modules/jira/issues/IssueCard.test.tsx | 2 +-
web/src/modules/jira/issues/IssueFilters.tsx | 2 +-
.../modules/jira/issues/IssueModal.test.tsx | 2 +-
.../modules/jira/issues/IssuesPage.test.tsx | 6 ++--
web/src/modules/jira/issues/commentsPref.ts | 5 +--
.../modules/jira/refresh/RefreshProvider.tsx | 2 +-
web/src/shell/Sidebar.test.tsx | 12 +++----
web/src/shell/Sidebar.tsx | 4 +--
web/src/shell/sidebarPref.test.ts | 13 ++++++++
web/src/shell/sidebarPref.ts | 6 ++--
25 files changed, 112 insertions(+), 43 deletions(-)
create mode 100644 web/src/api/legacyStorage.test.ts
create mode 100644 web/src/api/legacyStorage.ts
create mode 100644 web/src/shell/sidebarPref.test.ts
diff --git a/web/e2e/screens.spec.ts b/web/e2e/screens.spec.ts
index a3aa7f3..f76c21c 100644
--- a/web/e2e/screens.spec.ts
+++ b/web/e2e/screens.spec.ts
@@ -89,7 +89,7 @@ for (const theme of ["light", "dark"] as const) {
test("board-collapsed", async ({ page }) => {
test.skip(w < 640, "свёртка — только на широком экране");
- await page.addInitScript(() => { localStorage.setItem("jira-hub-sidebar", "collapsed"); });
+ await page.addInitScript(() => { localStorage.setItem("sdlc-hub-sidebar", "collapsed"); });
await login(page);
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
await settle(page);
diff --git a/web/index.html b/web/index.html
index 5f2db89..9168549 100644
--- a/web/index.html
+++ b/web/index.html
@@ -3,7 +3,7 @@
- Хаб Jira
+ SDLC Hub
diff --git a/web/package-lock.json b/web/package-lock.json
index aed0e23..2f43358 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,10 +1,10 @@
{
- "name": "jira-hub-web",
+ "name": "sdlc-hub-web",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "jira-hub-web",
+ "name": "sdlc-hub-web",
"dependencies": {
"@fontsource/ibm-plex-mono": "5.3.0",
"@fontsource/mulish": "5.3.0",
diff --git a/web/package.json b/web/package.json
index 45396bb..503e2a1 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,5 +1,5 @@
{
- "name": "jira-hub-web",
+ "name": "sdlc-hub-web",
"private": true,
"type": "module",
"scripts": {
diff --git a/web/src/api/legacyStorage.test.ts b/web/src/api/legacyStorage.test.ts
new file mode 100644
index 0000000..84efa64
--- /dev/null
+++ b/web/src/api/legacyStorage.test.ts
@@ -0,0 +1,31 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { readMigrated } from "./legacyStorage";
+
+describe("readMigrated", () => {
+ beforeEach(() => localStorage.clear());
+ afterEach(() => vi.restoreAllMocks());
+
+ it("переносит старый ключ jira-hub-* в sdlc-hub-*", () => {
+ localStorage.setItem("jira-hub-token", "abc");
+ expect(readMigrated("sdlc-hub-token")).toBe("abc");
+ expect(localStorage.getItem("sdlc-hub-token")).toBe("abc");
+ expect(localStorage.getItem("jira-hub-token")).toBeNull();
+ });
+
+ it("при наличии обоих возвращает новое и не трогает старое", () => {
+ localStorage.setItem("jira-hub-token", "old");
+ localStorage.setItem("sdlc-hub-token", "new");
+ expect(readMigrated("sdlc-hub-token")).toBe("new");
+ expect(localStorage.getItem("jira-hub-token")).toBe("old");
+ });
+
+ it("без обоих ключей возвращает null и ничего не пишет", () => {
+ expect(readMigrated("sdlc-hub-token")).toBeNull();
+ expect(localStorage.length).toBe(0);
+ });
+
+ it("исключение хранилища даёт null", () => {
+ vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { throw new Error("closed"); });
+ expect(readMigrated("sdlc-hub-token")).toBeNull();
+ });
+});
diff --git a/web/src/api/legacyStorage.ts b/web/src/api/legacyStorage.ts
new file mode 100644
index 0000000..76d19ae
--- /dev/null
+++ b/web/src/api/legacyStorage.ts
@@ -0,0 +1,18 @@
+// разовый перенос ключей localStorage после переименования jira-hub -> sdlc-hub: читает новый ключ,
+// а если его нет — старый "jira-hub-*", переписывая значение под новым именем. Ошибки хранилища глотаем.
+const NEW = "sdlc-hub-";
+const OLD = "jira-hub-";
+
+export function readMigrated(key: string): string | null {
+ try {
+ const cur = localStorage.getItem(key);
+ if (cur !== null) return cur;
+ if (!key.startsWith(NEW)) return null;
+ const oldKey = OLD + key.slice(NEW.length);
+ const v = localStorage.getItem(oldKey);
+ if (v === null) return null;
+ localStorage.setItem(key, v);
+ localStorage.removeItem(oldKey);
+ return v;
+ } catch { return null; }
+}
diff --git a/web/src/auth/LoginScreen.test.tsx b/web/src/auth/LoginScreen.test.tsx
index 863ccd6..9e7a954 100644
--- a/web/src/auth/LoginScreen.test.tsx
+++ b/web/src/auth/LoginScreen.test.tsx
@@ -18,7 +18,7 @@ describe("вход", () => {
renderWithProviders(app);
expect(screen.getByRole("heading", { name: "Вход в хаб" })).toBeInTheDocument();
expect(screen.queryByText("страница приложения")).not.toBeInTheDocument();
- expect(screen.getByText(/uv run jira-hub tokens/)).toBeInTheDocument();
+ expect(screen.getByText(/uv run sdlc-hub tokens/)).toBeInTheDocument();
expect(screen.getByLabelText("Клиентский токен хаба")).toHaveAttribute("type", "password");
});
@@ -28,7 +28,7 @@ describe("вход", () => {
await userEvent.type(screen.getByLabelText("Клиентский токен хаба"), "good-token");
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
expect(await screen.findByText("страница приложения")).toBeInTheDocument();
- expect(localStorage.getItem("jira-hub-token")).toBe("good-token");
+ expect(localStorage.getItem("sdlc-hub-token")).toBe("good-token");
expect(fetch.mock.calls[0][0]).toBe("/settings");
});
@@ -40,7 +40,7 @@ describe("вход", () => {
await userEvent.click(screen.getByRole("button", { name: "Войти" }));
expect(await screen.findByRole("alert")).toHaveTextContent(/Токен не подошёл/);
expect(input).toHaveValue("wrong");
- expect(localStorage.getItem("jira-hub-token")).toBeNull();
+ expect(localStorage.getItem("sdlc-hub-token")).toBeNull();
});
it("кириллица или пробел — «недопустимые символы», запроса нет", async () => {
diff --git a/web/src/auth/LoginScreen.tsx b/web/src/auth/LoginScreen.tsx
index eb1c29a..bf85df8 100644
--- a/web/src/auth/LoginScreen.tsx
+++ b/web/src/auth/LoginScreen.tsx
@@ -6,9 +6,9 @@ import { setToken, TOKEN_RE } from "./token.ts";
import { useAuth } from "./useAuth.ts";
import "./login.css";
-const BAD_TOKEN = "Токен не подошёл. Проверьте его по uv run jira-hub tokens: токен могли пересоздать.";
+const BAD_TOKEN = "Токен не подошёл. Проверьте его по uv run sdlc-hub tokens: токен могли пересоздать.";
const BAD_CHARS = "В токене недопустимые символы: скопируйте строку после «клиентский токен:» целиком, без пробелов и кириллицы.";
-const HUB_DOWN = "Хаб не отвечает: проверьте, что jira-hub запущен, и попробуйте снова.";
+const HUB_DOWN = "Хаб не отвечает: проверьте, что sdlc-hub запущен, и попробуйте снова.";
export function LoginScreen() {
const { reason } = useAuth();
@@ -40,8 +40,8 @@ export function LoginScreen() {
Вход в хаб
-
Приложению нужен клиентский токен хаба. Его показывает команда uv run jira-hub tokens в каталоге
- jira-hub на этом Mac. Токен сохранится в этом браузере.
+
Приложению нужен клиентский токен хаба. Его показывает команда uv run sdlc-hub tokens в каталоге
+ sdlc-hub на этом Mac. Токен сохранится в этом браузере.