sdlc-hub/docs/superpowers/plans/2026-10-24-sdlc-rename.md
Andrey Epifancev 10d2eaa513 plan(sdlc-rename): план реализации переименования
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:21:09 +04:00

216 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Переименование 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 <noreply@anthropic.com>`.
- Живой хаб (`: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` (`<title>SDLC Hub</title>`), `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 <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` для ручной проверки.
- Файлы проектов для правки: `<project>/.mcp.json` (`rewrite_mcp_json`), `<project>/.claude/settings*.json`, `<project>/CLAUDE.md`, `<project>/.claude/skills/**/*` текстовые (`rewrite_text`).
- Шаг 5 ждёт TCP-подключения к `127.0.0.1:<port>` до 30 с. Шаг 7: `claude mcp remove -s user jira-hub`, затем `claude mcp add -s user sdlc-jira -- uv run --project <checkout> 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).