24 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Процесс и доступ

sdlc-hub serve SHALL запускать один процесс: HTTP на 0.0.0.0:<port> (по умолчанию 8790), база и настройки в ~/.local/share/sdlc-hub/ (переопределяется SDLC_HUB_HOME). Все маршруты API SHALL требовать Authorization: Bearer <token>: токен агента модуля открывает только /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/<id>.json: {id, method, path, body, authorization, deadline}, где deadline — момент UTC (ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать;
  • реле SHALL брать запрос переименованием в work/<id>.json и обрабатывать каждый взятый запрос в отдельном потоке;
  • запрос с истёкшим deadline SHALL выбрасываться без ответа; POST /agent/claim SHALL пересылаться, только если до deadline осталось не меньше 35 с, иначе выбрасываться без ответа;
  • остальные запросы реле SHALL пересылать в свой HTTP на <host>:<port> процесса serve (127.0.0.1, если serve слушает 0.0.0.0) тем же методом, путём и телом, с заголовком Authorization из файла, и записывать ответ resp/<id>.json = {id, status, body} (тело — строка ответа HTTP) через временный файл с именем на точку и переименование; затем удалять work/<id>.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 <METHOD> <path> <status> <секунды>. 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 записываться в <SDLC_HUB_HOME>/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 и совпадает с origin/main (владелец сделал git pull --ff-only прямо перед запуском: скрипт приходит этим pull; неотслеживаемая прежняя сборка src/jira_hub/ без tracked-файлов не считается грязью); в Gitea доступен aep/sdlc-hub (владелец переименовал репозиторий; адрес выводится из origin заменой /aep/jira-hub на /aep/sdlc-hub, протокол сохраняется, CUTOVER_REMOTE переопределяет); есть uv, make, node, npm, claude, launchctl; ~/.local/share/sdlc-hub/ и jira-bridge/sdlc-hub не существуют;
  2. остановить launchd ru.aepif.jira-hub, дождаться закрытия порта и переименовать его plist в *.pre-sdlc;
  3. перенести каталог данных ~/.local/share/jira-hub → ~/.local/share/sdlc-hub;
  4. перенести checkout jira-bridge/jira-hub → jira-bridge/sdlc-hub, удалить неотслеживаемый src/jira_hub, git worktree repair, origin → aep/sdlc-hub, пересоздать .venv (uv sync), make web;
  5. поставить и запустить launchd ru.aepif.sdlc-hub (uv run --project <checkout> sdlc-hub serve, лог ~/Library/Logs/sdlc-hub.log) и дождаться ответа хаба на порту из настроек;
  6. в проектах-потребителях (по умолчанию ~/projects/awp-ca, ~/projects/stocks_drrk и каждый ~/projects/*/.mcp.json, где есть jira-hub, например sfm_foxpro; сухой прогон печатает полный список): в .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 и каталоги не тронуты