Compare commits

..
2 Commits
Author SHA1 Message Date
oqyude 7aec35b8df metaagent update to v1.1.0 2026-07-22 01:27:30 +03:00
oqyude 685d3262d8 metaagent updated 2026-07-22 01:27:17 +03:00
50 changed files with 839 additions and 3570 deletions
+12 -9
View File
@@ -1,8 +1,8 @@
{
"metaagent_version": "1.1.0",
"session_id": "metaagent-003",
"session_id": "metaagent-002",
"target_repo": "S:\\Git\\nifodea",
"goal": "i18n (ru/en) — инфраструктура, обёртка строк, контроль переводов",
"goal": "Обновление metaagent-артефактов до v1.0.0, валидация существующего кода и окружения",
"project_type": "existing",
"config": {
"depth": 4,
@@ -18,14 +18,17 @@
"red_team": "skipped",
"decomposition": "completed",
"environment": "completed",
"handoff": "pending"
"handoff": "completed"
},
"tasks": [
{ "id": "T9", "title": "i18n инфраструктура (cli/i18n.py)", "status": "completed", "depends_on": [], "acceptance_criteria": ["Translator, t(), setup_i18n(), set_lang()", "ru default, en fallback"] },
{ "id": "T10", "title": "Обёртка CLI-строк в t()", "status": "completed", "depends_on": ["T9"], "acceptance_criteria": ["main.py + config.py через t()", "ruff check проходит"] },
{ "id": "T11", "title": "AI-промпты через i18n", "status": "completed", "depends_on": ["T9"], "acceptance_criteria": ["prompts.py использует t()"] },
{ "id": "T12", "title": "Тесты i18n", "status": "completed", "depends_on": ["T9"], "acceptance_criteria": ["pytest проходит"] },
{ "id": "T13", "title": "Аудит и контроль актуальности переводов", "status": "pending", "depends_on": ["T9"], "acceptance_criteria": ["en-словарь синхронизирован с ru"] }
{ "id": "T1", "title": "Инициализация проекта и зависимостей", "status": "completed", "depends_on": [], "acceptance_criteria": ["pyproject.toml создан", "Все __init__.py созданы", "ruff проходит", "pytest запускается"] },
{ "id": "T2", "title": "Модель данных (dataclass + JSON)", "status": "completed", "depends_on": ["T1"], "acceptance_criteria": ["Все сущности dataclass", "FinancialModel save/load JSON"] },
{ "id": "T3", "title": "Forecast Engine", "status": "completed", "depends_on": ["T2"], "acceptance_criteria": ["forecast_cashflow работает", "recurring проецируются", "активы/обязательства учтены"] },
{ "id": "T4", "title": "Scenario Analysis", "status": "completed", "depends_on": ["T3"], "acceptance_criteria": ["3 сценария", "what-if модификация", "сравнение сценариев"] },
{ "id": "T5", "title": "Excel Sync", "status": "completed", "depends_on": ["T2"], "acceptance_criteria": ["импорт из Excel", "экспорт в Excel", "ошибки невалидного формата"] },
{ "id": "T6", "title": "CLI (Typer)", "status": "completed", "depends_on": ["T2","T3","T4","T5","T7"], "acceptance_criteria": ["init/forecast/analyze/import/export/scenario/whatif/compare команды"] },
{ "id": "T7", "title": "AI Assistant", "status": "completed", "depends_on": ["T3"], "acceptance_criteria": ["промпты с моделью и прогнозом", "заглушка ответа"] },
{ "id": "T8", "title": "Тесты", "status": "completed", "depends_on": ["T2","T3","T4","T5","T6","T7"], "acceptance_criteria": ["pytest проходит", "покрытие всех модулей"] }
],
"last_updated": "2026-07-22T18:00:00Z"
"last_updated": "2026-07-22T12:00:00Z"
}
+9 -11
View File
@@ -1,36 +1,34 @@
# BOUNDARIES — Рамки и границы
Что агенту **разрешено**, **запрещено** и в каких случаях **нужно остановиться**.
Что мета-агенту **разрешено**, **запрещено** и в каких случаях **нужно остановиться**.
## Разрешено
| Действие | Примечание |
|---|---|
| Читать любые файлы в целевом репозитории | Все файлы, включая .git, конфиги, историю |
| Создавать/изменять файлы в `.agent/` | Директория метаданных проекта (rules, decisions, tasks, context, archive, requests, roadmap) |
| Создавать `.temp/` в корне проекта | Для временных файлов агента (всегда на одном уровне с `.agent/`) |
| Писать production-код | В фазе EXECUTION, по задачам из manifest.json |
| Рефакторить существующий код | Только если это часть задачи в manifest.json |
| Делать коммиты | По завершении задачи, перед созданием request |
| Создавать/дополнять `.gitignore` | Только для добавления `.temp/` |
| Создавать/изменять файлы в `.agent/` | Единственная директория для артефактов |
| Устанавливать/обновлять зависимости | Только через штатный пакетный менеджер проекта |
| Изменять конфигурационные файлы | Только если это необходимо для сборки/тестов (например, добавить requirements.txt) |
| Запускать сборку и тесты | Для верификации окружения и проверки request-ов |
| Запускать сборку и тесты | Для верификации окружения |
| Читать документацию, issue, PRs | Для понимания контекста |
| Запрашивать уточнения у пользователя | Если не хватает информации для декомпозиции |
| Копировать исходники MetaAgent в `.agent/src/` целевого проекта | Только на фазе INIT, без перезаписи существующих файлов |
| Создавать/обновлять `AGENTS.md` в корне целевого проекта | Только если файла не существует |
| **Обязательно:** читать `.agent/rules/project-rules.md` перед каждой фазой | Исполнение правил пользователя — приоритет выше стандартных протоколов |
| Перемещать завершённые артефакты в `.agent/archive/` | На фазах METASTATE и HANDOFF, только для completed/failed артефактов |
| **Обязательно:** после выполнения задачи создавать request в `.agent/requests/active/` | Request — единица результата, основа для METASTATE |
| Перемещать завершённые артефакты в `.agent/archive/` | Только на фазе HANDOFF, только для completed/failed артефактов |
## Запрещено
| Действие | Почему |
|---|---|
| Писать production-код | Это работа исполнительного агента |
| Рефакторить существующий код | Мета-агент не меняет логику |
| Удалять файлы | Если файл мешает — нужно сообщить пользователю |
| Коммитить в main/master | Коммиты делает исполнительный агент по задачам |
| Менять удалённые настройки CI/CD | Если CI сломан — сообщить пользователю |
| Модифицировать код, не связанный с задачей | Только то, что нужно в рамках задачи из manifest.json |
| Пул-реквесты | Исполнительный агент создаёт PR после выполнения задач |
| Модифицировать код, не связанный с задачей | Только то, что нужно для окружения |
## Когда остановиться
+207 -251
View File
@@ -1,40 +1,23 @@
# META_AGENT_GUIDE — Главная инструкция v2.1
# META_AGENT_GUIDE — Главная инструкция
## Жизненный цикл сессии
```
.agent/metaagent-request.md
┌─────────────────────────────────────────────────────┐
PROJECT LOOP (однократно)
│ │
│ INIT → ANALYSE → ROADMAP → DESIGN → DECOMPOSITION │
│ │
│ Выход: .agent/tasks/manifest.json │
└──────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ WORK LOOP (циклически) │
│ │
│ EXECUTION → (request) → METASTATE (по команде) │
│ │
│ Цикл повторяется: беру задачу → делаю → │
│ создаю request → накопилось → METASTATE │
└──────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ HANDOFF (завершение) │
└─────────────────────────────────────────────────────┘
.agent/metaagent-request.md
INIT → ANALYSE → [DESIGN] → [RED_TEAM] → DECOMPOSITION → SETUP → (CHECKPOINT)* → HANDOFF → EXIT
▼ ▼
ADR (опц.) Invariant Tasks (опц.)
Alt.Arch (опц.)
Risk Register (опц.)
```
Фазы выполняются **строго последовательно** внутри PROJECT LOOP.
WORK LOOP может повторяться многократно.
HANDOFF — легковесное завершение.
Фазы выполняются **строго последовательно**. Фаза DESIGN — только если project_type = greenfield/scaffold.
Фаза RED_TEAM — только если config.red_team = yes.
Все артефакты размещаются в `.agent/` целевого репозитория.
Все артефакты размещаются в `.agent/` целевого репозитория (с layer-структурой или плоские, в зависимости от config).
---
@@ -49,12 +32,12 @@ HANDOFF — легковесное завершение.
| Уровень | Название | Что выполняется |
|---|---|---|
| 1-2 | Scaffold | INIT → ANALYSIS → SETUP (только структура, без реализации) |
| 3-4 | Light | + ROADMAP, DESIGN (без ADR/альтернатив), DECOMPOSITION — **(default)** |
| 3-4 | Light | + DESIGN (без ADR/альтернатив), DECOMPOSITION (без инвариантов), HANDOFF **(default)** |
| 5-6 | Standard | полный цикл с базовым DESIGN и DECOMPOSITION |
| 7-8 | Deep | + ADR, Alternative Architecture, Risk Register, Invariant Tests |
| 9-10 | Maximum | + Red Team Review, Executable Invariants для всех ADR |
### Функции
### Функции (таблица вкл/выкл)
| Функция | Фаза | Глубина | Описание |
|---|---|---|---|
@@ -63,190 +46,180 @@ HANDOFF — легковесное завершение.
| red_team | DESIGN (после) | >=9 | Red Team Review — попытка разрушить архитектуру |
| risk_register | DESIGN | >=7 | Явный реестр допущений |
| invariant_tests | DECOMPOSITION | >=7 | Задачи-инварианты для каждого ADR |
| layer_structure | HANDOFF | любая | Организация .agent/ по слоям (layer-0..3) |
---
## Фаза 0: INIT
**Вход:** целевой репозиторий + опционально `.agent/metaagent-request.md`.
**Протокол:** `PROTOCOLS/00_CONFIG.md`
**Действия:**
- Прочитать `VERSION` — текущая версия MetaAgent
- Склонировать/открыть целевой репозиторий
- Создать директорию `.agent/` в корне целевого репозитория (если нет)
- Создать `.temp/` в корне целевого репозитория (если нет), добавить в `.gitignore`
- Установить исходники MetaAgent в `.agent/src/`
- Создать структуру `.agent/`: `rules/`, `decisions/`, `tasks/` (с `backlog/`), `context/`, `requests/` (с `active/`, `archive/`), `roadmap/` (с `archive/`), `archive/` (с `tasks/`, `decisions/`, `checkpoints/`)
- Создать/обновить `AGENTS.md` в корне
- Прочитать/создать `.agent/metaagent-request.md` (интервью или default)
- Проверить версию, инициализировать `checkpoints.json`
- **Установить исходники MetaAgent в `.agent/src/`:**
- Скопировать `META_AGENT_GUIDE.md`, `BOUNDARIES.md`, `WORKFLOW.md`, `VERSION` в `.agent/src/`
- Скопировать `PROTOCOLS/` и `TEMPLATES/` в `.agent/src/`
- Скопировать `install.sh` и `install.ps1` в `.agent/src/` (для возможности обновления)
- Если файлы уже существуют — пропустить (не перезаписывать)
- **Создать `.agent/rules/`** — директорию для пользовательских правил
- Если `.agent/rules/project-rules.md` не существует — создать из шаблона `.agent/src/TEMPLATES/project-rules.md`
- **Создать/обновить `AGENTS.md` в корне целевого репозитория** (если нет — создать, если есть — не трогать)
- Прочитать `PROTOCOLS/00_CONFIG.md`
- Выполнить 00_CONFIG:
- Если `.agent/metaagent-request.md` существует — прочитать config из него
- Если нет — провести интервью с пользователем (или принять `default`)
- Валидировать config относительно depth
- Если не было файла — создать `.agent/metaagent-request.md` с пометкой Auto-generated
- **Проверить версию:** если `.agent/checkpoints.json` существует → выполнить `PROTOCOLS/00_MIGRATE.md` (сравнить metaagent_version, применить миграцию при необходимости)
- Прочитать `PROTOCOLS/01_ANALYSIS.md`
- Инициализировать `.agent/checkpoints.json` с `metaagent_version` (если не существовал)
**Выход:** готовая `.agent/` + checkpoints.json.
```json
{
"metaagent_version": "1.1.0",
"session_id": "<uuid>",
"target_repo": "<path>",
"goal": "<цель от пользователя>",
"project_type": "pending",
"config": {
"depth": 4,
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
},
"phases": {
"analysis": "pending",
"design": "pending",
"red_team": "pending",
"decomposition": "pending",
"environment": "pending",
"handoff": "pending"
},
"tasks": [],
"last_updated": "<timestamp>"
}
```
**Выход:** готовая `.agent/` + checkpoints.json с metaagent_version и config.
---
## Фаза 1: ANALYSIS
## Фаза 1: ANALYSE
**Вход:** целевой репозиторий, `.agent/metaagent-request.md` (или auto-generated), checkpoints.json (analysis: pending, config: from INIT).
**Протокол:** `PROTOCOLS/01_ANALYSIS.md`
**Действия:**
- Прочитать `.agent/rules/project-rules.md`
- Прочитать config из checkpoints.json
- Выполнить анализ репозитория:
- Определить тип проекта (existing / greenfield / scaffold)
- Зафиксировать стек, архитектуру, конвенции, тесты
- Для greenfield — извлечь требования из README
- Создать начальный `.agent/context/project-state.md` — слепок проекта
- Записать `.agent/context/analysis-report.md`
- Обновить checkpoints.json
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Прочитать config из checkpoints.json (уже получен на INIT через 00_CONFIG)
- Если config отсутствует — применить default config (depth=4) как fallback
- Выполнить анализ репозитория по протоколу (определяет тип проекта)
- Записать результат в `.agent/analysis-report.md`
- Обновить checkpoints.json: `phases.analysis = "completed"`, `project_type = "existing" | "greenfield" | "scaffold"`
**Выход:** `.agent/analysis-report.md`
**Ветвление:**
- `project_type = "greenfield"` или `"scaffold"` → далее ROADMAP → DESIGN
- `project_type = "existing"`далее ROADMAP (DESIGN пропускается)
**Выход:** `.agent/context/analysis-report.md`, `.agent/context/project-state.md`
- `project_type = "greenfield"` или `"scaffold"` → далее фаза DESIGN
- `project_type = "existing"` → DESIGN пропускается, сразу DECOMPOSITION
---
## Фаза 2: ROADMAP
## Фаза 2: DESIGN (условная)
**Протокол:** `PROTOCOLS/02_ROADMAP.md`
**Действия:**
- Прочитать `.agent/rules/project-rules.md`
- Сканировать FUTURE/ — долгосрочные планы
- Сканировать `.agent/decisions/index.json` — ADR, требующие реализации
- Учесть пользовательские запросы и выявленные улучшения
- Приоритизировать все источники (P0-P3)
- Создать `.agent/roadmap/sources.md`
**Выход:** `.agent/roadmap/sources.md`
---
## Фаза 3: DESIGN (условная)
**Вход:** analysis-report.md, checkpoints.json (analysis: completed, project_type: greenfield/scaffold).
**Протокол:** `PROTOCOLS/02_DESIGN.md`
Выполняется только для greenfield/scaffold.
**Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Спроектировать архитектуру, модули, данные, интерфейсы
- Если config.design.adr: создать ADR → `.agent/decisions/`
- Если config.risk_register: создать `.agent/context/risk-register.md`
- Записать `.agent/context/design-report.md`
- Если config.design.alternative_arch: описать альтернативную архитектуру
- Если config.design.adr: создать ADR для каждого ключевого решения → `.agent/layer-1/adr/`
- Если config.risk_register: создать `.agent/layer-1/risk-register.md`
- Записать результат в `.agent/design-report.md`
- Обновить checkpoints.json: `phases.design = "completed"`
**Выход:** `.agent/context/design-report.md`, опционально ADR, risk-register
**Ветвление:**
- Если config.red_team = yes → следующая фаза RED_TEAM
- Иначе → сразу DECOMPOSITION
**Выход:** `.agent/design-report.md`, опционально `.agent/layer-1/adr/*.md`, `.agent/layer-1/risk-register.md`
---
## Фаза 3b: RED_TEAM (опциональная)
## Фаза 2b: RED_TEAM (опциональная)
**Вход:** design-report.md, ADR (опционально), checkpoints.json (design: completed).
**Протокол:** `PROTOCOLS/02b_REDTEAM.md`
Только если config.red_team = yes (depth >= 9).
**Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Выполнить Red Team Review по протоколу
- Записать результат в `.agent/layer-1/red-team-report.md`
- Дополнить risk-register.md (если существует)
- Если найдены критические проблемы — исправить design-report
- Обновить checkpoints.json: `phases.red_team = "completed"`
**Выход:** `.agent/context/red-team-report.md`
**Выход:** `.agent/layer-1/red-team-report.md`
---
## Фаза 4: DECOMPOSITION
## Фаза 3: DECOMPOSITION
**Вход:** analysis-report.md + design-report.md (опционально) + ADR (опционально) + checkpoints.json.
**Протокол:** `PROTOCOLS/03_DECOMPOSITION.md`
**Действия:**
- Прочитать `.agent/rules/project-rules.md`
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Разбить цель (и дизайн) на атомарные задачи
- Каждой задаче присвоить `origin` (источник: roadmap, ADR, user, agent)
- Если есть `.agent/roadmap/sources.md` — сверить приоритеты
- Если config.invariant_tests: создать задачи-инварианты для ADR
- Записать `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md`
- Если config.decomposition.invariant_tests: создать задачи-инварианты для каждого ADR
- Записать манифест в `.agent/task-manifest.json` и `.agent/task-manifest.md`
- Обновить checkpoints.json: `phases.decomposition = "completed"`, заполнить `tasks`
**Выход:** `.agent/tasks/manifest.json` + `.agent/tasks/manifest.md`
**Выход:** `.agent/task-manifest.json`, `.agent/task-manifest.md`
---
## Фаза 5: EXECUTION (циклическая)
## Фаза 4: SETUP
**Протокол:** `PROTOCOLS/04_EXECUTION.md`
**Вход:** analysis-report.md, design-report.md (опционально), task-manifest.json, checkpoints.json (decomposition: completed).
**Протокол:** `PROTOCOLS/04_ENVIRONMENT_SETUP.md`
**Действия:**
1. Выбрать следующую задачу из manifest.json (pending, все depends_on выполнены)
2. Отметить `in_progress`
3. Реализовать (код, тесты, конфиги)
4. Верифицировать (тесты, LSP diagnostics)
5. Закоммитить
6. Создать request в `.agent/requests/active/req-{id}.json`
7. Отметить `completed` в manifest.json
8. Повторить, пока есть задачи
9. Если задач нет — ожидать команду пользователя
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Выполнить настройку окружения по протоколу (ветка A для existing, ветка B для greenfield)
- Записать результат проверки в `.agent/baseline-test-report.log` и `.agent/setup-report.log`
- Обновить checkpoints.json: `phases.environment = "completed"`
**Request — единица результата:**
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "Human-readable title",
"status": "ready_for_review",
"changes": {
"summary": "Суть изменений",
"commits": ["abc1234"],
"files_changed": ["path/to/file.py"]
},
"verification": {
"tests_passed": "24/24",
"lsp_clean": true
},
"fulfills_ac": ["AC1"]
}
```
**Выход:** выполненные задачи в manifest + request-ы в `.agent/requests/active/`
**Выход:** рабочее окружение + `.agent/baseline-test-report.log`
---
## Фаза 6: METASTATE (по команде пользователя)
## Фаза 5: CHECKPOINT (сквозная)
**Протокол:** `PROTOCOLS/06_METASTATE.md`
**Вход:** любая фаза.
Запускается по команде: «обнови метасостояние», «update metastate», «подведи итог».
**Протокол:** обновлять checkpoints.json после каждого значимого шага.
**Действия:**
1. **Ревью requests** — проверить каждый `ready_for_review`:
- ✅ approved → в `.agent/requests/archive/`, задача confirmed
- ❌ rejected → задача reopened, комментарий
2. **Архивация** — completed задачи → one-liner в manifest, детали в `.agent/archive/tasks/`
3. **Обновление project-state.md** — актуальный слепок проекта
4. **Обновление roadmap** — отметить выполненное, пересчитать приоритеты
5. **Создание handoff-summary.md** — полная сводка для следующего агента
6. **Индекс архива**`.agent/archive/index.json`
**Архивирование перед сохранением чекпоинта:**
- Если checkpoints.json уже существует — сохранить предыдущую версию в `.agent/archive/checkpoints/<last_updated>.json`
**Выход:** обновлённый `.agent/` — полный слепок проекта. Следующий агент читает только `.agent/`.
---
## Фаза 7: HANDOFF
**Протокол:** `PROTOCOLS/05_HANDOFF.md`
**Действия:**
- Если METASTATE был — просто валидировать и финализировать
- Если METASTATE не было — лёгкая архивация completed задач
- Валидация структуры `.agent/`
- Создание `.agent/session-summary.md`
- Финализация checkpoints.json
**Выход:** `.agent/session-summary.md`, финальный checkpoints.json
---
## Checkpoint (сквозная)
checkpoints.json обновляется после каждой фазы:
**Формат:**
```json
{
"metaagent_version": "2.1.0",
"metaagent_version": "1.1.0",
"session_id": "<uuid>",
"target_repo": "<path>",
"goal": "<цель>",
@@ -256,125 +229,108 @@ checkpoints.json обновляется после каждой фазы:
"design": { "adr": true, "alternative_arch": true },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": true }
"decomposition": { "invariant_tests": true },
"handoff": { "layer_structure": true }
},
"phases": {
"analysis": "completed",
"roadmap": "completed",
"design": "completed",
"red_team": "skipped",
"decomposition": "completed",
"execution": "completed",
"metastate": "completed",
"handoff": "completed"
"decomposition": "in_progress",
"environment": "pending",
"handoff": "pending"
},
"tasks": [
{ "id": "T1", "title": "...", "status": "archived", "origin": "user:direct" },
{ "id": "T2", "title": "...", "status": "pending", "origin": "roadmap:010" }
{ "id": "T1", "title": "...", "status": "completed",
"depends_on": [], "acceptance_criteria": ["..."] },
{ "id": "T2", "title": "...", "status": "pending",
"depends_on": ["T1"], "acceptance_criteria": ["..."] }
],
"last_updated": "<timestamp>"
}
```
`status` может быть: `pending`, `in_progress`, `completed`, `failed`, `skipped`.
Фаза `red_team` может быть `skipped` если config.red_team = false.
---
## Фаза 6: HANDOFF
**Вход:** все предыдущие фазы completed.
**Протокол:** `PROTOCOLS/05_HANDOFF.md`
**Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- **Архивировать завершённые задачи:**
- Для каждой задачи со статусом `completed` в `task-manifest.json`:
- Перенести полное описание в `.agent/archive/tasks/<id>.json`
- Заменить в манифесте на one-liner: `{ "id": "<id>", "title": "<title>", "status": "archived" }`
- Создать `.agent/archive/index.json` со списком архивированных задач
- Заархивировать предыдущий `checkpoints.json` в `.agent/archive/checkpoints/`
- Выполнить валидацию всех артефактов
- Если config.handoff.layer_structure: организовать `.agent/` по слоям
- Записать `.agent/handoff-summary.md` (в layer-3 при layer_structure=yes)
- Создать `.agent/session-summary.md` (в layer-0 при layer_structure=yes)
- Обновить checkpoints.json: `phases.handoff = "completed"`
- Сообщить пользователю/оркестратору
**Выход:** `.agent/handoff-summary.md` — итоговый документ для исполнительного агента.
---
## Фаза 7: EXIT
Мета-агент завершает работу. Управление переходит к исполнительному агенту.
---
## Структура .agent/
.agent/ всегда содержит служебную директорию `src/` с исходниками MetaAgent (см. фазу INIT).
При layer_structure=yes артефакты сессии раскладываются по слоям layer-0..3.
```
.agent/
checkpoints.json # состояние сессии (ядро)
session-summary.md # краткая сводка сессии
handoff-summary.md # сводка для следующего агента (создаётся METASTATE)
src/ # исходники MetaAgent (всегда)
META_AGENT_GUIDE.md
PROTOCOLS/
TEMPLATES/
BOUNDARIES.md
WORKFLOW.md
VERSION
install.sh / install.ps1
rules/
project-rules.md
roadmap/ # ИСТОЧНИКИ ЗАДАЧ (новое в v2.1)
sources.md # консолидированный список с приоритетами
archive/ # устаревшие roadmap-планы
decisions/ # архитектурные решения (ADR)
index.json
001-*.md
tasks/ # задачи
manifest.json # + поле origin
manifest.md
backlog/
requests/ # ЕДИНИЦЫ РЕЗУЛЬТАТА (новое в v2.1)
active/ # req-T1.json (ready_for_review)
archive/ # req-T1.json (approved/rejected)
context/
analysis-report.md # замороженный анализ на старте
project-state.md # динамический слепок проекта (обновляется METASTATE)
design-report.md
src/ # исходники MetaAgent (всегда)
META_AGENT_GUIDE.md # главная инструкция
PROTOCOLS/ # протоколы фаз
TEMPLATES/ # шаблоны артефактов
BOUNDARIES.md # границы
WORKFLOW.md # примеры работы
VERSION # версия MetaAgent
install.sh # скрипт установки/обновления (Unix)
install.ps1 # скрипт установки/обновления (Windows)
rules/ # пользовательские правила (всегда)
project-rules.md # правила проекта — читать перед каждой фазой
archive/ # архив завершённых артефактов (создаётся при HANDOFF)
index.json # мета-индекс архива
tasks/ # детали завершённых задач
checkpoints/ # исторические чекпоинты
adr/ # заменённые ADR
reports/ # устаревшие отчёты
layer-0/ # ядро сессии (только при layer_structure=yes)
checkpoints.json # всегда (ядро)
session-summary.md # краткая сводка сессии
layer-1/ # архитектурные решения (справочно)
adr/
001-технологический-стек.md
002-архитектурный-паттерн.md
...
risk-register.md
red-team-report.md
layer-2/ # дизайн и анализ (справочно)
analysis-report.md
design-report.md
layer-3/ # состояние исполнения
handoff-summary.md
task-manifest.json
task-manifest.md
baseline-test-report.log
archive/
index.json
tasks/
decisions/
requests/
checkpoints/
setup-report.log
```
`.temp/` в корне проекта:
```
.temp/
downloads/
patches/
cache/
agent-session-xxx/
```
---
## Принципы работы
### Два контура
**Project Loop** (однократно): INIT → ANALYSIS → ROADMAP → DESIGN → DECOMPOSITION.
Настраивает проект, определяет задачи.
**Work Loop** (циклически): EXECUTION → (request) → METASTATE (по команде).
Агент работает, создаёт requests, по команде пользователя подводит итог.
### Request — единица результата
Каждая выполненная задача завершается созданием request. Не просто «сделано», а документированный результат:
- суть изменений (не diff, а именно суть)
- ссылки на коммиты
- верификация (тесты, LSP)
- какие acceptance criteria закрыты
Request проходит ревью в METASTATE.
### .agent/ как слепок проекта
После METASTATE `.agent/` содержит полную картину. Следующий агент читает `.agent/` и не лезет в исходники проекта.
### Depth scale
Определяет глубину проработки:
| Depth | PROJECT LOOP | WORK LOOP |
|-------|-------------|-----------|
| 1-2 | INIT → ANALYSIS → DECOMP | EXECUTION (scaffold only) |
| 3-4 | + ROADMAP, DESIGN (light) | EXECUTION → METASTATE |
| 5-6 | + DESIGN (full), invariants | EXECUTION → METASTATE |
| 7-8 | + ADR, risk_register | EXECUTION → METASTATE |
| 9-10 | + Red Team | EXECUTION → METASTATE |
Исполнительный агент всегда начинает с layer-0 (checkpoints + session-summary),
затем при необходимости обращается к layer-1 (ADR для понимания "почему"),
layer-2 (детали дизайна), layer-3 (что было сделано).
+5 -2
View File
@@ -66,6 +66,9 @@ Q5 (если глубина >= 9): Нужен Red Team Review?
"risk_register": false,
"decomposition": {
"invariant_tests": false
},
"handoff": {
"layer_structure": false
}
}
```
@@ -75,7 +78,7 @@ Depth=4 (Light) означает:
- DESIGN — выполняется (если greenfield), но **без** ADR, Alternative Architecture, Risk Register
- DECOMPOSITION — задачи с acceptance criteria, **без** invariant-тестов
- SETUP — полный
- HANDOFF — `.agent/` организован по семантическим группам (decisions, tasks, context, rules)
- HANDOFF — плоский `.agent/` (без layer-структуры)
### 0.4. Запись .agent/metaagent-request.md
@@ -95,7 +98,7 @@ Depth=4 (Light) означает:
| RISK_REGISTER | {{ risk_register }} | — |
| DECOMPOSITION | ✓ | invariant_tests={{ invariant_tests }} |
| SETUP | ✓ | — |
| HANDOFF | ✓ | |
| HANDOFF | ✓ | layer_structure={{ layer_structure }} |
## Глубина проработки
+1 -154
View File
@@ -45,7 +45,6 @@ def needs_migration(stored, current):
|---|---|---|
| v0.x (нет поля) | v1.0.0 | M3.1 — M3.4 |
| v1.0.0 | v1.1.0 | M3.5 — M3.6 (см. ниже) |
| v1.1.x | v2.0.0 | M6.1 — M6.17 (см. ниже) |
### M4. Шаги миграции v0.x → v1.0.0
@@ -98,158 +97,6 @@ touch .agent/layer-1/adr/.gitkeep
Если `risk-register.md` уже существует на верхнем уровне — переместить в `.agent/layer-1/risk-register.md`.
### M6. Шаги миграции v1.1.x → v2.0.0
Цель: перейти от layer-0..3 структуры к семантической (decisions/tasks/context/rules).
#### M6.1. Удалить `handoff.layer_structure` из config
Если в `checkpoints.json` присутствует `config.handoff.layer_structure` — удалить поле:
```python
checkpoints["config"].pop("handoff", None)
# или если handoff пуст — удалить целиком
if "handoff" in checkpoints["config"] and not checkpoints["config"]["handoff"]:
del checkpoints["config"]["handoff"]
```
#### M6.2. Создать новые директории
```bash
mkdir -p .agent/decisions
mkdir -p .agent/tasks/backlog
mkdir -p .agent/context
mkdir -p .agent/archive/decisions
```
#### M6.3. Перенести ADR (.agent/layer-1/adr/ → .agent/decisions/)
```bash
if [ -d ".agent/layer-1/adr" ]; then
cp -n .agent/layer-1/adr/*.md .agent/decisions/ 2>/dev/null || true
fi
```
#### M6.4. Создать decisions/index.json
Если в `.agent/decisions/` есть .md файлы — создать индекс:
```json
{
"version": "2.0.0",
"decisions": [
{ "id": "001", "title": "<извлечь из первого заголовка>", "file": "001-....md" }
],
"created_at": "<timestamp>"
}
```
#### M6.5. Перенести risk-register.md (.agent/layer-1/ → .agent/context/)
```bash
if [ -f ".agent/layer-1/risk-register.md" ]; then
mv .agent/layer-1/risk-register.md .agent/context/risk-register.md
fi
```
#### M6.6. Перенести red-team-report.md (.agent/layer-1/ → .agent/context/)
```bash
if [ -f ".agent/layer-1/red-team-report.md" ]; then
mv .agent/layer-1/red-team-report.md .agent/context/red-team-report.md
fi
```
#### M6.7. Перенести analysis-report.md (.agent/layer-2/ → .agent/context/)
```bash
if [ -f ".agent/layer-2/analysis-report.md" ]; then
mv .agent/layer-2/analysis-report.md .agent/context/analysis-report.md
fi
```
#### M6.8. Перенести design-report.md (.agent/layer-2/ → .agent/context/)
```bash
if [ -f ".agent/layer-2/design-report.md" ]; then
mv .agent/layer-2/design-report.md .agent/context/design-report.md
fi
```
#### M6.9. Перенести task-manifest (.agent/task-manifest.json → .agent/tasks/manifest.json)
```bash
if [ -f ".agent/task-manifest.json" ]; then
mv .agent/task-manifest.json .agent/tasks/manifest.json
fi
if [ -f ".agent/task-manifest.md" ]; then
mv .agent/task-manifest.md .agent/tasks/manifest.md
fi
```
#### M6.10. Перенести handoff-summary.md (.agent/layer-3/ → .agent/)
```bash
if [ -f ".agent/layer-3/handoff-summary.md" ]; then
mv .agent/layer-3/handoff-summary.md .agent/handoff-summary.md
fi
```
#### M6.11. Перенести baseline-test-report.log (.agent/layer-3/ → .agent/context/)
```bash
if [ -f ".agent/layer-3/baseline-test-report.log" ]; then
mv .agent/layer-3/baseline-test-report.log .agent/context/baseline-test-report.log
fi
```
#### M6.12. Перенести setup-report.log (.agent/layer-3/ → .agent/context/)
```bash
if [ -f ".agent/layer-3/setup-report.log" ]; then
mv .agent/layer-3/setup-report.log .agent/context/setup-report.log
fi
```
#### M6.13. Перенести session-summary.md (.agent/layer-0/ → .agent/)
```bash
if [ -f ".agent/layer-0/session-summary.md" ]; then
mv .agent/layer-0/session-summary.md .agent/session-summary.md
fi
```
#### M6.14. Перенести checkpoints.json (.agent/layer-0/ → .agent/)
```bash
if [ -f ".agent/layer-0/checkpoints.json" ]; then
cp .agent/layer-0/checkpoints.json .agent/checkpoints.json
echo "[backup] layer-0/checkpoints.json сохранён на случай отката"
fi
```
#### M6.15. Перенести archive/adr/ → archive/decisions/
```bash
if [ -d ".agent/archive/adr" ]; then
cp -n .agent/archive/adr/* .agent/archive/decisions/ 2>/dev/null || true
rm -rf .agent/archive/adr
fi
```
#### M6.16. Удалить пустые layer-директории
```bash
rm -rf .agent/layer-0 .agent/layer-1 .agent/layer-2 .agent/layer-3
rm -rf .agent/archive/reports 2>/dev/null || true
```
#### M6.17. Обновить metaagent_version в checkpoints.json
```json
"metaagent_version": "2.0.0"
```
### M4. После миграции — резюме
Записать в `.agent/migration-report.log`:
@@ -265,7 +112,7 @@ rm -rf .agent/archive/reports 2>/dev/null || true
## Выход
- Обновлённый `.agent/checkpoints.json` (metaagent_version + config)
- Обновлённая структура `.agent/` (decisions/tasks/context вместо layer-0..3)
- Опционально: `.agent/layer-1/` структура
- `.agent/migration-report.log`
## Критерии завершения
+7 -17
View File
@@ -24,7 +24,8 @@
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false }
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
}
```
@@ -35,7 +36,8 @@
"design": { "adr": true, "alternative_arch": true },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": true }
"decomposition": { "invariant_tests": true },
"handoff": { "layer_structure": true }
}
```
@@ -123,27 +125,15 @@
- Вопросы, которые нужно задать пользователю перед проектированием
- Противоречия в README
### 1.8. Initial project state snapshot
После завершения анализа создать `.agent/context/project-state.md` — начальный слепок проекта по шаблону `TEMPLATES/project-state.md`:
- Тип проекта
- Текущая архитектура (кратко, из p.1.3)
- Ключевые модули и их статус (existing/stub/nonexistent)
- Tech stack (из p.1.2)
- Статус тестов (из p.1.5)
- Этот файл будет обновляться фазой METASTATE по мере эволюции проекта
## Выход
- `.agent/context/analysis-report.md` по шаблону `TEMPLATES/analysis-report.md`
- `.agent/context/project-state.md` — начальный слепок проекта
`.agent/analysis-report.md` по шаблону `TEMPLATES/analysis-report.md`.
Обновить checkpoints.json: `phases.analysis = "completed"`. Если проект `greenfield`, также установить `project_type = "greenfield"`.
## Критерии завершения фазы
- [ ] Тип проекта определён (existing / greenfield / scaffold)
- [ ] Все соответствующие разделы (1.1–1.8) выполнены
- [ ] `.agent/context/analysis-report.md` создан и заполнен
- [ ] `.agent/context/project-state.md` создан с начальным слепком
- [ ] Все соответствующие разделы (1.1–1.7) выполнены
- [ ] `.agent/analysis-report.md` создан и заполнен
- [ ] checkpoints.json обновлён
+9 -9
View File
@@ -6,7 +6,7 @@
## Вход
- `.agent/context/analysis-report.md` (project_type: greenfield или scaffold)
- `.agent/analysis-report.md` (project_type: greenfield или scaffold)
- `.agent/metaagent-request.md` (конфигурация сессии: adr, alternative_arch, risk_register)
- `.agent/checkpoints.json` (фаза design: pending)
@@ -120,16 +120,16 @@
Для каждого ключевого архитектурного решения (стек, БД, паттерн, структура модулей) создать отдельный ADR-файл:
```
.agent/decisions/001-технологический-стек.md
.agent/decisions/002-модульный-монолит.md
.agent/decisions/003-json-хранение.md
.agent/layer-1/adr/001-технологический-стек.md
.agent/layer-1/adr/002-модульный-монолит.md
.agent/layer-1/adr/003-json-хранение.md
```
Формат — по шаблону `TEMPLATES/adr-NNNN.md`.
### 2.10. Risk Register (если config.risk_register = yes)
Создать `.agent/context/risk-register.md` по шаблону `TEMPLATES/risk-register.md`:
Создать `.agent/layer-1/risk-register.md` по шаблону `TEMPLATES/risk-register.md`:
| # | Assumption | Impact if wrong | Mitigation | Review trigger |
|---|---|---|---|---|
@@ -150,9 +150,9 @@ T4: API endpoints
## Выход
- `.agent/context/design-report.md` по шаблону `TEMPLATES/design-report.md`
- `.agent/decisions/*.md` (если adr=yes)
- `.agent/context/risk-register.md` (если risk_register=yes)
- `.agent/design-report.md` по шаблону `TEMPLATES/design-report.md`
- `.agent/layer-1/adr/*.md` (если adr=yes)
- `.agent/layer-1/risk-register.md` (если risk_register=yes)
- Предварительная группировка задач (для передачи в DECOMPOSITION)
Обновить checkpoints.json: `phases.design = "completed"`.
@@ -169,5 +169,5 @@ T4: API endpoints
- [ ] ADR созданы (если config требует)
- [ ] Risk Register создан (если config требует)
- [ ] Задачи предварительно сгруппированы
- [ ] `.agent/context/design-report.md` создан
- [ ] `.agent/design-report.md` создан
- [ ] checkpoints.json обновлён
-99
View File
@@ -1,99 +0,0 @@
# Протокол 02: Дорожная карта (ROADMAP)
## Цель
Определить источники задач для проекта, их приоритеты и взаимосвязи. ROADMAP — мост между видением проекта и конкретными задачами в манифесте.
## Вход
- `.agent/context/analysis-report.md` — анализ репозитория
- `.agent/metaagent-request.md` — цель сессии
- `FUTURE/` — директория долгосрочных планов (если существует)
- `.agent/decisions/index.json` — принятые ADR (опционально)
- `.agent/checkpoints.json` (фаза roadmap: pending)
- Внешние источники: issues, feedback, пользовательские запросы
## Шаги
### 2.1. Сканирование FUTURE/
Если в корне проекта существует `FUTURE/`:
- Прочитать все `.md` файлы
- Каждый план: название, статус (active/archived), приоритет, зависимости
- Зафиксировать, какие планы уже реализованы, какие ожидают
### 2.2. Сканирование ADR
Если существует `.agent/decisions/index.json`:
- Прочитать индекс ADR
- Определить, какие решения требуют реализации (не все ADR — технические, часть может быть организационными)
- Для каждого ADR, требующего реализации: сформулировать задачу
### 2.3. Внешние источники
- Прочитать `.agent/metaagent-request.md` — явные запросы пользователя
- Если есть issues / feedback — включить в анализ
- Если агент обнаружил tech debt или улучшения в ANALYSIS — зафиксировать
### 2.4. Приоритизация
Присвоить каждой задаче приоритет:
| Приоритет | Описание |
|-----------|----------|
| **P0** | Критично, делать следующим |
| **P1** | Важно, сделать скоро |
| **P2** | Желательно |
| **P3** | В долгосрочной перспективе / отложено |
Правила приоритизации:
- Блокирующие зависимости поднимают приоритет задачи
- User-requested задачи получают P0-P1 по умолчанию
- ADR-задачи получают приоритет, соответствующий срочности решения
### 2.5. Консолидация в sources.md
Создать `.agent/roadmap/sources.md` по шаблону `TEMPLATES/roadmap-sources.md`:
```
# Roadmap Sources
## FUTURE Plans
| План | Приоритет | Статус |
|------|-----------|--------|
| 010-omo-integration | P1 | active |
## ADR-Derived Tasks
| ADR | Задача | Приоритет |
|-----|--------|-----------|
## User Requests
| Запрос | Приоритет | Источник |
|--------|-----------|----------|
## Agent-Identified Improvements
| Наблюдение | Задача | Приоритет |
|------------|--------|-----------|
## Consolidated Priority Queue
1. task (origin) — P0
```
### 2.6. Архивация устаревших roadmap
Если в `.agent/roadmap/archive/` есть предыдущие версии — они остаются справочно.
Если какие-то планы из FUTURE/* больше не актуальны — переместить в `FUTURE/archive/`.
## Выход
- `.agent/roadmap/sources.md` — консолидированный список источников задач с приоритетами
- Обновлённый `FUTURE/` (если были перемещения в archive)
- Обновить checkpoints.json: `phases.roadmap = "completed"`
## Критерии завершения
- [ ] Все источники задач просканированы (FUTURE, ADR, пользователь, агент)
- [ ] `.agent/roadmap/sources.md` создан с приоритетами P0-P3
- [ ] Каждая задача имеет origin-ссылку на источник
- [ ] Устаревшие планы перемещены в archive
- [ ] checkpoints.json обновлён
+3 -3
View File
@@ -6,8 +6,8 @@
## Вход
- `.agent/context/design-report.md`
- `.agent/decisions/*.md` (если созданы)
- `.agent/design-report.md`
- `.agent/layer-1/adr/*.md` (если созданы)
- `.agent/metaagent-request.md` (глубина проработки >= 9)
## Когда выполняется
@@ -56,7 +56,7 @@
## Выход
`.agent/context/red-team-report.md` с секциями:
`.agent/layer-1/red-team-report.md` с секциями:
```
## Найденные проблемы
+15 -33
View File
@@ -6,11 +6,10 @@
## Вход
- `.agent/context/analysis-report.md`
- `.agent/context/design-report.md` (опционально — для greenfield/scaffold)
- `.agent/decisions/*.md` (опционально)
- `.agent/context/risk-register.md` (опционально)
- `.agent/roadmap/sources.md` (опционально — из фазы ROADMAP)
- `.agent/analysis-report.md`
- `.agent/design-report.md` (опционально — для greenfield/scaffold)
- `.agent/layer-1/adr/*.md` (опционально)
- `.agent/layer-1/risk-register.md` (опционально)
- `.agent/metaagent-request.md` (конфигурация сессии)
- Цель пользователя (из checkpoints.json)
- `.agent/checkpoints.json` (фаза decomposition: pending)
@@ -34,14 +33,7 @@
- Затрагивает 5+ файлов
- Содержит союзы "и", "а также", "после чего"
### 3.3. Учёт roadmap
Если существует `.agent/roadmap/sources.md`:
- Сверить задачи с roadmap-приоритетами
- Задачи из roadmap получают приоритет P0-P3 в соответствии с sources.md
- Задачи без явного источника получают `origin: "decomposition"`
### 3.4. Структура задачи
### 3.3. Структура задачи
Каждая задача содержит:
@@ -52,20 +44,12 @@
| `description` | Описание (как и зачем) | "Создать SQLAlchemy модель..." |
| `type` | Тип задачи | `feature`, `refactor`, `test`, `fix`, `config`, `design`, `docs` |
| `status` | Статус задачи | `pending`, `in_progress`, `completed`, `failed`, `archived` |
| `origin` | Источник задачи | `roadmap:filename`, `adr:NNN`, `user:direct`, `agent:analysis`, `decomposition` |
| `files` | Список файлов, которые нужно создать/изменить | `["app/models/user.py"]` |
| `depends_on` | ID задач, от которых зависит | `[]` или `["T0"]` |
| `acceptance_criteria` | Список критериев приёмки (3-5 пунктов) | `["Модель проходит миграцию"]` |
| `context` | Доп. информация (ссылки на доки, примеры, релевантные секции из design-report) | `"Смотри app/models/base.py"` |
`origin` связывает задачу с источником:
- `roadmap:{filename}` — из FUTURE/ или roadmap плана
- `adr:{NNN}` — из Architecture Decision Record
- `user:direct` — напрямую от пользователя
- `agent:analysis` — выявлено агентом при анализе
- `decomposition` — создано при декомпозиции без внешнего источника
### 3.5. Типы задач (нумерация сдвинута)
### 3.4. Типы задач
| Тип | Описание |
|---|---|
@@ -78,25 +62,25 @@
| `docs` | Документация |
| `invariant` | Тест, проверяющий архитектурный инвариант (см. 3.7) |
### 3.6. Зелёная декомпозиция (для greenfield/scaffold)
### 3.5. Зелёная декомпозиция (для greenfield/scaffold)
Если есть `.agent/context/design-report.md` — задачи формируются на основе группировки из дизайна:
Если есть `.agent/design-report.md` — задачи формируются на основе группировки из дизайна:
1. **T1: init** — инициализация проекта, зависимости, конфиги, scaffold
2. **T2..Tn: features** — модули/функциональность по одному
3. **Tn+1: tests** — тесты на каждый модуль (можно в составе feature-задачи)
4. **Tn+2: polish** — документация, форматирование, финальная проверка
### 3.7. Сортировка
### 3.6. Сортировка
Задачи в манифесте располагаются в порядке выполнения:
1. Сначала задачи без зависимостей
2. Потом те, чьи зависимости уже выполнены
3. Последними — задачи с наибольшим числом зависимостей
### 3.8. Executable Invariants (если config.invariant_tests = yes)
### 3.7. Executable Invariants (если config.invariant_tests = yes)
Для каждого ADR (из `.agent/decisions/`) создать задачу типа `invariant` — тест, проверяющий архитектурное правило.
Для каждого ADR (из layer-1/adr/) создать задачу типа `invariant` — тест, проверяющий архитектурное правило.
**Правила превращения ADR в инварианты:**
@@ -127,25 +111,23 @@
## Выход
- `.agent/tasks/manifest.json` — по шаблону `TEMPLATES/task-manifest.json`
- `.agent/tasks/manifest.md` — по шаблону `TEMPLATES/task-manifest.md`
- `.agent/task-manifest.json` — по схеме `TEMPLATES/task-manifest.json`
- `.agent/task-manifest.md` — по шаблону `TEMPLATES/task-manifest.md`
Обновить checkpoints.json:
- `phases.decomposition = "completed"`
- `tasks` = полный массив задач со статусом `pending`
> **Примечание:** после HANDOFF завершённые задачи будут архивированы —
> полное описание уходит в `.agent/archive/tasks/`, в manifest.json остаётся
> полное описание уходит в `.agent/archive/tasks/`, в манифесте остаётся
> one-liner с `"status": "archived"`.
## Критерии завершения фазы
- [ ] Цель разбита на атомарные задачи
- [ ] Для каждой задачи указаны acceptance criteria
- [ ] Для каждой задачи указан origin (источник)
- [ ] Для каждой задачи указаны affected files
- [ ] Зависимости между задачами корректны (нет циклов)
- [ ] Задачи сверены с roadmap приоритетами (если sources.md существует)
- [ ] Invariant-задачи созданы для каждого ADR (если config требует)
- [ ] `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md` созданы
- [ ] `.agent/task-manifest.json` и `.agent/task-manifest.md` созданы
- [ ] checkpoints.json обновлён
+9 -13
View File
@@ -1,8 +1,4 @@
# DEPRECATED — Протокол 04: Настройка окружения (SETUP)
> **Устарел в MetaAgent v2.1.** Заменён на `PROTOCOLS/04_EXECUTION.md`.
> Оставлен для обратной совместимости (проекты, использующие v2.0).
> Новые проекты используют фазу EXECUTION, в которой настройка окружения — первый шаг перед выполнением задач.
# Протокол 04: Настройка окружения (SETUP)
## Цель
@@ -10,9 +6,9 @@
## Вход
- `.agent/context/analysis-report.md`
- `.agent/context/design-report.md` (опционально, для greenfield)
- `.agent/tasks/manifest.json`
- `.agent/analysis-report.md`
- `.agent/design-report.md` (опционально, для greenfield)
- `.agent/task-manifest.json`
- `.agent/checkpoints.json` (фаза environment: pending)
## Поведение в зависимости от типа проекта
@@ -48,7 +44,7 @@
### 4A.4. Baseline-тесты
- Запустить все тесты проекта
- Записать в `.agent/context/baseline-test-report.log`:
- Записать в `.agent/baseline-test-report.log`:
- Команда запуска
- Общее количество тестов
- Пройдено / упало / пропущено
@@ -101,7 +97,7 @@ class CashflowService:
- Создать пустой тестовый файл для каждого модуля
- Настроить тестовый раннер (pytest, jest и т.д.)
- Записать в `.agent/context/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
- Записать в `.agent/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
### 4B.5. Проверка сборки
@@ -114,8 +110,8 @@ class CashflowService:
## Выход
- Работоспособное окружение / инициализированный проект
- `.agent/context/baseline-test-report.log` — результат прогона тестов
- `.agent/context/setup-report.log` — лог установки зависимостей и сборки
- `.agent/baseline-test-report.log` — результат прогона тестов
- `.agent/setup-report.log` — лог установки зависимостей и сборки
Обновить checkpoints.json: `phases.environment = "completed"`.
@@ -124,7 +120,7 @@ class CashflowService:
- [ ] Зависимости установлены / проект инициализирован
- [ ] Проект собирается / импортируется без ошибок
- [ ] Baseline-тесты запущены, результат записан
- [ ] `.agent/context/baseline-test-report.log` и `.agent/context/setup-report.log` созданы
- [ ] `.agent/baseline-test-report.log` и `.agent/setup-report.log` созданы
- [ ] checkpoints.json обновлён
Если проект не собирается — **фаза считается проваленной**, checkpoints.json отмечает `phases.environment = "failed"`, управление возвращается пользователю.
-138
View File
@@ -1,138 +0,0 @@
# Протокол 04: Исполнение задач (EXECUTION)
## Цель
Выполнить задачи из manifest.json: реализовать код, написать тесты, закоммитить, создать request — артефакт результата.
## Вход
- `.agent/tasks/manifest.json` — манифест с задачами
- `.agent/context/analysis-report.md` — контекст проекта
- `.agent/context/design-report.md` — архитектурный план (опционально)
- `.agent/decisions/*.md` — ADR (опционально)
- `.agent/rules/project-rules.md` — правила проекта
- `.agent/checkpoints.json` (фаза execution: pending)
## Шаги
### 4.0. Setup окружения (первый запуск)
Если это первый запуск EXECUTION в сессии:
- Установить зависимости (через штатный пакетный менеджер)
- Запустить сборку/базовые тесты для верификации окружения
- Записать baseline в `.agent/context/baseline-test-report.log`
### 4.1. Выбор задачи
Найти в `.agent/tasks/manifest.json` задачу, удовлетворяющую всем условиям:
- `status: "pending"`
- Все `depends_on` имеют статус `completed` или `archived`
Если таких задач нет — EXECUTION завершён, перейти к ожиданию команды пользователя.
### 4.2. Блокировка задачи
Отметить задачу в манифесте:
```json
{
"id": "T1",
"status": "in_progress"
}
```
### 4.3. Исполнение
Реализовать задачу в соответствии с acceptance criteria:
- Следовать конвенциям проекта (выявленным в ANALYSIS)
- Соблюдать BOUNDARIES.md
- Если задача ссылается на ADR — следовать архитектурному решению
- Писать код + тесты
### 4.4. Верификация
- Запустить тесты (все или релевантные)
- Проверить LSP diagnostics на изменённых файлах
- Убедиться, что acceptance criteria выполнены
### 4.5. Коммит
Сделать git-коммит с результатами задачи. Сообщение коммита должно отражать суть выполненной задачи.
### 4.6. Создание request
Создать `.agent/requests/active/req-{task_id}.json`:
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "GET /health endpoint",
"status": "ready_for_review",
"goal": "Добавить ручку GET /health с тестами",
"changes": {
"summary": "Создан health router, подключён в main.py, написаны тесты",
"commits": ["abc1234", "abc1235"],
"files_changed": [
"app/routers/health.py",
"app/main.py",
"tests/test_health.py"
]
},
"verification": {
"tests_passed": "24/24",
"lsp_clean": true
},
"fulfills_ac": [
"Ручка возвращает 200 + {\"status\":\"ok\"}"
]
}
```
Request фиксирует:
- **changes.summary** — краткая суть изменений (не полный diff, а именно суть)
- **changes.commits** — ссылки на коммиты (чтобы можно было проанализировать при ревью)
- **changes.files_changed** — какие файлы затронуты
- **verification** — результаты проверки
- **fulfills_ac** — какие acceptance criteria закрыты
### 4.7. Завершение задачи
В манифесте:
```json
{
"id": "T1",
"status": "completed"
}
```
### 4.8. Цикл
Перейти к шагу 4.1 — выбрать следующую задачу.
Если задач больше нет — сообщить пользователю и ожидать команду (METASTATE или новую задачу).
## Request как единица результата
Request — ключевой артефакт v2.1. Не просто «задача сделана», а документированный результат:
- Что сделано (суть, не diff)
- Как проверить (коммиты, тесты)
- Что закрыто (acceptance criteria)
Request проходит ревью в фазе METASTATE:
- `ready_for_review` → после проверки → `approved` или `rejected`
## Выход
- Выполненные задачи в manifest.json (status: completed)
- `.agent/requests/active/req-{task_id}.json` для каждой выполненной задачи
- Обновлённый checkpoints.json (`phases.execution = "in_progress"` или `"completed"`)
## Критерии завершения
Фаза EXECUTION не имеет единого момента завершения — она циклична. Критерии для одной итерации:
- [ ] Acceptance criteria задачи выполнены
- [ ] Тесты проходят
- [ ] LSP diagnostics чист
- [ ] Коммит создан
- [ ] Request создан в `.agent/requests/active/`
- [ ] Задача в manifest.json отмечена completed
+134 -69
View File
@@ -1,123 +1,188 @@
# Протокол 07: Завершение сессии (HANDOFF)
# Протокол 05: Передача исполнительному агенту (HANDOFF)
## Цель
Легковесное завершение сессии: валидация структуры `.agent/`, финализация чекпоинтов, формирование сводки. Архивация и обновление project-state выполняются фазой METASTATE.
> **Важно:** если перед HANDOFF была выполнена фаза METASTATE (06) — архивация, project-state и handoff-summary уже готовы.
> HANDOFF в этом случае только валидирует и финализирует.
Подготовить и передать исполнительному агенту полный контекст для работы: задачи, окружение, правила.
## Вход
- `.agent/analysis-report.md`
- `.agent/design-report.md` (опционально, для greenfield)
- `.agent/layer-1/adr/*.md` (опционально)
- `.agent/layer-1/risk-register.md` (опционально)
- `.agent/layer-1/red-team-report.md` (опционально)
- `.agent/task-manifest.json`
- `.agent/task-manifest.md`
- `.agent/baseline-test-report.log`
- `.agent/checkpoints.json` (все предыдущие фазы: completed)
- `.agent/context/project-state.md` (опционально, создан в ANALYSIS, обновлён в METASTATE)
- `.agent/handoff-summary.md` (опционально, создан в METASTATE)
- `.agent/tasks/manifest.json`
- `.agent/roadmap/sources.md` (опционально)
- Все артефакты `.agent/`
## Шаги
### 5.1. Проверка: была ли METASTATE?
### 5.1. Архивация завершённых артефактов
Если существует `.agent/handoff-summary.md` и `.agent/context/project-state.md`:
- METASTATE уже выполнен
- Перейти к шагу 5.3 (Валидация)
Перед валидацией и передачей выполнить архивирование.
Если нет:
- METASTATE не выполнялся (например, сессия завершается до execution)
- Перейти к шагу 5.2 (Лёгкая архивация)
**Архивировать завершённые задачи:**
### 5.2. Лёгкая архивация (если METASTATE не было)
Для каждой задачи в `task-manifest.json` со статусом `completed`:
1. Создать `.agent/archive/tasks/<id>.json` — перенести полное описание задачи (все поля)
2. В `task-manifest.json` заменить задачу на one-liner:
```json
{ "id": "<id>", "title": "<title>", "status": "archived" }
```
Если есть completed задачи в manifest.json:
- Архивировать их в `.agent/archive/tasks/{id}.json`
- Заменить в manifest.json на one-liner
- Создать `.agent/archive/index.json`
**Архивировать чекпоинты:**
Если нет completed задач — пропустить.
Если `checkpoints.json` уже существует — сохранить предыдущую версию в `.agent/archive/checkpoints/<last_updated>.json`.
### 5.3. Валидация
**Создать индекс архива:**
Проверить:
```json
{
"version": "1.1.0",
"archived_at": "<timestamp>",
"tasks": [
{ "id": "T1", "title": "...", "archived_at": "<timestamp>" }
],
"checkpoints": [
{ "file": "checkpoints/2026-07-15T10-00-00.json", "archived_at": "<timestamp>" }
]
}
```
- [ ] Все фазы отмечены как `completed` или `skipped` в checkpoints.json
- [ ] `.agent/` содержит обязательные файлы:
### 5.2. Валидация
Перед передачей проверить:
- [ ] Все фазы отмечены как `completed` в checkpoints.json
- [ ] `.agent/` содержит все обязательные файлы:
- `checkpoints.json`
- `context/analysis-report.md`
- `context/project-state.md`
- `tasks/manifest.json` + `tasks/manifest.md`
- `analysis-report.md`
- `task-manifest.json` + `task-manifest.md`
- `baseline-test-report.log`
- `setup-report.log`
- `src/META_AGENT_GUIDE.md`
- `src/BOUNDARIES.md`
- `src/VERSION`
- `src/PROTOCOLS/`
- `src/TEMPLATES/`
- `rules/project-rules.md`
- [ ] В `.agent/tasks/manifest.json` нет циклических зависимостей
- `archive/index.json`
- [ ] Для greenfield: `design-report.md` присутствует
- [ ] В task-manifest.json нет циклических зависимостей
- [ ] Все acceptance criteria сформулированы измеримо
- [ ] Для каждой задачи указаны affected files и origin
- [ ] `.agent/src/` содержит актуальные исходники
- [ ] Для каждой задачи указаны affected files
- [ ] В репозитории нет незакоммиченных изменений (кроме `.agent/`)
- [ ] `.agent/src/` содержит актуальные исходники MetaAgent (META_AGENT_GUIDE.md, PROTOCOLS/, TEMPLATES/, BOUNDARIES.md, VERSION)
- [ ] `AGENTS.md` присутствует в корне репозитория
- [ ] `.agent/rules/` содержит `project-rules.md`
### 5.4. Создание session-summary.md
**Дополнительные проверки (если config включает):**
- [ ] ADR присутствуют (если adr=yes)
- [ ] Risk Register заполнен (если risk_register=yes)
- [ ] Red Team Report есть (если red_team=yes)
- [ ] Invariant-задачи в манифесте (если invariant_tests=yes)
Создать `.agent/session-summary.md` — краткая сводка сессии:
### 5.3. Layer-структура .agent/
```markdown
# Session Summary
Если `config.layer_structure = yes`, организовать артефакты по слоям:
**Session:** <id>
**MetaAgent version:** 2.1.0
**Date:** <timestamp>
**Goal:** <goal>
## Phases Executed
- [x] INIT
- [x] ANALYSIS
- [x] ROADMAP
- [x] DESIGN
- [x] DECOMPOSITION
- [x] EXECUTION (N tasks)
- [x] METASTATE
- [x] HANDOFF
## Results
- Tasks completed: N
- Requests approved: N
- Files changed: [list]
## Next
Следующий агент: читай .agent/handoff-summary.md
```
.agent/
layer-0/
checkpoints.json # всегда (ядро)
session-summary.md # краткая сводка сессии (создаётся здесь)
layer-1/
adr/ # ADR (опционально)
risk-register.md # (опционально)
red-team-report.md # (опционально)
layer-2/
analysis-report.md
design-report.md
design-report.md
layer-3/
handoff-summary.md
task-manifest.json
task-manifest.md
baseline-test-report.log
setup-report.log
```
### 5.5. Финализация checkpoints
Если `layer_structure = no` — артефакты остаются плоскими в `.agent/`, как раньше.
### 5.4. Создать handoff-summary.md
Заполнить по шаблону `TEMPLATES/handoff-summary.md`:
- **Session Info** — ID, цель, дата
- **Configuration** — какие функции были включены, глубина
- **Repo Summary** — краткая выжимка из analysis-report
- **Environment Status** — результат сборки и тестов
- **Design Summary** (если есть design-report) — ключевые архитектурные решения
- **ADR Summary** (если adr=yes) — какие решения задокументированы
- **Risk Register** (если risk_register=yes) — основные допущения
- **Task Overview** — количество задач, типы, список
- **Next Steps** — с какой задачи начинать исполнительному агенту
- **Project Rules** — ссылка на `.agent/rules/project-rules.md` (передаётся exec-агенту)
- **Archive** — ссылка на `.agent/archive/index.json` (история завершённых задач)
- **Caveats** — известные проблемы, ограничения, неясные моменты
- **Checkpoints** — актуальное состояние чекпоинтов
### 5.5. Финализировать checkpoints
- Отметить `phases.handoff = "completed"`
- Записать финальный `last_updated`
### 5.6. Сигнал
Сообщить пользователю/оркестратору:
```
HANDOFF COMPLETE
Session: <session_id>
Target: <target_repo>
Type: <existing | greenfield | scaffold>
Tasks: <N> total, <M> completed, <K> pending
Config: depth=<N>, adr=<yes|no>, red_team=<yes|no>, ...
Tasks: <count> tasks ready
Следующий агент начинает с .agent/handoff-summary.md
Исполнительный агент может начинать с задачи <T1>.
Контекст: .agent/handoff-summary.md
Манифест: .agent/task-manifest.json
```
## Что получает исполнительный агент
1. **Целевой репозиторий** — полностью настроенный, с установленными зависимостями
2. **`.agent/`** — директория со всеми артефактами (layer-структура или плоская)
3. **`task-manifest.json`** — машиночитаемый список задач
4. **`task-manifest.md`** — человекочитаемый список задач
5. **`handoff-summary.md`** — итоговая сводка
6. **`checkpoints.json`** — актуальное состояние (исполнительный агент будет его обновлять)
7. **`layer-1/adr/*.md`** (опционально) — ключевые решения
8. **`layer-1/risk-register.md`** (опционально) — допущения
9. **`layer-2/analysis-report.md`** — полный анализ репозитория (справочно)
10. **`layer-2/design-report.md`** (только для greenfield) — архитектурный план
11. **`layer-3/baseline-test-report.log`** — baseline тестов (чтобы не сломать существующее)
12. **`.agent/src/`** — полные исходники MetaAgent (справочно, всегда присутствуют)
13. **`.agent/rules/`** — пользовательские правила проекта
14. **`AGENTS.md`** — инструкция для AI-агента в корне проекта (всегда присутствует)
15. **`.agent/archive/`** — архив завершённых задач, чекпоинтов и устаревших артефактов
## Выход
- `.agent/session-summary.md`
- `.agent/checkpoints.json` (финальный)
- Если METASTATE не было: `.agent/archive/index.json`
- `.agent/layer-0/session-summary.md` (если layer_structure=yes)
- `.agent/layer-3/handoff-summary.md`
- `.agent/layer-0/checkpoints.json` (финальный)
- `.agent/archive/index.json` (создаётся при архивации)
## Критерии завершения
- [ ] Все артефакты на месте (согласно структуре .agent/)
- [ ] Если METASTATE не было — completed задачи архивированы
- [ ] session-summary.md создан
- [ ] Все артефакты на месте (с учётом layer-структуры)
- [ ] `.agent/src/` содержит актуальные исходники MetaAgent
- [ ] `.agent/rules/` содержит `project-rules.md`
- [ ] `AGENTS.md` присутствует в корне репозитория
- [ ] `.agent/archive/index.json` создан, завершённые задачи архивированы
- [ ] handoff-summary.md заполнен (включая config, design summary, ADR summary, archive)
- [ ] checkpoints.json финализирован
- [ ] Сигнал отправлен пользователю
- [ ] Сигнал отправлен пользователю/оркестратору
-149
View File
@@ -1,149 +0,0 @@
# Протокол 06: Обновление метасостояния (METASTATE)
## Цель
По команде пользователя «обнови метасостояние проекта» — провести ревью накопленных requests, синхронизировать манифест, обновить слепок проекта и подготовить `.agent/` как полную картину для следующей сессии.
## Вход
- `.agent/requests/active/` — все request-ы со статусом `ready_for_review`
- `.agent/tasks/manifest.json` — текущее состояние задач
- `.agent/context/project-state.md` — текущий слепок проекта (создан в ANALYSIS)
- `.agent/roadmap/sources.md` — дорожная карта (создана в ROADMAP)
- `.agent/checkpoints.json`
## Шаги
### 6.1. Сбор requests
Прочитать все файлы из `.agent/requests/active/` со статусом `ready_for_review`.
Каждый request — это выполненная задача, ожидающая подтверждения.
### 6.2. Ревью каждого request
Для каждого request:
1. **Верифицировать** — проверить, что verification корректен:
- Тесты действительно проходят (перезапустить, если нужно)
- LSP diagnostics чист
- Acceptance criteria выполнены
- При необходимости — проверить коммиты (git show)
2. **Принять или отклонить:**
- ✅ **approved** — всё ОК:
- Переместить request: `.agent/requests/active/``.agent/requests/archive/`
- Убедиться, что задача в manifest.json имеет `status: "completed"`
- ❌ **rejected** — есть проблемы:
- Оставить request в active/ с комментарием о причинах отказа
- В manifest.json: `status: "reopened"`, снять `claimed_by`
- Добавить `rejection_reason` в request
### 6.3. Архивация завершённых задач
Для каждой задачи в manifest.json со статусом `completed`:
1. Создать `.agent/archive/tasks/{id}.json` — полное описание задачи (все поля)
2. В manifest.json заменить на one-liner:
```json
{ "id": "T1", "title": "GET /health endpoint", "status": "archived", "origin": "user:direct" }
```
### 6.4. Обновление project-state.md
Переписать `.agent/context/project-state.md` с учётом выполненных задач:
- Обновить список модулей (какие добавлены/изменены)
- Обновить архитектурную схему (кратко)
- Обновить статус тестов
- Добавить новые ADR, если появились
- Убрать закрытые concerns
Цель: следующий агент читает project-state.md и понимает проект, не открывая исходники.
### 6.5. Обновление roadmap
В `.agent/roadmap/sources.md`:
- Отметить выполненные пункты
- Пересчитать приоритеты
- Если появились новые источники — добавить
### 6.6. Индекс архива
Создать/обновить `.agent/archive/index.json`:
```json
{
"version": "2.1.0",
"archived_at": "<timestamp>",
"tasks": [
{ "id": "T1", "title": "GET /health", "archived_at": "<timestamp>" }
],
"requests": [
{ "id": "req-T1", "task_id": "T1", "archived_at": "<timestamp>" }
],
"checkpoints": [
{ "file": "checkpoints/<timestamp>.json", "archived_at": "<timestamp>" }
]
}
```
### 6.7. Создание handoff-summary.md
Создать `.agent/handoff-summary.md` — полную сводку для следующего агента:
```markdown
## Session Summary
**Session:** <id>
**Goal:** <goal>
**Completed:** N tasks
**Pending:** M tasks
**Approved requests:** req-T1, req-T2
## Project State
(краткая выжимка из project-state.md)
## Next Steps
(с чего начать следующую сессию)
## Key Artifacts
- Project state: `.agent/context/project-state.md`
- Tasks: `.agent/tasks/manifest.json`
- Roadmap: `.agent/roadmap/sources.md`
- Pending reviews: `.agent/requests/active/`
- Archive: `.agent/archive/index.json`
```
### 6.8. Финализация чекпоинта
Обновить checkpoints.json:
- `phases.metastate = "completed"`
- Актуальный список задач
- `last_updated`
## Выход
- Подтверждённые requests: `.agent/requests/archive/`
- Архив задач: `.agent/archive/tasks/{id}.json`
- Обновлённый project-state.md
- Обновлённый roadmap/sources.md
- `.agent/handoff-summary.md` — полная сводка
- `.agent/archive/index.json`
- Финальный checkpoints.json
## Критерии завершения
- [ ] Все ready_for_review requests проверены (approved/rejected)
- [ ] Approved requests перемещены в archive
- [ ] Completed задачи архивированы (one-liner в manifest)
- [ ] project-state.md отражает актуальное состояние проекта
- [ ] roadmap/sources.md обновлён
- [ ] archive/index.json создан
- [ ] handoff-summary.md готов
- [ ] checkpoints.json финализирован
## Когда запускать
По команде пользователя:
- «обнови метасостояние»
- «update metastate»
- «подведи итог»
- «заверши сессию»
Может запускаться многократно в течение жизни проекта — после каждой группы выполненных задач.
+2 -2
View File
@@ -17,7 +17,7 @@
## Project Type
- **Type:** {{ project_type }}
- **Design report:** {% if project_type == "greenfield" or project_type == "scaffold" %}`.agent/context/design-report.md`{% else %}—{% endif %}
- **Design report:** {% if project_type == "greenfield" or project_type == "scaffold" %}`.agent/design-report.md`{% else %}—{% endif %}
## ADR Summary (если применимо)
@@ -39,7 +39,7 @@
- **Build:** {{ build_status }}
- **Tests:** {{ tests_passed }}/{{ tests_total }} passed
- **Baseline log:** `.agent/context/baseline-test-report.log`
- **Baseline log:** `.agent/baseline-test-report.log`
- **Dependencies:** {{ deps_status }}
## Task Overview
+1 -1
View File
@@ -14,7 +14,7 @@
| RISK_REGISTER | ✗ | — |
| DECOMPOSITION | ✓ | invariant_tests=yes |
| SETUP | ✓ | — |
| HANDOFF | ✓ | |
| HANDOFF | ✓ | layer_structure=yes |
## Глубина проработки
-43
View File
@@ -1,43 +0,0 @@
# Project State
# Auto-generated — updated by ANALYSE (initial) and METASTATE (on updates)
**Last updated:** {{ timestamp }}
**Session:** {{ session_id }}
## Project Type
{{ project_type }}
## Tech Stack
| Category | Technology |
|----------|-----------|
| Language | {{ language }} |
| Framework | {{ framework }} |
| Database | {{ database }} |
| Test runner | {{ test_runner }} |
| Package manager | {{ package_manager }} |
## Current Architecture
{{ architecture_description }}
## Key Modules
| Module | Status | Description |
|--------|--------|-------------|
| {{ module_name }} | {{ existing / stub / new }} | {{ module_description }} |
## Decisions in Effect
| ADR | Decision | Status |
|-----|----------|--------|
| {{ adr_id }} | {{ decision_summary }} | {{ active / superseded }} |
## Testing Status
{{ testing_summary }}
## Open Concerns
- {{ concern_1 }}
-29
View File
@@ -1,29 +0,0 @@
{
"$schema": ".agent/src/TEMPLATES/schemas/request-schema.json",
"template_version": "1.0",
"request_id": "req-{{ task_id }}",
"task_id": "{{ task_id }}",
"title": "{{ task_title }}",
"status": "ready_for_review",
"created_at": "{{ timestamp }}",
"goal": "{{ task_goal }}",
"changes": {
"summary": "{{ changes_summary }}",
"commits": [
"{{ commit_hash }}"
],
"files_changed": [
"path/to/file.py"
]
},
"verification": {
"tests_passed": "{{ test_results }}",
"lsp_clean": true
},
"fulfills_ac": [
"{{ acceptance_criterion }}"
]
}
-42
View File
@@ -1,42 +0,0 @@
# Roadmap Sources
# Auto-generated — created by ROADMAP phase
**Created:** {{ timestamp }}
**Session:** {{ session_id }}
## Priority Legend
- **P0** — Critical, do next
- **P1** — Important, do soon
- **P2** — Nice to have
- **P3** — Future / deferred
## Sources
### FUTURE Plans
| Plan | Priority | Status | Origin File |
|------|----------|--------|-------------|
| {{ plan_title }} | {{ P0-P3 }} | {{ active / archived }} | FUTURE/{{ filename }}.md |
### ADR-Derived Tasks
| Source ADR | Task | Priority |
|------------|------|----------|
| {{ adr_id }} | {{ task_description }} | {{ P0-P3 }} |
### User Requests
| Request | Priority | Source |
|---------|----------|--------|
| {{ request }} | {{ P0-P3 }} | {{ direct / issue / feedback }} |
### Agent-Identified Improvements
| Observation | Suggested Task | Priority |
|-------------|----------------|----------|
| {{ observation }} | {{ task }} | {{ P0-P3 }} |
## Consolidated Priority Queue
1. **{{ task_title }}** ({{ origin }}) — {{ priority }}
@@ -1,93 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/checkpoints/2.0.0",
"title": "MetaAgent Checkpoints",
"description": "Schema for .agent/checkpoints.json — session state",
"type": "object",
"properties": {
"metaagent_version": {
"type": "string",
"description": "MetaAgent version that created this checkpoint",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"session_id": {
"type": "string",
"description": "Unique session identifier"
},
"target_repo": {
"type": "string",
"description": "Path to the target repository"
},
"goal": {
"type": "string",
"description": "Session goal"
},
"project_type": {
"type": "string",
"enum": ["pending", "existing", "greenfield", "scaffold"],
"description": "Type of the target project"
},
"config": {
"type": "object",
"properties": {
"depth": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"description": "Depth of the session (1-10)"
},
"design": {
"type": "object",
"properties": {
"adr": { "type": "boolean" },
"alternative_arch": { "type": "boolean" }
},
"additionalProperties": false
},
"red_team": { "type": "boolean" },
"risk_register": { "type": "boolean" },
"decomposition": {
"type": "object",
"properties": {
"invariant_tests": { "type": "boolean" }
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"phases": {
"type": "object",
"properties": {
"analysis": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"design": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"red_team": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"decomposition": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"environment": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] },
"handoff": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "skipped"] }
},
"additionalProperties": false
},
"tasks": {
"type": "array",
"description": "List of tasks (one-liners after archiving)",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"status": { "type": "string", "enum": ["pending", "in_progress", "completed", "failed", "archived"] }
},
"required": ["id", "title", "status"],
"additionalProperties": false
}
},
"last_updated": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of last update"
}
},
"required": ["metaagent_version", "session_id", "goal", "config", "phases", "last_updated"],
"additionalProperties": false
}
@@ -1,60 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/decisions-index/2.0.0",
"title": "MetaAgent Decisions Index",
"description": "Schema for .agent/decisions/index.json — machine-readable index of ADRs",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "Schema version",
"enum": ["2.0"]
},
"decisions": {
"type": "array",
"description": "List of architecture decision records",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9]{3,}$",
"description": "ADR number (001, 002, ...)"
},
"title": {
"type": "string",
"description": "Short title of the decision"
},
"status": {
"type": "string",
"enum": ["proposed", "accepted", "deprecated", "superseded"],
"description": "ADR status"
},
"file": {
"type": "string",
"description": "Filename in .agent/decisions/ (e.g. 001-stack.md)"
},
"date": {
"type": "string",
"format": "date",
"description": "Decision date (ISO 8601)"
}
},
"required": ["id", "title", "file"],
"additionalProperties": false
}
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of index creation"
},
"updated_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of last update"
}
},
"required": ["version", "decisions"],
"additionalProperties": false
}
@@ -1,91 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "metaagent/task-manifest/2.0.0",
"title": "MetaAgent Task Manifest",
"description": "Schema for .agent/tasks/manifest.json — the global task manifest",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "Schema version",
"enum": ["2.0"]
},
"session_id": {
"type": "string",
"description": "Session ID that created this manifest"
},
"goal": {
"type": "string",
"description": "Overall goal of the session"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of creation"
},
"tasks": {
"type": "array",
"description": "List of tasks",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^T[0-9]+$",
"description": "Unique task identifier (T1, T2, ...)"
},
"title": {
"type": "string",
"description": "Task title"
},
"description": {
"type": "string",
"description": "Detailed description"
},
"type": {
"type": "string",
"enum": ["feature", "refactor", "test", "fix", "config", "design", "docs", "invariant"],
"description": "Task type"
},
"files": {
"type": "array",
"items": { "type": "string" },
"description": "Files affected by this task"
},
"depends_on": {
"type": "array",
"items": { "type": "string" },
"description": "Task IDs this task depends on"
},
"acceptance_criteria": {
"type": "array",
"items": { "type": "string" },
"description": "Measurable criteria for completion"
},
"context": {
"type": "string",
"description": "Additional context or references"
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed", "failed", "archived"],
"description": "Current task status"
},
"claimed_by": {
"type": "string",
"description": "Worker ID if claimed"
},
"claimed_at": {
"type": "string",
"format": "date-time",
"description": "When the task was claimed"
}
},
"required": ["id", "title", "type", "status"],
"additionalProperties": false
}
}
},
"required": ["version", "tasks"],
"additionalProperties": false
}
+4 -3
View File
@@ -14,6 +14,7 @@
| Red Team | {{ red_team_enabled }} |
| Risk Register | {{ risk_register_enabled }} |
| Invariant Tests | {{ invariant_tests_enabled }} |
| Layer Structure | {{ layer_structure_enabled }} |
## Phase Status
@@ -28,7 +29,7 @@
## Quick Links
- Task Manifest: `.agent/tasks/manifest.json`
- Task Manifest: `.agent/task-manifest.json`
- Handoff Summary: `.agent/handoff-summary.md`
- Design Report: `.agent/context/design-report.md`
- ADR: `.agent/decisions/` (если есть)
- Design Report: `.agent/analysis-report.md`
- ADR: `.agent/layer-1/adr/` (если есть)
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": ".agent/src/TEMPLATES/schemas/task-manifest-schema.json",
"$schema": "metaagent-task-manifest",
"version": "1.0",
"session_id": "{{ session_id }}",
"goal": "{{ goal }}",
+1 -1
View File
@@ -1 +1 @@
2.1.0
1.1.0
+14 -202
View File
@@ -28,7 +28,8 @@
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false }
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
},
"phases": {
"analysis": "pending",
@@ -49,7 +50,7 @@
Мета-агент выполняет `PROTOCOLS/01_ANALYSIS.md`. Определяет тип проекта: `existing`.
Результат `.agent/context/analysis-report.md`:
Результат `.agent/analysis-report.md`:
```markdown
## 2. Стек технологий
@@ -90,7 +91,7 @@
| T2 | Подключить router в main.py | config | T1 | Ручка доступна по /health |
| T3 | Написать тесты для /health | test | T2 | Тесты проверяют 200 и структуру ответа |
Создан `.agent/tasks/manifest.json` и `.agent/tasks/manifest.md`.
Создан `.agent/task-manifest.json` и `.agent/task-manifest.md`.
Чекпоинт обновлён: `decomposition = "completed"`. Tasks: T1-T3 со статусом `pending`.
@@ -102,7 +103,7 @@
- `pip install -r requirements.txt` — OK
- Запуск pytest — OK, 12 passed (базовый тест)
- Результат в `.agent/context/baseline-test-report.log`
- Результат в `.agent/baseline-test-report.log`
Чекпоинт обновлён: `environment = "completed"`.
@@ -137,7 +138,8 @@
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false }
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
},
"phases": {
"analysis": "completed",
@@ -197,7 +199,8 @@ Tasks: 3 tasks ready
"design": { "adr": true, "alternative_arch": true },
"red_team": false,
"risk_register": true,
"decomposition": { "invariant_tests": true }
"decomposition": { "invariant_tests": true },
"handoff": { "layer_structure": true }
},
"phases": {
"analysis": "pending",
@@ -240,7 +243,7 @@ Tasks: 3 tasks ready
Мета-агент выполняет `PROTOCOLS/02_DESIGN.md`.
Результат `.agent/context/design-report.md`:
Результат `.agent/design-report.md`:
```markdown
## 1. Технологический стек
@@ -310,7 +313,7 @@ T6: tests — тесты
- Установка fastapi, uvicorn, sqlalchemy, matplotlib, pytest
- Создание scaffold-структуры: `app/models/`, `app/services/`, `app/schemas/`, `tests/`
- Пустые заглушки модулей
- `.agent/context/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
- `.agent/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
---
@@ -325,15 +328,15 @@ Config: depth=7, adr=yes, risk_register=yes, invariant_tests=yes
Tasks: 6 tasks ready
Исполнительный агент может начинать с задачи T1 (init).
Архитектурный план: .agent/context/design-report.md
ADR: .agent/decisions/
Архитектурный план: .agent/design-report.md
ADR: .agent/layer-1/adr/
```
---
## После HANDOFF: работа исполнительного агента
Исполнительный агент читает `.agent/handoff-summary.md`, `.agent/tasks/manifest.json`, выполняет задачи по порядку, обновляя checkpoints.json после каждой.
Исполнительный агент читает `.agent/handoff-summary.md`, `.agent/task-manifest.json`, выполняет задачи по порядку, обновляя checkpoints.json после каждой.
После завершения всех задач:
@@ -351,194 +354,3 @@ T6: Тесты ✓
Все тесты проходят: 24/24 passed.
```
---
## Сценарий C: v2.1 — Координирующий агент + requests + METASTATE
**Цель:** Рефакторинг модуля авторизации: вынести логику из монолитного файла в отдельные модули.
**Целевой репозиторий:** `github.com/example/fastapi-app`
**Пользователь:** "Вынеси авторизацию в отдельные модули: auth/router.py, auth/schemas.py, auth/deps.py"
**Версия MetaAgent: 2.1.0**
---
### PROJECT LOOP
#### Фаза INIT
Агент создаёт `.agent/`, инициализирует чекпоинт:
```json
{
"metaagent_version": "2.1.0",
"session_id": "ses_v21_001",
"goal": "Рефакторинг авторизации: вынести в модули auth/",
"project_type": "existing",
"phases": {
"analysis": "pending",
"roadmap": "pending",
"design": "skipped",
"decomposition": "pending",
"execution": "pending",
"metastate": "pending",
"handoff": "pending"
}
}
```
#### Фаза ANALYSIS
Агент сканирует проект:
- Стек: Python, FastAPI, SQLAlchemy
- auth/login.py — 450 строк, монолит (цель рефакторинга)
- Создаёт `.agent/context/analysis-report.md`
- Создаёт `.agent/context/project-state.md` — начальный слепок
#### Фаза ROADMAP
Сканирование FUTURE/ — пусто. ADR — нет. Единственный источник — пользовательский запрос.
Создаётся `.agent/roadmap/sources.md`:
```markdown
## Sources
### User Requests
| Вынести авторизацию | P0 | user:direct |
### Consolidated Priority Queue
1. Вынести auth/ (user:direct) — P0
```
#### Фаза DECOMPOSITION
Задачи с origin:
```json
{
"tasks": [
{
"id": "T1",
"title": "Создать auth/router.py",
"origin": "user:direct",
"status": "pending",
"files": ["app/auth/router.py"]
},
{
"id": "T2",
"title": "Создать auth/schemas.py",
"origin": "user:direct",
"status": "pending",
"files": ["app/auth/schemas.py"],
"depends_on": ["T1"]
},
{
"id": "T3",
"title": "Создать auth/deps.py",
"origin": "user:direct",
"status": "pending",
"files": ["app/auth/deps.py"],
"depends_on": ["T1"]
}
]
}
```
---
### WORK LOOP (первая итерация)
#### Фаза EXECUTION — задача T1
1. Берёт T1 (`pending`, нет зависимостей)
2. Отмечает `in_progress`
3. Создаёт `app/auth/router.py` — переносит логику роутов из `auth/login.py`
4. Пишет тесты
5. Тесты проходят: 48/48
6. Коммит: `abc1234 — feat: extract auth router`
7. Создаёт request:
```json
{
"request_id": "req-T1",
"task_id": "T1",
"title": "Создать auth/router.py",
"status": "ready_for_review",
"changes": {
"summary": "Вынесены роуты авторизации из auth/login.py в auth/router.py. Добавлены тесты.",
"commits": ["abc1234"],
"files_changed": ["app/auth/router.py", "app/auth/__init__.py", "tests/test_auth_router.py"]
},
"verification": { "tests_passed": "48/48", "lsp_clean": true },
"fulfills_ac": ["Роуты авторизации доступны через app/auth/router.py", "Старые тесты проходят"]
}
```
8. T1 → completed
#### Фаза EXECUTION — задача T2 (аналогично)
Создаёт `auth/schemas.py`, request `req-T2`.
#### Фаза EXECUTION — задача T3 (аналогично)
Создаёт `auth/deps.py`, request `req-T3`.
Задачи закончились. Агент ждёт команду.
---
### METASTATE (по команде пользователя)
**Пользователь:** "обнови метасостояние"
1. **Ревью requests:** три request-а, все approved
- req-T1 → `.agent/requests/archive/req-T1.json`
- req-T2 → `.agent/requests/archive/req-T2.json`
- req-T3 → `.agent/requests/archive/req-T3.json`
2. **Архивация задач:**
- T1 в manifest → one-liner, детали в `.agent/archive/tasks/T1.json`
- T2, T3 — аналогично
3. **Обновление project-state.md:**
```markdown
## Key Modules
| Module | Status | Description |
|--------|--------|-------------|
| app/auth/router.py | new | Вынесенные роуты авторизации |
| app/auth/schemas.py | new | Pydantic схемы |
| app/auth/deps.py | new | Dependency injection |
```
4. **Обновление roadmap:** задачи выполнены → moved to done
5. **Создание handoff-summary.md:**
```markdown
## Session Summary
**Goal:** Рефакторинг авторизации
**Completed:** 3/3 tasks
**Approved requests:** req-T1, req-T2, req-T3
## Project State
Модуль auth разбит на router+schema+deps.
Исходный auth/login.py: 450 → 120 строк.
## Next Steps
- Проверить, не осталось ли прямых импортов из старого login.py
- Обновить main.py если нужно
```
---
### HANDOFF
```text
HANDOFF COMPLETE
Session: ses_v21_001
Type: existing
Tasks: 3/3 completed
Следующий агент начинает с .agent/handoff-summary.md
```
+79 -237
View File
@@ -1,45 +1,29 @@
#!/usr/bin/env pwsh
# MetaAgent — установка исходников в целевой проект
# Usage: .\install.ps1 [[-Path] target_path] [-Check] [-Update]
# Usage: .\install.ps1 [[-Path] target_path] [-Update]
param(
[string]$Path = "",
[switch]$Check,
[switch]$Update,
[switch]$Help
)
$MetaAgentSrc = Split-Path -Parent $MyInvocation.MyCommand.Path
# --- helpers ---
function Write-Info { Write-Host "" -NoNewline -ForegroundColor Blue; Write-Host " $args" }
function Write-Ok { Write-Host "" -NoNewline -ForegroundColor Green; Write-Host " $args" }
function Write-Skip { Write-Host " " -NoNewline -ForegroundColor Yellow; Write-Host " $args" }
function Write-Warn { Write-Host "" -NoNewline -ForegroundColor Yellow; Write-Host " $args" }
function Write-Fail { Write-Host "" -NoNewline -ForegroundColor Red; Write-Host " $args" }
function Write-Header { param([string]$Label)
Write-Host ""
Write-Host ("" * 40)
Write-Host " $Label"
Write-Host ("" * 40)
}
function Show-Usage {
@"
Usage: install.ps1 [[-Path] target_path] [-Check] [-Update] [-Help]
Usage: install.ps1 [[-Path] target_path] [-Update] [-Help]
Install MetaAgent sources into <target>/.agent/src/
Options:
-Path Path to target project (default: interactive prompt)
-Check Dry-run: only check target readiness, no install
-Update Overwrite existing files in .agent/src/
-Help Show this help
Examples:
.\install.ps1
.\install.ps1 -Path C:\Projects\MyApp
.\install.ps1 -Path C:\Projects\MyApp -Check
.\install.ps1 -Path C:\Projects\MyApp -Update
"@
exit 0
@@ -47,163 +31,69 @@ Examples:
if ($Help) { Show-Usage }
# --- resolve target ---
$TargetPath = $Path
if (-not $TargetPath) {
$TargetPath = Read-Host "Enter path to target project"
}
$TargetPath = $TargetPath.Trim()
# --- pre-flight -----------------------------------------------------------
Write-Header "Pre-flight"
# 1. target exists?
if (-not (Test-Path $TargetPath -PathType Container)) {
Write-Fail "Target directory '$TargetPath' does not exist."
Write-Error "Directory '$TargetPath' does not exist."
exit 1
}
$TargetPath = (Resolve-Path $TargetPath).Path
Write-Ok "Target: $TargetPath"
# 2. write permission? (try to create a temp file as probe)
$probe = [System.IO.Path]::Combine($TargetPath, ".metaagent_probe.tmp")
try {
[System.IO.File]::WriteAllBytes($probe, [byte[]]@())
Remove-Item $probe -Force
Write-Ok "Write permission: yes"
} catch {
Write-Fail "No write permission on '$TargetPath'."
exit 1
}
# 3. already installed? compare versions
$AgentDir = Join-Path $TargetPath ".agent"
$SrcDir = Join-Path $AgentDir "src"
$VersionFile = Join-Path $MetaAgentSrc "VERSION"
$Version = if (Test-Path $VersionFile -PathType Leaf) {
(Get-Content $VersionFile -Raw -Encoding UTF8).Trim()
} else { "?" }
$oldVerPath = Join-Path $SrcDir "VERSION"
if (Test-Path $oldVerPath -PathType Leaf) {
$oldVer = (Get-Content $oldVerPath -Raw -Encoding UTF8).Trim()
if ($oldVer -ne $Version) {
Write-Info "Existing MetaAgent v$oldVer found → upgrading to v$Version"
} else {
Write-Skip "MetaAgent v${Version} already installed (use -Update to reinstall)"
if (-not $Check) {
Write-Warn "No changes applied. Run with -Update to overwrite existing files."
}
}
} else {
Write-Info "Fresh install: MetaAgent v$Version"
}
# 4. summary
$AgentsMd = Join-Path $TargetPath "AGENTS.md"
$RulesDir = Join-Path $AgentDir "rules"
$DecisionsDir = Join-Path $AgentDir "decisions"
$TasksDir = Join-Path $AgentDir "tasks"
$ContextDir = Join-Path $AgentDir "context"
$ArchiveDir = Join-Path $AgentDir "archive"
$RequestsDir = Join-Path $AgentDir "requests"
$RoadmapDir = Join-Path $AgentDir "roadmap"
$TempDir = Join-Path $TargetPath ".temp"
$ArchiveDir = Join-Path $AgentDir "archive"
$VersionFile = Join-Path $MetaAgentSrc "VERSION"
$Version = if (Test-Path $VersionFile) { Get-Content $VersionFile -Raw | ForEach-Object { $_.Trim() } } else { "?" }
$DirList = @(
$SrcDir, $RulesDir, $DecisionsDir, $TasksDir,
(Join-Path $TasksDir "backlog"), $ContextDir,
$ArchiveDir,
(Join-Path $ArchiveDir "tasks"),
(Join-Path $ArchiveDir "decisions"),
(Join-Path $ArchiveDir "checkpoints"),
(Join-Path $RequestsDir "active"),
(Join-Path $RequestsDir "archive"),
$RoadmapDir,
(Join-Path $RoadmapDir "archive"),
$TempDir
)
if ($Check) {
Write-Host ""
Write-Info "--check mode: all checks passed, no changes applied."
exit 0
}
# --- phase 1: directories ------------------------------------------------
Write-Header "Directories"
foreach ($d in $DirList) {
$null = New-Item -ItemType Directory -Path $d -Force
$short = $d.Replace("$TargetPath\", "")
if (Test-Path $d -PathType Container) {
Write-Ok $short
} else {
Write-Fail "$short (creation failed)"
}
}
# --- phase 2: files ------------------------------------------------------
Write-Header "Files"
$copyCount = 0
$skipCount = 0
$failCount = 0
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
New-Item -ItemType Directory -Path $RulesDir -Force | Out-Null
New-Item -ItemType Directory -Path $ArchiveDir -Force | Out-Null
Write-Host "Installing MetaAgent v$Version$SrcDir"
# --- copy files ---
function Copy-File {
param([string]$Src, [string]$DstDir)
$name = Split-Path $Src -Leaf
$dst = Join-Path $DstDir $name
if (-not (Test-Path $Src -PathType Leaf)) {
Write-Skip "$name (source not found)"
$script:skipCount++
Write-Host " [skip] $name (not found)"
return
}
$dst = Join-Path $DstDir $name
if ($Update -or -not (Test-Path $dst)) {
try {
Copy-Item $Src $dst -Force -ErrorAction Stop
Write-Ok $name
$script:copyCount++
} catch {
Write-Fail $name
$script:failCount++
}
Copy-Item $Src $dst -Force
Write-Host " [copy] $name"
} else {
Write-Skip "$name (exists, use -Update to overwrite)"
$script:skipCount++
Write-Host " [skip] $name (exists, use -Update to overwrite)"
}
}
function Copy-Dir {
param([string]$Src, [string]$DstDir)
$name = Split-Path $Src -Leaf
$dst = Join-Path $DstDir $name
if (-not (Test-Path $Src -PathType Container)) {
Write-Skip "$name/ (source not found)"
$script:skipCount++
Write-Host " [skip] $name/ (not found)"
return
}
$null = New-Item -ItemType Directory -Path $dst -Force
try {
if ($Update) {
Get-ChildItem $Src | ForEach-Object {
Copy-Item $_.FullName $dst -Recurse -Force -ErrorAction Stop
}
} else {
Get-ChildItem $Src | ForEach-Object {
$targetPath = Join-Path $dst $_.Name
if (-not (Test-Path $targetPath)) {
Copy-Item $_.FullName $dst -Recurse -ErrorAction Stop
}
$dst = Join-Path $DstDir $name
New-Item -ItemType Directory -Path $dst -Force | Out-Null
if ($Update) {
Get-ChildItem $Src | ForEach-Object {
Copy-Item $_.FullName $dst -Recurse -Force
}
} else {
Get-ChildItem $Src | ForEach-Object {
$targetPath = Join-Path $dst $_.Name
if (-not (Test-Path $targetPath)) {
Copy-Item $_.FullName $dst -Recurse
}
}
Write-Ok "$name/"
$script:copyCount++
} catch {
Write-Fail "$name/ (partial copy)"
$script:failCount++
}
Write-Host " [copy] $name/"
}
Copy-File (Join-Path $MetaAgentSrc "META_AGENT_GUIDE.md") $SrcDir
@@ -215,107 +105,59 @@ Copy-Dir (Join-Path $MetaAgentSrc "TEMPLATES") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.sh") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.ps1") $SrcDir
# --- phase 3: AGENTS.md --------------------------------------------------
Write-Header "AGENTS.md"
# --- create / update AGENTS.md in root of target ---
$AgentsMd = Join-Path $TargetPath "AGENTS.md"
function New-AgentsMd {
param([string]$Path)
@"
# MetaAgent
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v$Version
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | `.agent/src/META_AGENT_GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| Правила проекта | `.agent/rules/project-rules.md` |
| Примеры работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | `.agent/checkpoints.json` |
| Манифест задач | `.agent/task-manifest.json` |
| Сводка для exec-агента | `.agent/handoff-summary.md` |
| Анализ репозитория | `.agent/analysis-report.md` |
## Для исполнительного агента
1. **Прочитай** `.agent/src/META_AGENT_GUIDE.md` пойми жизненный цикл MetaAgent.
2. **Прочитай** `.agent/src/BOUNDARIES.md` соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` выполни пользовательские правила.
4. **Проверь** `.agent/checkpoints.json` если существует, используй как состояние сессии.
5. **Проверь** `.agent/task-manifest.json` если существует, выполняй задачи по порядку.
6. Если `.agent/` не инициализирован или устарел запусти `install.ps1 -Update` для
обновления исходников MetaAgent до актуальной версии.
"@
}
if (-not (Test-Path $AgentsMd -PathType Leaf)) {
$content = @"
# MetaAgent
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v$Version
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | `.agent/src/META_AGENT_GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| Правила проекта | `.agent/rules/project-rules.md` |
| Примеры работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | `.agent/checkpoints.json` |
| Манифест задач | `.agent/task-manifest.json` |
| Сводка для exec-агента | `.agent/handoff-summary.md` |
| Анализ репозитория | `.agent/analysis-report.md` |
## Для исполнительного агента
1. **Прочитай** `.agent/src/META_AGENT_GUIDE.md` пойми жизненный цикл MetaAgent.
2. **Прочитай** `.agent/src/BOUNDARIES.md` соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` выполни пользовательские правила.
4. **Проверь** `.agent/checkpoints.json` если существует, используй как состояние сессии.
5. **Проверь** `.agent/task-manifest.json` если существует, выполняй задачи по порядку.
6. Если `.agent/` не инициализирован или устарел запусти `install.sh --update` для
обновления исходников MetaAgent до актуальной версии.
"@
$utf8 = [System.Text.Encoding]::UTF8
[System.IO.File]::WriteAllBytes($AgentsMd, $utf8.GetBytes($content))
Write-Ok "AGENTS.md created"
New-AgentsMd $AgentsMd | Out-File -FilePath $AgentsMd -Encoding utf8
Write-Host " [create] AGENTS.md"
} elseif ($Update) {
$content = @"
# MetaAgent
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v$Version
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | `.agent/src/META_AGENT_GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| Правила проекта | `.agent/rules/project-rules.md` |
| Примеры работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | `.agent/checkpoints.json` |
| Манифест задач | `.agent/task-manifest.json` |
| Сводка для exec-агента | `.agent/handoff-summary.md` |
| Анализ репозитория | `.agent/analysis-report.md` |
## Для исполнительного агента
1. **Прочитай** `.agent/src/META_AGENT_GUIDE.md` пойми жизненный цикл MetaAgent.
2. **Прочитай** `.agent/src/BOUNDARIES.md` соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` выполни пользовательские правила.
4. **Проверь** `.agent/checkpoints.json` если существует, используй как состояние сессии.
5. **Проверь** `.agent/task-manifest.json` если существует, выполняй задачи по порядку.
6. Если `.agent/` не инициализирован или устарел запусти `install.sh --update` для
обновления исходников MetaAgent до актуальной версии.
"@
$utf8 = [System.Text.Encoding]::UTF8
[System.IO.File]::WriteAllBytes($AgentsMd, $utf8.GetBytes($content))
Write-Ok "AGENTS.md updated"
New-AgentsMd $AgentsMd | Out-File -FilePath $AgentsMd -Encoding utf8
Write-Host " [update] AGENTS.md"
} else {
Write-Skip "AGENTS.md (exists, use -Update to overwrite)"
$script:skipCount++
Write-Host " [skip] AGENTS.md (exists, use -Update to overwrite)"
}
# --- summary -------------------------------------------------------------
Write-Header "Summary"
Write-Host " MetaAgent v$Version$SrcDir"
Write-Host ""
if ($copyCount -gt 0) { Write-Ok "$copyCount file(s) copied" }
if ($skipCount -gt 0) { Write-Skip "$skipCount file(s) skipped" }
if ($failCount -gt 0) { Write-Fail "$failCount file(s) failed" }
Write-Host ""
if ($failCount -eq 0) {
Write-Ok "Installation completed successfully."
} else {
Write-Fail "Installation completed with $failCount error(s)."
exit 1
}
Write-Host "Done! MetaAgent v$Version installed at $SrcDir"
+26 -163
View File
@@ -1,207 +1,89 @@
#!/usr/bin/env bash
# MetaAgent — установка исходников в целевой проект
# Usage: ./install.sh [--check|--update] [target_path]
# Usage: ./install.sh [--update] [target_path]
set -euo pipefail
METAAGENT_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- helpers ---
red=; grn=; ylw=; blu=; rst=
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
red=$(tput setaf 1); grn=$(tput setaf 2)
ylw=$(tput setaf 3); blu=$(tput setaf 4)
rst=$(tput sgr0)
fi
info() { echo " ${blu}${rst} $*"; }
ok() { echo " ${grn}${rst} $*"; }
skip() { echo " ${ylw}${rst} $*"; }
warn() { echo " ${ylw}${rst} $*"; }
fail() { echo " ${red}${rst} $*"; }
header(){ echo; echo "────────────────────────────────────────"; echo " $*"; echo "────────────────────────────────────────"; }
usage() {
cat <<EOF
Usage: $0 [--check|--update] [target_path]
Usage: $0 [--update] [target_path]
Install MetaAgent sources into <target>/.agent/src/
Options:
--check, -c Dry-run: only check target readiness, no install
--update, -u Overwrite existing files in .agent/src/
--help, -h Show this help
Examples:
$0
$0 /path/to/project
$0 --check /path/to/project
$0 --update /path/to/project
EOF
exit 0
}
# --- arg parsing ---
CHECK=false
UPDATE=false
TARGET_PATH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--check|-c) CHECK=true; shift ;;
--update|-u) UPDATE=true; shift ;;
--help|-h) usage ;;
--*) echo "${red}Unknown option:${rst} $1"; usage ;;
--update|-u) UPDATE=true; shift ;;
--help|-h) usage ;;
--*) echo "Unknown option: $1"; usage ;;
*) TARGET_PATH="$1"; shift ;;
esac
done
# --- resolve target ---
if [[ -z "$TARGET_PATH" ]]; then
read -r -p "Enter path to target project: " TARGET_PATH
fi
TARGET_PATH="${TARGET_PATH/#\~/$HOME}"
# --- pre-flight -----------------------------------------------------------
header "Pre-flight"
# 1. target exists?
if [[ ! -d "$TARGET_PATH" ]]; then
fail "Target directory '$TARGET_PATH' does not exist."
exit 1
fi
# resolve to absolute path
TARGET_PATH="$(cd "$TARGET_PATH" 2>/dev/null && pwd)" || {
fail "Cannot access '$TARGET_PATH'."
echo "Error: Directory '$TARGET_PATH' does not exist."
exit 1
}
ok "Target: $TARGET_PATH"
# 2. write permission?
if [[ ! -w "$TARGET_PATH" ]]; then
fail "No write permission on '$TARGET_PATH'."
exit 1
fi
ok "Write permission: yes"
# 3. already installed? compare versions
AGENT_DIR="$TARGET_PATH/.agent"
SRC_DIR="$AGENT_DIR/src"
VERSION="$(cat "$METAAGENT_SRC/VERSION" 2>/dev/null || echo '?')"
if [[ -f "$SRC_DIR/VERSION" ]]; then
OLD_VER="$(cat "$SRC_DIR/VERSION" 2>/dev/null || echo '?')"
if [[ "$OLD_VER" != "$VERSION" ]]; then
info "Existing MetaAgent v${OLD_VER} found → upgrading to v${VERSION}"
else
skip "MetaAgent v${VERSION} already installed (use --update to reinstall)"
if [[ "$CHECK" == false ]]; then
warn "No changes applied. Run with --update to overwrite existing files."
fi
fi
else
info "Fresh install: MetaAgent v$VERSION"
fi
# 4. summary
AGENTS_MD="$TARGET_PATH/AGENTS.md"
RULES_DIR="$AGENT_DIR/rules"
DECISIONS_DIR="$AGENT_DIR/decisions"
TASKS_DIR="$AGENT_DIR/tasks"
CONTEXT_DIR="$AGENT_DIR/context"
ARCHIVE_DIR="$AGENT_DIR/archive"
ARCHIVE_TASKS_DIR="$ARCHIVE_DIR/tasks"
ARCHIVE_DECISIONS_DIR="$ARCHIVE_DIR/decisions"
ARCHIVE_CHECKPOINTS_DIR="$ARCHIVE_DIR/checkpoints"
REQUESTS_DIR="$AGENT_DIR/requests"
REQUESTS_ACTIVE_DIR="$REQUESTS_DIR/active"
REQUESTS_ARCHIVE_DIR="$REQUESTS_DIR/archive"
ROADMAP_DIR="$AGENT_DIR/roadmap"
ROADMAP_ARCHIVE_DIR="$ROADMAP_DIR/archive"
TEMP_DIR="$TARGET_PATH/.temp"
if [[ "$CHECK" == true ]]; then
echo ""
info "${ylw}--check mode:${rst} all checks passed, no changes applied."
exit 0
fi
# --- phase 1: directories ------------------------------------------------
header "Directories"
mkdir -p "$SRC_DIR" "$RULES_DIR" "$DECISIONS_DIR" "$TASKS_DIR" "$TASKS_DIR/backlog" \
"$CONTEXT_DIR" "$ARCHIVE_DIR" "$ARCHIVE_TASKS_DIR" "$ARCHIVE_DECISIONS_DIR" \
"$ARCHIVE_CHECKPOINTS_DIR" \
"$REQUESTS_ACTIVE_DIR" "$REQUESTS_ARCHIVE_DIR" \
"$ROADMAP_DIR" "$ROADMAP_ARCHIVE_DIR" \
"$TEMP_DIR"
for d in "$SRC_DIR" "$RULES_DIR" "$DECISIONS_DIR" "$TASKS_DIR" "$TASKS_DIR/backlog" \
"$CONTEXT_DIR" "$ARCHIVE_DIR" "$ARCHIVE_TASKS_DIR" "$ARCHIVE_DECISIONS_DIR" \
"$ARCHIVE_CHECKPOINTS_DIR" \
"$REQUESTS_ACTIVE_DIR" "$REQUESTS_ARCHIVE_DIR" \
"$ROADMAP_DIR" "$ROADMAP_ARCHIVE_DIR" \
"$TEMP_DIR"; do
short="${d#$TARGET_PATH/}"
if [[ -d "$d" ]]; then
ok "$short"
else
fail "$short (creation failed)"
fi
done
# --- phase 2: files ------------------------------------------------------
header "Files"
COPY_COUNT=0
SKIP_COUNT=0
FAIL_COUNT=0
mkdir -p "$SRC_DIR" "$RULES_DIR" "$ARCHIVE_DIR"
echo "Installing MetaAgent v$VERSION$SRC_DIR"
# --- copy files ---
copy_file() {
local src="$1" dst_dir="$2"
local name; name="$(basename "$src")"
local dst="$dst_dir/$name"
if [[ ! -f "$src" ]]; then
skip "$name (source not found)"
((SKIP_COUNT += 1))
echo " [skip] $name (not found)"
return
fi
if [[ "$UPDATE" == true ]] || [[ ! -f "$dst" ]]; then
if cp "$src" "$dst"; then
ok "$name"
((COPY_COUNT++))
else
fail "$name"
((FAIL_COUNT++))
fi
if [[ "$UPDATE" == true ]] || [[ ! -f "$dst_dir/$name" ]]; then
cp "$src" "$dst_dir/$name"
echo " [copy] $name"
else
skip "$name (exists, use --update to overwrite)"
((SKIP_COUNT += 1))
echo " [skip] $name (exists, use --update to overwrite)"
fi
}
copy_dir() {
local src="$1" dst_dir="$2"
local name; name="$(basename "$src")"
local dst="$dst_dir/$name"
if [[ ! -d "$src" ]]; then
skip "$name/ (source not found)"
((SKIP_COUNT += 1))
echo " [skip] $name/ (not found)"
return
fi
mkdir -p "$dst"
mkdir -p "$dst_dir/$name"
if [[ "$UPDATE" == true ]]; then
if cp -rf "$src"/* "$dst/" 2>/dev/null; then
ok "$name/"
((COPY_COUNT++))
else
fail "$name/ (partial copy)"
((FAIL_COUNT++))
fi
cp -rf "$src"/* "$dst_dir/$name/" 2>/dev/null || true
else
cp -rn "$src"/* "$dst/" 2>/dev/null || true
ok "$name/"
((COPY_COUNT++))
cp -rn "$src"/* "$dst_dir/$name/" 2>/dev/null || true
fi
echo " [copy] $name/"
}
copy_file "$METAAGENT_SRC/META_AGENT_GUIDE.md" "$SRC_DIR"
@@ -213,8 +95,8 @@ copy_dir "$METAAGENT_SRC/TEMPLATES" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.sh" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.ps1" "$SRC_DIR"
# --- phase 3: AGENTS.md --------------------------------------------------
header "AGENTS.md"
# --- create / update AGENTS.md in root of target ---
AGENTS_MD="$TARGET_PATH/AGENTS.md"
create_agents_md() {
cat > "$1" << AGENTS_EOF
@@ -258,32 +140,13 @@ AGENTS_EOF
if [[ ! -f "$AGENTS_MD" ]]; then
create_agents_md "$AGENTS_MD"
ok "AGENTS.md created"
echo " [create] AGENTS.md"
elif [[ "$UPDATE" == true ]]; then
create_agents_md "$AGENTS_MD"
ok "AGENTS.md updated"
echo " [update] AGENTS.md"
else
skip "AGENTS.md (exists, use --update to overwrite)"
((SKIP_COUNT += 1))
echo " [skip] AGENTS.md (exists, use --update to overwrite)"
fi
# --- summary -------------------------------------------------------------
header "Summary"
echo " MetaAgent v$VERSION$SRC_DIR"
echo ""
if (( COPY_COUNT > 0 )); then
ok "${COPY_COUNT} file(s) copied"
fi
if (( SKIP_COUNT > 0 )); then
skip "${SKIP_COUNT} file(s) skipped"
fi
if (( FAIL_COUNT > 0 )); then
fail "${FAIL_COUNT} file(s) failed"
fi
echo ""
if (( FAIL_COUNT == 0 )); then
ok "Installation completed successfully."
else
fail "Installation completed with ${FAIL_COUNT} error(s)."
exit 1
fi
echo "Done! MetaAgent v$VERSION installed at $SRC_DIR"
+143 -51
View File
@@ -1,79 +1,171 @@
{
"$schema": "metaagent-task-manifest",
"version": "1.0",
"session_id": "metaagent-003",
"goal": "i18n (ru/en) — инфраструктура и обёртка строк, контроль актуальности переводов",
"created_at": "2026-07-22T18:00:00Z",
"session_id": "metaagent-002",
"goal": "Обновление metaagent-артефактов до v1.0.0, валидация существующего кода и окружения",
"created_at": "2026-07-12T20:00:00Z",
"tasks": [
{
"id": "T9",
"title": "i18n инфраструктура (cli/i18n.py)",
"description": "Создан модуль cli/i18n.py с Translator, t(), set_lang(), setup_i18n(). Язык: CF_LANG (env), по умолчанию 'ru'. Русские переводы — полные, английский — скелет (fallback на ru).",
"type": "feature",
"files": ["cli/i18n.py"],
"id": "T1",
"title": "Инициализация проекта и зависимостей",
"description": "Создать структуру директорий, pyproject.toml, venv, установить зависимости",
"type": "config",
"files": [
"pyproject.toml",
"cashflow_model/__init__.py",
"sync/__init__.py",
"engine/__init__.py",
"ai/__init__.py",
"cli/__init__.py",
"data/.gitkeep",
"exports/.gitkeep"
],
"depends_on": [],
"acceptance_criteria": [
"t() возвращает русский текст при CF_LANG=ru",
"t() возвращает русский текст при CF_LANG=en (fallback)",
"t('nonexistent') возвращает 'nonexistent'",
"setup_i18n() читает CF_LANG из окружения"
"pyproject.toml создан с правильными зависимостями",
"Все директории модулей созданы с __init__.py",
"ruff lint проходит без ошибок",
"pytest запускается"
],
"status": "completed"
},
{
"id": "T10",
"title": "Обёртка CLI-строк в t()",
"description": "Все user-facing строки в cli/main.py и cli/config.py заменены на вызовы t('key', ...). Русский словарь содержит ~150 ключей.",
"type": "refactor",
"files": ["cli/main.py", "cli/config.py"],
"depends_on": ["T9"],
"id": "T2",
"title": "Модель данных (dataclass + JSON serialization)",
"description": "Реализовать все сущности: Account, Transaction, RecurringCashflow, Asset, Liability, ForecastScenario, FinancialModel",
"type": "feature",
"files": [
"cashflow_model/__init__.py",
"cashflow_model/account.py",
"cashflow_model/transaction.py",
"cashflow_model/recurring.py",
"cashflow_model/asset.py",
"cashflow_model/liability.py",
"cashflow_model/scenario.py",
"cashflow_model/model.py"
],
"depends_on": ["T1"],
"acceptance_criteria": [
"Все help-строки typer.Option/Argument через t()",
"Все console.print сообщения через t()",
"Все docstrings оставлены как комментарии (typer не использует)",
"ruff check проходит"
"Все сущности — dataclass с правильными полями и типами",
"FinancialModel корректно сохраняется и загружается из JSON",
"Создание Account, Transaction, Asset, Liability через конструктор работает"
],
"status": "completed"
},
{
"id": "T11",
"title": "AI-промпты через i18n",
"description": "prompt.analyze, prompt.advice, prompt.scenario_comparison добавлены в словарь i18n, prompts.py использует t().",
"type": "refactor",
"files": ["ai/prompts.py"],
"depends_on": ["T9"],
"id": "T3",
"title": "Forecast Engine (базовый прогноз)",
"description": "Реализовать ForecastService с методами forecast_cashflow, apply_recurring, project_balance",
"type": "feature",
"files": [
"engine/__init__.py",
"engine/forecast.py"
],
"depends_on": ["T2"],
"acceptance_criteria": [
"ANALYZE_PROMPT = t('prompt.analyze')",
"ADVICE_PROMPT = t('prompt.advice')",
"SCENARIO_COMPARISON_PROMPT = t('prompt.scenario_comparison')"
"forecast_cashflow(months=12) возвращает список помесячных балансов",
"Регулярные платежи корректно проецируются на будущие периоды",
"Активы учитываются с ростом (growth_rate)",
"Обязательства учитываются с процентами и платежами"
],
"status": "completed"
},
{
"id": "T12",
"title": "Тесты i18n",
"description": "test_i18n.py: базовые тесты Translator, t(), set_lang, fallback, неизвестный ключ.",
"id": "T4",
"title": "Scenario Analysis",
"description": "Реализовать ScenarioService с методами: сценарии, what-if, сравнение",
"type": "feature",
"files": [
"engine/__init__.py",
"engine/scenarios.py"
],
"depends_on": ["T3"],
"acceptance_criteria": [
"Три предустановленных сценария (baseline, optimistic, pessimistic)",
"What-if: изменение параметров (доход +10%, расход -5%)",
"Сравнение сценариев возвращает сводку различий"
],
"status": "completed"
},
{
"id": "T5",
"title": "Excel Sync (import/export)",
"description": "Реализовать ExcelSync: чтение модели из .xlsx, запись результатов прогноза в .xlsx",
"type": "feature",
"files": [
"sync/__init__.py",
"sync/excel_sync.py"
],
"depends_on": ["T2"],
"acceptance_criteria": [
"Импорт из Excel заполняет FinancialModel",
"Экспорт FinancialModel в Excel создаёт корректный .xlsx",
"Обработка ошибок при невалидном формате Excel"
],
"status": "completed"
},
{
"id": "T6",
"title": "CLI (Typer) — все команды",
"description": "Реализовать CLI через Typer с командами: init, import, export, forecast, scenario, analyze, whatif, compare",
"type": "feature",
"files": [
"cli/__init__.py",
"cli/main.py",
"pyproject.toml"
],
"depends_on": ["T2", "T3", "T4", "T5", "T7"],
"acceptance_criteria": [
"Команда 'cf init' создаёт пустую модель и JSON",
"Команда 'cf forecast --months 12' выводит таблицу прогноза",
"Команда 'cf analyze' вызывает AI Assistant",
"Команда 'cf import' и 'cf export' работают с Excel",
"Команда 'cf scenario' применяет и выводит сценарий",
"Команда 'cf whatif' выполняет what-if анализ",
"Команда 'cf compare' сравнивает сценарии"
],
"status": "completed"
},
{
"id": "T7",
"title": "AI Assistant (промпты + интерфейс)",
"description": "Реализовать AssistantService: генерация промптов, заглушка для вызова AI API",
"type": "feature",
"files": [
"ai/__init__.py",
"ai/prompts.py",
"ai/assistant.py"
],
"depends_on": ["T3"],
"acceptance_criteria": [
"Промпт 'analyze' включает модель и прогноз в JSON",
"Промпт 'advice' формирует запрос на финансовые рекомендации",
"AssistantService возвращает структурированный ответ (заглушка)"
],
"status": "completed"
},
{
"id": "T8",
"title": "Тесты на все модули",
"description": "Написать pytest-тесты для всех модулей",
"type": "test",
"files": ["tests/test_i18n.py"],
"depends_on": ["T9"],
"files": [
"tests/test_model.py",
"tests/test_forecast.py",
"tests/test_scenarios.py",
"tests/test_excel_sync.py",
"tests/test_cli.py",
"tests/test_ai.py",
"tests/conftest.py"
],
"depends_on": ["T2", "T3", "T4", "T5", "T6", "T7"],
"acceptance_criteria": [
"pytest tests/test_i18n.py проходит",
"Покрытие: ru default, en fallback, неизвестный ключ, format args"
"pytest запускается и все тесты проходят",
"Покрытие базовых сценариев для каждой сущности",
"Roundtrip-тест Excel: export → import → compare",
"Forecast-тест: известные входные данные → ожидаемый результат"
],
"status": "completed"
},
{
"id": "T13",
"title": "Аудит и контроль актуальности переводов",
"description": "Периодическая проверка: все ли ключи из TRANSLATIONS['ru'] имеют соответствующий перевод в TRANSLATIONS['en']. При добавлении новых фич — новые ключи должны добавляться в оба словаря.",
"type": "audit",
"files": ["cli/i18n.py"],
"depends_on": ["T9"],
"acceptance_criteria": [
"Все ru-ключи имеют en-перевод или fallback",
"При добавлении нового t('key') он регистрируется в _r()"
],
"status": "pending"
}
]
}
+18 -18
View File
@@ -1,35 +1,35 @@
# MetaAgent
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v2.1.0 —
Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v1.1.0 —
набор инструкций для AI-агента.
## Контекст MetaAgent
| Ресурс | Путь |
|--------|------|
| Главная инструкция | .agent/src/META_AGENT_GUIDE.md |
| Протоколы фаз | .agent/src/PROTOCOLS/ |
| Шаблоны артефактов | .agent/src/TEMPLATES/ |
| Границы (что разрешено/запрещено) | .agent/src/BOUNDARIES.md |
| Правила проекта | .agent/rules/project-rules.md |
| Примеры работы | .agent/src/WORKFLOW.md |
| Версия | .agent/src/VERSION |
| Главная инструкция | `.agent/src/META_AGENT_GUIDE.md` |
| Протоколы фаз | `.agent/src/PROTOCOLS/` |
| Шаблоны артефактов | `.agent/src/TEMPLATES/` |
| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` |
| Правила проекта | `.agent/rules/project-rules.md` |
| Примеры работы | `.agent/src/WORKFLOW.md` |
| Версия | `.agent/src/VERSION` |
## Состояние сессии (если инициализировано)
| Артефакт | Путь |
|----------|------|
| Чекпоинты сессии | .agent/checkpoints.json |
| Манифест задач | .agent/task-manifest.json |
| Сводка для exec-агента | .agent/handoff-summary.md |
| Анализ репозитория | .agent/analysis-report.md |
| Чекпоинты сессии | `.agent/checkpoints.json` |
| Манифест задач | `.agent/task-manifest.json` |
| Сводка для exec-агента | `.agent/handoff-summary.md` |
| Анализ репозитория | `.agent/analysis-report.md` |
## Для исполнительного агента
1. **Прочитай** .agent/src/META_AGENT_GUIDE.md — пойми жизненный цикл MetaAgent.
2. **Прочитай** .agent/src/BOUNDARIES.md — соблюдай границы.
3. **Прочитай** .agent/rules/project-rules.md — выполни пользовательские правила.
4. **Проверь** .agent/checkpoints.json — если существует, используй как состояние сессии.
5. **Проверь** .agent/task-manifest.json — если существует, выполняй задачи по порядку.
6. Если .agent/ не инициализирован или устарел — запусти install.sh --update для
1. **Прочитай** `.agent/src/META_AGENT_GUIDE.md` — пойми жизненный цикл MetaAgent.
2. **Прочитай** `.agent/src/BOUNDARIES.md` — соблюдай границы.
3. **Прочитай** `.agent/rules/project-rules.md` — выполни пользовательские правила.
4. **Проверь** `.agent/checkpoints.json` — если существует, используй как состояние сессии.
5. **Проверь** `.agent/task-manifest.json` — если существует, выполняй задачи по порядку.
6. Если `.agent/` не инициализирован или устарел — запусти `install.sh --update` для
обновления исходников MetaAgent до актуальной версии.
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Jor Oqyude
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -15
View File
@@ -1,7 +1,7 @@
import json
from ai import prompts
from cashflow_model import CurrencyConverter, FinancialModel
from cashflow_model import FinancialModel
from engine.forecast import ForecastService
@@ -10,15 +10,8 @@ class AssistantError(Exception):
class AssistantService:
def __init__(
self,
model: FinancialModel,
converter: CurrencyConverter | None = None,
display_currency: str | None = None,
):
def __init__(self, model: FinancialModel):
self.model = model
self.converter = converter or CurrencyConverter(model.exchange_rates)
self.display_currency = display_currency or model.base_currency
def analyze(self, months: int = 12) -> dict:
forecast_service = ForecastService(self.model)
@@ -29,8 +22,6 @@ class AssistantService:
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False),
months=months,
base_currency=self.model.base_currency,
display_currency=self.display_currency,
)
return {
@@ -48,8 +39,6 @@ class AssistantService:
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False),
question=question,
base_currency=self.model.base_currency,
display_currency=self.display_currency,
)
return {
@@ -60,8 +49,6 @@ class AssistantService:
def compare_scenarios(self, scenarios_json: str) -> dict:
prompt = prompts.SCENARIO_COMPARISON_PROMPT.format(
scenarios_json=scenarios_json,
base_currency=self.model.base_currency,
display_currency=self.display_currency,
)
return {
"prompt": prompt,
+38 -13
View File
@@ -1,21 +1,46 @@
from cli.i18n import t
ANALYZE_PROMPT = """
Ты финансовый AI-ассистент. Проанализируй финансовую модель пользователя.
ANALYZE_PROMPT = t("prompt.analyze")
ADVICE_PROMPT = t("prompt.advice")
SCENARIO_COMPARISON_PROMPT = t("prompt.scenario_comparison")
### Модель (JSON):
{model_json}
### Прогноз на {months} месяцев:
{forecast_json}
Дай анализ по пунктам:
1. Общее финансовое состояние
2. Тренд денежного потока (рост/падение)
3. Достаточность ликвидности
4. Рекомендации по улучшению
"""
ADVICE_PROMPT = """
Ты финансовый AI-ассистент. Дай персональные рекомендации.
### Модель:
{model_json}
### Прогноз:
{forecast_json}
Вопрос пользователя: {question}
Ответь как опытный финансовый консультант.
"""
SCENARIO_COMPARISON_PROMPT = """
Ты финансовый AI-ассистент. Сравни сценарии прогноза.
### Результаты сценариев:
{scenarios_json}
Дай рекомендацию: какой сценарий наиболее вероятен и почему.
"""
def format_context(
model_json: str,
forecast_json: str,
months: int = 12,
base_currency: str = "RUB",
display_currency: str = "RUB",
) -> str:
def format_context(model_json: str, forecast_json: str, months: int = 12) -> str:
return ANALYZE_PROMPT.format(
model_json=model_json,
forecast_json=forecast_json,
months=months,
base_currency=base_currency,
display_currency=display_currency,
)
-5
View File
@@ -1,6 +1,5 @@
from cashflow_model.account import Account
from cashflow_model.asset import Asset
from cashflow_model.currency import CURRENCY_SYMBOLS, CurrencyConverter, CurrencyError, ExchangeRate
from cashflow_model.liability import Liability
from cashflow_model.model import FinancialModel
from cashflow_model.recurring import RecurringCashflow
@@ -15,8 +14,4 @@ __all__ = [
"Liability",
"ForecastScenario",
"FinancialModel",
"ExchangeRate",
"CurrencyConverter",
"CurrencyError",
"CURRENCY_SYMBOLS",
]
-79
View File
@@ -1,79 +0,0 @@
from dataclasses import dataclass
CURRENCY_SYMBOLS = {
"RUB": "",
"USD": "$",
"EUR": "",
"GBP": "£",
"CNY": "¥",
"JPY": "¥",
"KZT": "",
"UAH": "",
}
@dataclass
class ExchangeRate:
from_currency: str = "USD"
to_currency: str = "RUB"
rate: float = 80.0
def to_dict(self) -> dict:
return {
"from_currency": self.from_currency,
"to_currency": self.to_currency,
"rate": self.rate,
}
@classmethod
def from_dict(cls, data: dict) -> "ExchangeRate":
return cls(
from_currency=data.get("from_currency", "USD"),
to_currency=data.get("to_currency", "RUB"),
rate=data.get("rate", 80.0),
)
DEFAULT_RATES: list[ExchangeRate] = [
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
]
class CurrencyError(Exception):
pass
class CurrencyConverter:
def __init__(self, rates: list[ExchangeRate] | None = None):
self._rates: dict[tuple[str, str], float] = {}
if rates:
for r in rates:
self.set_rate(r.from_currency, r.to_currency, r.rate)
def set_rate(self, from_currency: str, to_currency: str, rate: float) -> None:
if rate <= 0:
raise CurrencyError(f"Rate must be positive: {rate}")
self._rates[(from_currency, to_currency)] = rate
inverse = 1.0 / rate
self._rates[(to_currency, from_currency)] = inverse
def get_rate(self, from_currency: str, to_currency: str) -> float:
if from_currency == to_currency:
return 1.0
try:
return self._rates[(from_currency, to_currency)]
except KeyError:
raise CurrencyError(f"No exchange rate: {from_currency}{to_currency}")
def convert(self, amount: float, from_currency: str, to_currency: str) -> float:
if from_currency == to_currency:
return amount
rate = self.get_rate(from_currency, to_currency)
return round(amount * rate, 2)
def get_symbol(self, currency: str) -> str:
return CURRENCY_SYMBOLS.get(currency, currency)
@classmethod
def with_defaults(cls) -> "CurrencyConverter":
return cls(DEFAULT_RATES)
-7
View File
@@ -4,7 +4,6 @@ from pathlib import Path
from cashflow_model.account import Account
from cashflow_model.asset import Asset
from cashflow_model.currency import DEFAULT_RATES, ExchangeRate
from cashflow_model.liability import Liability
from cashflow_model.recurring import RecurringCashflow
from cashflow_model.scenario import ForecastScenario
@@ -13,38 +12,32 @@ from cashflow_model.transaction import Transaction
@dataclass
class FinancialModel:
base_currency: str = "RUB"
accounts: list[Account] = field(default_factory=list)
transactions: list[Transaction] = field(default_factory=list)
recurring: list[RecurringCashflow] = field(default_factory=list)
assets: list[Asset] = field(default_factory=list)
liabilities: list[Liability] = field(default_factory=list)
scenarios: list[ForecastScenario] = field(default_factory=list)
exchange_rates: list[ExchangeRate] = field(default_factory=lambda: DEFAULT_RATES.copy())
def to_dict(self) -> dict:
return {
"base_currency": self.base_currency,
"accounts": [a.to_dict() for a in self.accounts],
"transactions": [t.to_dict() for t in self.transactions],
"recurring": [r.to_dict() for r in self.recurring],
"assets": [a.to_dict() for a in self.assets],
"liabilities": [li.to_dict() for li in self.liabilities],
"scenarios": [s.to_dict() for s in self.scenarios],
"exchange_rates": [r.to_dict() for r in self.exchange_rates],
}
@classmethod
def from_dict(cls, data: dict) -> "FinancialModel":
return cls(
base_currency=data.get("base_currency", "RUB"),
accounts=[Account.from_dict(a) for a in data.get("accounts", [])],
transactions=[Transaction.from_dict(t) for t in data.get("transactions", [])],
recurring=[RecurringCashflow.from_dict(r) for r in data.get("recurring", [])],
assets=[Asset.from_dict(a) for a in data.get("assets", [])],
liabilities=[Liability.from_dict(li) for li in data.get("liabilities", [])],
scenarios=[ForecastScenario.from_dict(s) for s in data.get("scenarios", [])],
exchange_rates=[ExchangeRate.from_dict(r) for r in data.get("exchange_rates", [])],
)
def save(self, path: str | Path) -> None:
-428
View File
@@ -1,428 +0,0 @@
from pathlib import Path
import typer
from rich.console import Console
from rich.table import Table
from cashflow_model import (
Account,
Asset,
ExchangeRate,
FinancialModel,
Liability,
RecurringCashflow,
Transaction,
)
from cli.i18n import t
app = typer.Typer(name="config", help=t("config.help"))
console = Console()
DATA_DIR = Path("data")
MODEL_PATH = DATA_DIR / "model.json"
def _load_model() -> FinancialModel:
if MODEL_PATH.exists():
return FinancialModel.load(MODEL_PATH)
return FinancialModel()
def _save_model(model: FinancialModel) -> None:
model.save(MODEL_PATH)
# ── base-currency ──────────────────────────────────────────────
@app.command()
def base_currency(
currency: str = typer.Argument(None, help=t("cmd.config.base_currency.arg")),
) -> None:
"""Показать или установить базовую валюту модели"""
model = _load_model()
if currency:
model.base_currency = currency.upper()
_save_model(model)
ok = t("global.ok")
msg = t("cmd.config.base_currency.ok", currency=model.base_currency)
console.print(f"[green]{ok}[/green] {msg}")
else:
console.print(t("cmd.config.base_currency.show", currency=f"[bold]{model.base_currency}[/bold]"))
# ── rate ───────────────────────────────────────────────────────
@app.command()
def rate_set(
from_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.from")),
to_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.to")),
rate: float = typer.Argument(..., help=t("cmd.config.rate_set.arg.rate")),
) -> None:
"""Добавить или обновить курс валюты"""
model = _load_model()
fc, tc = from_currency.upper(), to_currency.upper()
existing = [r for r in model.exchange_rates if r.from_currency == fc and r.to_currency == tc]
if existing:
existing[0].rate = rate
else:
model.exchange_rates.append(ExchangeRate(
from_currency=from_currency.upper(),
to_currency=to_currency.upper(),
rate=rate,
))
_save_model(model)
fc, tc = from_currency.upper(), to_currency.upper()
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.rate_set.ok', fc=fc, tc=tc, rate=rate)}")
@app.command(name="rate-list")
def rate_list() -> None:
"""Список курсов валют"""
model = _load_model()
if not model.exchange_rates:
console.print(f"[yellow]{t('cmd.config.rate_list.empty')}[/yellow]")
return
table = Table(title=t("table.rates.title"))
table.add_column(t("table.rates.col.from"), style="cyan")
table.add_column(t("table.rates.col.to"), style="cyan")
table.add_column(t("table.rates.col.rate"), justify="right")
for r in model.exchange_rates:
table.add_row(r.from_currency, r.to_currency, str(r.rate))
console.print(table)
@app.command(name="rate-remove")
def rate_remove(
from_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.from")),
to_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.to")),
) -> None:
"""Удалить курс валюты"""
model = _load_model()
fc, tc = from_currency.upper(), to_currency.upper()
initial = len(model.exchange_rates)
model.exchange_rates = [
r for r in model.exchange_rates
if not (r.from_currency == fc and r.to_currency == tc)
]
if len(model.exchange_rates) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.rate_remove.ok', fc=fc, tc=tc)}")
else:
console.print(f"[red]{t('cmd.config.rate_remove.err', fc=fc, tc=tc)}[/red]")
raise typer.Exit(1)
# ── account ────────────────────────────────────────────────────
@app.command()
def account_add(
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.account_add.opt.name")),
balance: float = typer.Option(0.0, "--balance", "-b", help=t("cmd.config.account_add.opt.balance")),
currency: str = typer.Option("", "--currency", "-c", help=t("cmd.config.account_add.opt.currency")),
) -> None:
"""Добавить счёт"""
model = _load_model()
curr = currency.upper() if currency else model.base_currency
account = Account(name=name, balance=balance, currency=curr)
model.accounts.append(account)
_save_model(model)
ok = t("global.ok")
msg = t("cmd.config.account_add.ok", name=name, balance=balance, currency=curr)
console.print(f"[green]{ok}[/green] {msg}")
@app.command(name="account-list")
def account_list() -> None:
"""Список счетов"""
model = _load_model()
if not model.accounts:
console.print(f"[yellow]{t('cmd.config.account_list.empty')}[/yellow]")
return
table = Table(title=t("table.accounts_config.title"))
table.add_column(t("table.accounts_config.col.id"), style="dim")
table.add_column(t("table.accounts_config.col.name"), style="cyan")
table.add_column(t("table.accounts_config.col.currency"))
table.add_column(t("table.accounts_config.col.balance"), justify="right")
for a in model.accounts:
table.add_row(str(a.id)[:8], a.name, a.currency, f"{a.balance:,.2f}")
console.print(table)
@app.command(name="account-remove")
def account_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.account_remove.arg")),
) -> None:
"""Удалить счёт по ID или названию"""
model = _load_model()
initial = len(model.accounts)
model.accounts = [
a for a in model.accounts
if str(a.id) != identifier and a.name != identifier
]
if len(model.accounts) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.account_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.account_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── transaction ────────────────────────────────────────────────
@app.command()
def transaction_add(
account: str = typer.Option(..., "--account", "-a", help=t("cmd.config.transaction_add.opt.account")),
amount: float = typer.Option(..., "--amount", "-m", help=t("cmd.config.transaction_add.opt.amount")),
category: str = typer.Option("", "--category", "-c", help=t("cmd.config.transaction_add.opt.category")),
description: str = typer.Option("", "--description", "-d", help=t("cmd.config.transaction_add.opt.description")),
date: str = typer.Option("", "--date", "-D", help=t("cmd.config.transaction_add.opt.date")),
) -> None:
"""Добавить транзакцию"""
model = _load_model()
account_id = _resolve_account_id(model, account)
if not account_id:
console.print(f"[red]{t('cmd.config.transaction_add.err', account=account)}[/red]")
raise typer.Exit(1)
txn = Transaction(
date=date or "",
account=account_id,
category=category,
amount=amount,
description=description,
)
model.transactions.append(txn)
_save_model(model)
kind = t("cmd.config.transaction_add.kind.income") if amount >= 0 else t("cmd.config.transaction_add.kind.expense")
ok = t("global.ok")
msg = t("cmd.config.transaction_add.ok", kind=kind, amount=abs(amount), account=account)
console.print(f"[green]{ok}[/green] {msg}")
@app.command(name="transaction-list")
def transaction_list() -> None:
"""Список транзакций"""
model = _load_model()
if not model.transactions:
console.print(f"[yellow]{t('cmd.config.transaction_list.empty')}[/yellow]")
return
table = Table(title=t("table.transactions.title"))
table.add_column(t("table.accounts_config.col.id"), style="dim")
table.add_column(t("table.transactions.col.date"))
table.add_column(t("table.accounts_config.col.name"))
table.add_column(t("table.transactions.col.category"))
table.add_column(t("table.transactions.col.amount"), justify="right")
table.add_column(t("table.transactions.col.description"))
for tx in model.transactions:
account_name = _account_name(model, tx.account)
table.add_row(
str(tx.id)[:8], tx.date, account_name, tx.category,
f"{tx.amount:,.2f}", tx.description,
)
console.print(table)
@app.command(name="transaction-remove")
def transaction_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.transaction_remove.arg")),
) -> None:
"""Удалить транзакцию по ID"""
model = _load_model()
initial = len(model.transactions)
model.transactions = [t for t in model.transactions if str(t.id) != identifier]
if len(model.transactions) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.transaction_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.transaction_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── recurring ──────────────────────────────────────────────────
@app.command()
def recurring_add(
amount: float = typer.Option(..., "--amount", "-m", help=t("cmd.config.recurring_add.opt.amount")),
category: str = typer.Option("", "--category", "-c", help=t("cmd.config.recurring_add.opt.category")),
frequency: str = typer.Option("monthly", "--frequency", "-f", help=t("cmd.config.recurring_add.opt.frequency")),
start_date: str = typer.Option("", "--start", "-s", help=t("cmd.config.recurring_add.opt.start")),
end_date: str = typer.Option("", "--end", "-e", help=t("cmd.config.recurring_add.opt.end")),
) -> None:
"""Добавить регулярный платёж"""
model = _load_model()
rc = RecurringCashflow(
start_date=start_date or "", end_date=end_date or "",
frequency=frequency, amount=amount, category=category,
)
model.recurring.append(rc)
_save_model(model)
ok = t("global.ok")
msg = t("cmd.config.recurring_add.ok", amount=abs(amount), frequency=frequency)
console.print(f"[green]{ok}[/green] {msg}")
@app.command(name="recurring-list")
def recurring_list() -> None:
"""Список регулярных платежей"""
model = _load_model()
if not model.recurring:
console.print(f"[yellow]{t('cmd.config.recurring_list.empty')}[/yellow]")
return
table = Table(title=t("table.recurring.title"))
table.add_column(t("table.recurring.col.id"), style="dim")
table.add_column(t("table.recurring.col.start"))
table.add_column(t("table.recurring.col.end"))
table.add_column(t("table.recurring.col.period"))
table.add_column(t("table.recurring.col.amount"), justify="right")
table.add_column(t("table.recurring.col.category"))
for r in model.recurring:
row = (str(r.id)[:8], r.start_date, r.end_date, r.frequency, f"{r.amount:,.2f}", r.category)
table.add_row(*row)
console.print(table)
@app.command(name="recurring-remove")
def recurring_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.recurring_remove.arg")),
) -> None:
"""Удалить регулярный платёж по ID"""
model = _load_model()
initial = len(model.recurring)
model.recurring = [r for r in model.recurring if str(r.id) != identifier]
if len(model.recurring) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.recurring_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.recurring_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── asset ──────────────────────────────────────────────────────
@app.command()
def asset_add(
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.asset_add.opt.name")),
value: float = typer.Option(0.0, "--value", "-v", help=t("cmd.config.asset_add.opt.value")),
growth_rate: float = typer.Option(0.0, "--growth", "-g", help=t("cmd.config.asset_add.opt.growth")),
) -> None:
"""Добавить актив"""
model = _load_model()
asset = Asset(name=name, value=value, growth_rate=growth_rate)
model.assets.append(asset)
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.asset_add.ok', name=name, value=value)}")
@app.command(name="asset-list")
def asset_list() -> None:
"""Список активов"""
model = _load_model()
if not model.assets:
console.print(f"[yellow]{t('cmd.config.asset_list.empty')}[/yellow]")
return
table = Table(title=t("table.assets.title"))
table.add_column(t("table.assets.col.id"), style="dim")
table.add_column(t("table.assets.col.name"), style="cyan")
table.add_column(t("table.assets.col.value"), justify="right")
table.add_column(t("table.assets.col.growth"), justify="right")
for a in model.assets:
table.add_row(str(a.id)[:8], a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
console.print(table)
@app.command(name="asset-remove")
def asset_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.asset_remove.arg")),
) -> None:
"""Удалить актив по ID или названию"""
model = _load_model()
initial = len(model.assets)
model.assets = [
a for a in model.assets
if str(a.id) != identifier and a.name != identifier
]
if len(model.assets) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.asset_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.asset_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── liability ──────────────────────────────────────────────────
@app.command()
def liability_add(
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.liability_add.opt.name")),
balance: float = typer.Option(0.0, "--balance", "-b", help=t("cmd.config.liability_add.opt.balance")),
interest: float = typer.Option(0.0, "--interest", "-i", help=t("cmd.config.liability_add.opt.interest")),
payment: float = typer.Option(0.0, "--payment", "-p", help=t("cmd.config.liability_add.opt.payment")),
) -> None:
"""Добавить обязательство"""
model = _load_model()
liability = Liability(name=name, balance=balance, interest=interest, payment=payment)
model.liabilities.append(liability)
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.liability_add.ok', name=name, balance=balance)}")
@app.command(name="liability-list")
def liability_list() -> None:
"""Список обязательств"""
model = _load_model()
if not model.liabilities:
console.print(f"[yellow]{t('cmd.config.liability_list.empty')}[/yellow]")
return
table = Table(title=t("table.liabilities.title"))
table.add_column(t("table.liabilities.col.id"), style="dim")
table.add_column(t("table.liabilities.col.name"), style="cyan")
table.add_column(t("table.liabilities.col.balance"), justify="right")
table.add_column(t("table.liabilities.col.rate"), justify="right")
table.add_column(t("table.liabilities.col.payment"), justify="right")
for li in model.liabilities:
b, i, p = f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}"
row = (str(li.id)[:8], li.name, b, i, p)
table.add_row(*row)
console.print(table)
@app.command(name="liability-remove")
def liability_remove(
identifier: str = typer.Argument(..., help=t("cmd.config.liability_remove.arg")),
) -> None:
"""Удалить обязательство по ID или названию"""
model = _load_model()
initial = len(model.liabilities)
model.liabilities = [
li for li in model.liabilities
if str(li.id) != identifier and li.name != identifier
]
if len(model.liabilities) < initial:
_save_model(model)
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.liability_remove.ok', id=identifier)}")
else:
console.print(f"[red]{t('cmd.config.liability_remove.err', id=identifier)}[/red]")
raise typer.Exit(1)
# ── helpers ────────────────────────────────────────────────────
def _resolve_account_id(model: FinancialModel, identifier: str) -> str | None:
for a in model.accounts:
if str(a.id) == identifier or a.name == identifier:
return str(a.id)
return None
def _account_name(model: FinancialModel, account_id: str) -> str:
for a in model.accounts:
if str(a.id) == account_id:
return a.name
return account_id[:8]
-350
View File
@@ -1,350 +0,0 @@
import os
_TRANSLATIONS: dict[str, dict[str, str]] = {}
def _r(key: str, text: str) -> str:
_TRANSLATIONS.setdefault("ru", {})[key] = text
return text
def _e(key: str, text: str) -> str:
_TRANSLATIONS.setdefault("en", {})[key] = text
return text
# ── app ────────────────────────────────────────────────────────
_r("app.help", "CashFlow Forecast — личная финансовая модель")
# ── cmd.init ───────────────────────────────────────────────────
_r("cmd.init.help", "Создать пустую финансовую модель")
_r("cmd.init.ok", "Пустая модель создана в {path}")
# ── cmd.forecast ───────────────────────────────────────────────
_r("cmd.forecast.help", "Запустить прогноз денежных потоков")
_r("cmd.forecast.opt.months", "Количество месяцев прогноза")
_r("cmd.forecast.opt.currency", "Валюта отображения (по умолчанию — базовая валюта модели)")
_r("cmd.forecast.title", "Прогноз на {months} мес. ({currency})")
_r("cmd.forecast.col.account", "Счёт")
_r("cmd.forecast.col.month", "Месяц")
_r("cmd.forecast.col.balance", "Баланс")
_r("cmd.forecast.col.income", "Доход")
_r("cmd.forecast.col.expenses", "Расход")
_r("cmd.forecast.total", "Итог: Баланс: {balance} | Доход: {income} | Расход: {expenses}")
# ── cmd.scenario ──────────────────────────────────────────────
_r("cmd.scenario.help", "Применить сценарий и показать прогноз")
_r("cmd.scenario.arg.name", "Имя сценария: baseline, optimistic, pessimistic")
_r("cmd.scenario.opt.months", "Количество месяцев")
_r("cmd.scenario.err.unknown", "Неизвестный сценарий: {name}")
_r("cmd.scenario.err.available", "Доступны: {scenarios}")
_r("cmd.scenario.label", "Сценарий")
_r("cmd.scenario.label.balance", "Баланс")
_r("cmd.scenario.label.income", "Доход")
_r("cmd.scenario.label.expenses", "Расход")
# ── cmd.whatif ────────────────────────────────────────────────
_r("cmd.whatif.help", "What-if анализ с произвольными множителями")
_r("cmd.whatif.opt.income", "Множитель дохода")
_r("cmd.whatif.opt.expense", "Множитель расхода")
_r("cmd.whatif.opt.growth", "Множитель роста активов")
_r("cmd.whatif.opt.months", "Количество месяцев")
_r("cmd.whatif.title", "What-if анализ")
_r("cmd.whatif.params", "Доход x{income} | Расход x{expense} | Рост x{growth}")
# ── cmd.compare ───────────────────────────────────────────────
_r("cmd.compare.help", "Сравнить все сценарии")
_r("cmd.compare.opt.months", "Количество месяцев")
_r("cmd.compare.title", "Сравнение сценариев ({currency})")
_r("cmd.compare.col.scenario", "Сценарий")
# ── cmd.import ────────────────────────────────────────────────
_r("cmd.import.help", "Импорт данных из Excel")
_r("cmd.import.arg.path", "Путь к .xlsx файлу")
_r("cmd.import.ok",
"Импортировано: {accounts} счетов, {transactions} транзакций, "
"{recurring} регулярных платежей, {assets} активов, {liabilities} обязательств")
_r("cmd.import.base_currency", "Базовая валюта: {currency}")
_r("cmd.import.err", "Ошибка импорта: {error}")
# ── cmd.export ────────────────────────────────────────────────
_r("cmd.export.help", "Экспорт модели в Excel")
_r("cmd.export.arg.path", "Путь для .xlsx файла")
_r("cmd.export.ok", "Модель экспортирована в {path}")
_r("cmd.export.err", "Ошибка экспорта: {error}")
# ── cmd.analyze ───────────────────────────────────────────────
_r("cmd.analyze.help", "AI-анализ финансовой модели")
_r("cmd.analyze.opt.months", "Количество месяцев для анализа")
_r("cmd.analyze.title.prompt", "Промпт для AI")
_r("cmd.analyze.title.summary", "Сводка")
_r("cmd.analyze.base_currency", "Базовая валюта: {currency}")
_r("cmd.analyze.stub", "AI-ответ: заглушка. Подключите реальный API в ai/assistant.py")
# ── cmd.info ──────────────────────────────────────────────────
_r("cmd.info.help", "Сводка всей финансовой модели")
_r("cmd.info.metadata", "Метаданные")
_r("cmd.info.base_currency", "Базовая валюта: {currency} ({symbol})")
_r("cmd.info.accounts", "Счетов: {count}")
_r("cmd.info.transactions", "Транзакций: {count}")
_r("cmd.info.recurring", "Регулярных платежей: {count}")
_r("cmd.info.assets", "Активов: {count}")
_r("cmd.info.liabilities", "Обязательств: {count}")
_r("cmd.info.rates", "Курсов валют: {count}")
_r("cmd.info.more", "... и ещё {count} транзакций")
# ── table headers ─────────────────────────────────────────────
_r("table.accounts.title", "Счета")
_r("table.accounts_config.title", "Счета")
_r("table.accounts.col.name", "Название")
_r("table.accounts.col.currency", "Валюта")
_r("table.accounts.col.balance", "Баланс")
_r("table.transactions.title", "Транзакции")
_r("table.transactions.col.date", "Дата")
_r("table.transactions.col.category", "Категория")
_r("table.transactions.col.amount", "Сумма")
_r("table.transactions.col.description", "Описание")
_r("table.recurring.title", "Регулярные платежи")
_r("table.recurring.col.period", "Период")
_r("table.recurring.col.amount", "Сумма")
_r("table.recurring.col.category", "Категория")
_r("table.recurring.col.id", "ID")
_r("table.recurring.col.start", "Начало")
_r("table.recurring.col.end", "Конец")
_r("table.assets.title", "Активы")
_r("table.assets.col.id", "ID")
_r("table.assets.col.name", "Название")
_r("table.assets.col.value", "Стоимость")
_r("table.assets.col.growth", "Рост (%)")
_r("table.liabilities.title", "Обязательства")
_r("table.liabilities.col.id", "ID")
_r("table.liabilities.col.name", "Название")
_r("table.liabilities.col.balance", "Долг")
_r("table.liabilities.col.rate", "Ставка")
_r("table.liabilities.col.payment", "Платёж")
_r("table.rates.title", "Курсы валют")
_r("table.rates.col.from", "Из")
_r("table.rates.col.to", "В")
_r("table.rates.col.rate", "Курс")
_r("table.accounts_config.col.id", "ID")
_r("table.accounts_config.col.name", "Название")
_r("table.accounts_config.col.currency", "Валюта")
_r("table.accounts_config.col.balance", "Баланс")
# ── config ────────────────────────────────────────────────────
_r("config.help", "Управление моделью (счета, транзакции, валюты...)")
_r("cmd.config.base_currency.help", "Показать или установить базовую валюту модели")
_r("cmd.config.base_currency.arg", "Код валюты (RUB, USD, EUR...)")
_r("cmd.config.base_currency.ok", "Базовая валюта установлена: {currency}")
_r("cmd.config.base_currency.show", "Базовая валюта: {currency}")
_r("cmd.config.rate_set.help", "Добавить или обновить курс валюты")
_r("cmd.config.rate_set.arg.from", "Из валюты")
_r("cmd.config.rate_set.arg.to", "В валюту")
_r("cmd.config.rate_set.arg.rate", "Курс")
_r("cmd.config.rate_set.ok", "Курс {fc}{tc} = {rate}")
_r("cmd.config.rate_list.help", "Список курсов валют")
_r("cmd.config.rate_list.empty", "Курсы не заданы")
_r("cmd.config.rate_remove.help", "Удалить курс валюты")
_r("cmd.config.rate_remove.ok", "Курс {fc}{tc} удалён")
_r("cmd.config.rate_remove.err", "Курс {fc}{tc} не найден")
_r("cmd.config.account_add.help", "Добавить счёт")
_r("cmd.config.account_add.opt.name", "Название счёта")
_r("cmd.config.account_add.opt.balance", "Начальный баланс")
_r("cmd.config.account_add.opt.currency", "Валюта счёта")
_r("cmd.config.account_add.ok", "Счёт '{name}' добавлен (баланс: {balance}, валюта: {currency})")
_r("cmd.config.account_list.help", "Список счетов")
_r("cmd.config.account_list.empty", "Счета не добавлены")
_r("cmd.config.account_remove.help", "Удалить счёт по ID или названию")
_r("cmd.config.account_remove.arg", "ID или название счёта")
_r("cmd.config.account_remove.ok", "Счёт '{id}' удалён")
_r("cmd.config.account_remove.err", "Счёт '{id}' не найден")
_r("cmd.config.transaction_add.help", "Добавить транзакцию")
_r("cmd.config.transaction_add.opt.account", "ID или название счёта")
_r("cmd.config.transaction_add.opt.amount", "Сумма (доход/расход)")
_r("cmd.config.transaction_add.opt.category", "Категория")
_r("cmd.config.transaction_add.opt.description", "Описание")
_r("cmd.config.transaction_add.opt.date", "Дата (YYYY-MM-DD)")
_r("cmd.config.transaction_add.ok", "Транзакция '{kind}' на {amount:.2f} (счёт: {account})")
_r("cmd.config.transaction_add.kind.income", "доход")
_r("cmd.config.transaction_add.kind.expense", "расход")
_r("cmd.config.transaction_add.err", "Счёт '{account}' не найден")
_r("cmd.config.transaction_list.help", "Список транзакций")
_r("cmd.config.transaction_list.empty", "Транзакции не добавлены")
_r("cmd.config.transaction_remove.help", "Удалить транзакцию по ID")
_r("cmd.config.transaction_remove.arg", "ID транзакции")
_r("cmd.config.transaction_remove.ok", "Транзакция '{id}' удалена")
_r("cmd.config.transaction_remove.err", "Транзакция '{id}' не найдена")
_r("cmd.config.recurring_add.help", "Добавить регулярный платёж")
_r("cmd.config.recurring_add.opt.amount", "Сумма")
_r("cmd.config.recurring_add.opt.category", "Категория")
_r("cmd.config.recurring_add.opt.frequency", "Периодичность")
_r("cmd.config.recurring_add.opt.start", "Дата начала (YYYY-MM-DD)")
_r("cmd.config.recurring_add.opt.end", "Дата окончания (YYYY-MM-DD)")
_r("cmd.config.recurring_add.ok", "Регулярный платёж на {amount:.2f} ({frequency})")
_r("cmd.config.recurring_list.help", "Список регулярных платежей")
_r("cmd.config.recurring_list.empty", "Регулярные платежи не добавлены")
_r("cmd.config.recurring_remove.help", "Удалить регулярный платёж по ID")
_r("cmd.config.recurring_remove.arg", "ID регулярного платежа")
_r("cmd.config.recurring_remove.ok", "Регулярный платёж '{id}' удалён")
_r("cmd.config.recurring_remove.err", "Регулярный платёж '{id}' не найден")
_r("cmd.config.asset_add.help", "Добавить актив")
_r("cmd.config.asset_add.opt.name", "Название актива")
_r("cmd.config.asset_add.opt.value", "Стоимость")
_r("cmd.config.asset_add.opt.growth", "Годовой рост (%)")
_r("cmd.config.asset_add.ok", "Актив '{name}' добавлен (стоимость: {value})")
_r("cmd.config.asset_list.help", "Список активов")
_r("cmd.config.asset_list.empty", "Активы не добавлены")
_r("cmd.config.asset_remove.help", "Удалить актив по ID или названию")
_r("cmd.config.asset_remove.arg", "ID или название актива")
_r("cmd.config.asset_remove.ok", "Актив '{id}' удалён")
_r("cmd.config.asset_remove.err", "Актив '{id}' не найден")
_r("cmd.config.liability_add.help", "Добавить обязательство")
_r("cmd.config.liability_add.opt.name", "Название")
_r("cmd.config.liability_add.opt.balance", "Остаток долга")
_r("cmd.config.liability_add.opt.interest", "Процентная ставка (%)")
_r("cmd.config.liability_add.opt.payment", "Ежемесячный платёж")
_r("cmd.config.liability_add.ok", "Обязательство '{name}' добавлено (долг: {balance})")
_r("cmd.config.liability_list.help", "Список обязательств")
_r("cmd.config.liability_list.empty", "Обязательства не добавлены")
_r("cmd.config.liability_remove.help", "Удалить обязательство по ID или названию")
_r("cmd.config.liability_remove.arg", "ID или название обязательства")
_r("cmd.config.liability_remove.ok", "Обязательство '{id}' удалено")
_r("cmd.config.liability_remove.err", "Обязательство '{id}' не найдено")
# ── global ─────────────────────────────────────────────────────
_r("global.ok", "OK")
# ── prompts ───────────────────────────────────────────────────
_r("prompt.analyze", (
"Ты — финансовый AI-ассистент. Проанализируй финансовую модель пользователя.\n"
"\n"
"### Модель (JSON):\n"
"{model_json}\n"
"\n"
"### Прогноз на {months} месяцев:\n"
"{forecast_json}\n"
"\n"
"### Валюта:\n"
"Базовая валюта модели: {base_currency}\n"
"Отображаемая валюта: {display_currency}\n"
"\n"
"Дай анализ по пунктам:\n"
"1. Общее финансовое состояние\n"
"2. Тренд денежного потока (рост/падение)\n"
"3. Достаточность ликвидности\n"
"4. Рекомендации по улучшению"
))
_r("prompt.advice", (
"Ты — финансовый AI-ассистент. Дай персональные рекомендации.\n"
"\n"
"### Модель:\n"
"{model_json}\n"
"\n"
"### Прогноз:\n"
"{forecast_json}\n"
"\n"
"### Валюта:\n"
"Базовая валюта модели: {base_currency}\n"
"Отображаемая валюта: {display_currency}\n"
"\n"
"Вопрос пользователя: {question}\n"
"\n"
"Ответь как опытный финансовый консультант."
))
_r("prompt.scenario_comparison", (
"Ты — финансовый AI-ассистент. Сравни сценарии прогноза.\n"
"\n"
"### Результаты сценариев:\n"
"{scenarios_json}\n"
"\n"
"### Валюта:\n"
"Базовая валюта модели: {base_currency}\n"
"Отображаемая валюта: {display_currency}\n"
"\n"
"Дай рекомендацию: какой сценарий наиболее вероятен и почему."
))
_current_lang = "ru"
class Translator:
def __init__(self, lang: str | None = None):
self.lang = lang or _current_lang
if self.lang not in _TRANSLATIONS:
_TRANSLATIONS[self.lang] = {}
def t(self, key: str, **kwargs) -> str:
src = _TRANSLATIONS.get(self.lang) or _TRANSLATIONS.get("ru") or {}
if key in src:
template = src[key]
elif "ru" in _TRANSLATIONS and key in _TRANSLATIONS["ru"]:
template = _TRANSLATIONS["ru"][key]
else:
return key
return template.format(**kwargs) if kwargs else template
_GLOBAL = Translator()
def setup_i18n() -> None:
lang = os.environ.get("CF_LANG", "ru")
if lang not in _TRANSLATIONS:
_TRANSLATIONS[lang] = {}
_GLOBAL.lang = lang
def set_lang(lang: str) -> None:
if lang not in _TRANSLATIONS:
_TRANSLATIONS[lang] = {}
_GLOBAL.lang = lang
def get_lang() -> str:
return _GLOBAL.lang
def t(key: str, **kwargs) -> str:
return _GLOBAL.t(key, **kwargs)
+66 -195
View File
@@ -6,9 +6,7 @@ from rich.console import Console
from rich.table import Table
from ai.assistant import AssistantService
from cashflow_model import CurrencyConverter, FinancialModel
from cli.config import app as config_app
from cli.i18n import setup_i18n, t
from cashflow_model import FinancialModel
from engine.forecast import ForecastService
from engine.scenarios import DEFAULT_SCENARIOS, ScenarioService
from sync.excel_sync import ExcelSync
@@ -18,16 +16,12 @@ try:
except (AttributeError, OSError):
pass
setup_i18n()
app = typer.Typer(name="cf", help=t("app.help"))
app = typer.Typer(name="cf", help="CashFlow Forecast - personal finance model")
console = Console()
DATA_DIR = Path("data")
MODEL_PATH = DATA_DIR / "model.json"
CURRENCY_OPTION = typer.Option(None, "--currency", "-c", help=t("cmd.forecast.opt.currency"))
def _load_model() -> FinancialModel:
if MODEL_PATH.exists():
@@ -39,292 +33,169 @@ def _save_model(model: FinancialModel) -> None:
model.save(MODEL_PATH)
def _get_converter(model: FinancialModel) -> CurrencyConverter:
return CurrencyConverter(model.exchange_rates)
def _fmt(amount: float, currency: str, symbol: str) -> str:
return f"{symbol}{amount:,.2f}"
def _resolve_currency(model: FinancialModel, currency: str | None) -> tuple[str, str]:
target = currency or model.base_currency
converter = _get_converter(model)
symbol = converter.get_symbol(target)
return target, symbol
def _convert_value(
converter: CurrencyConverter, amount: float, from_curr: str, to_curr: str
) -> float:
if to_curr == from_curr:
return amount
return converter.convert(amount, from_curr, to_curr)
@app.command()
def init() -> None:
"""Создать пустую финансовую модель"""
model = FinancialModel()
_save_model(model)
console.print(t("cmd.init.ok", path=str(MODEL_PATH)))
console.print("[green]OK[/green] Пустая модель создана в data/model.json")
@app.command()
def forecast(
months: int = typer.Option(12, "--months", "-m", help=t("cmd.forecast.opt.months")),
currency: str | None = CURRENCY_OPTION,
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев прогноза"),
) -> None:
"""Запустить прогноз денежных потоков"""
model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ForecastService(model)
results = service.forecast_cashflow(months)
summary = service.summary(months)
def _cv(val):
return _convert_value(converter, val, base_curr, target_curr)
def _f(val):
return _fmt(val, target_curr, symbol)
if results:
table = Table(title=t("cmd.forecast.title", months=months, currency=target_curr))
table.add_column(t("cmd.forecast.col.account"), style="cyan")
table.add_column(t("cmd.forecast.col.month"), style="white")
table.add_column(t("cmd.forecast.col.balance"), justify="right", style="green")
table.add_column(t("cmd.forecast.col.income"), justify="right")
table.add_column(t("cmd.forecast.col.expenses"), justify="right")
table = Table(title=f"Прогноз на {months} мес.")
table.add_column("Счёт", style="cyan")
table.add_column("Месяц", style="white")
table.add_column("Баланс", justify="right", style="green")
table.add_column("Доход", justify="right")
table.add_column("Расход", justify="right")
for r in results:
table.add_row(
r["account"], str(r["month"]),
_f(_cv(r["balance"])),
_f(_cv(r["income"])),
_f(_cv(r["expenses"])),
f"${r['balance']:.2f}",
f"${r['income']:.2f}",
f"${r['expenses']:.2f}",
)
console.print(table)
cb = _cv(summary["total_balance"])
ci = _cv(summary["total_income"])
ce = _cv(summary["total_expenses"])
console.print(t("cmd.forecast.total", balance=_f(cb), income=_f(ci), expenses=_f(ce)))
console.print(f"\n[bold]Итог:[/bold] Баланс: ${summary['total_balance']:.2f} | "
f"Доход: ${summary['total_income']:.2f} | "
f"Расход: ${summary['total_expenses']:.2f}")
@app.command()
def scenario(
name: str = typer.Argument("baseline", help=t("cmd.scenario.arg.name")),
months: int = typer.Option(12, "--months", "-m", help=t("cmd.scenario.opt.months")),
currency: str | None = CURRENCY_OPTION,
name: str = typer.Argument("baseline", help="Имя сценария: baseline, optimistic, pessimistic"),
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
) -> None:
"""Применить сценарий и показать прогноз"""
model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ScenarioService(model)
if name in DEFAULT_SCENARIOS:
scenario_obj = DEFAULT_SCENARIOS[name]
else:
console.print(f"[red]{t('cmd.scenario.err.unknown', name=name)}[/red]")
console.print(t("cmd.scenario.err.available", scenarios=", ".join(DEFAULT_SCENARIOS.keys())))
console.print(f"[red]Неизвестный сценарий: {name}[/red]")
console.print(f"Доступны: {', '.join(DEFAULT_SCENARIOS.keys())}")
raise typer.Exit(1)
result = service.apply(scenario_obj, months)
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
console.print(f"[bold]{t('cmd.scenario.label')}:[/bold] {result['scenario']}")
console.print(f"[bold]Сценарий:[/bold] {result['scenario']}")
console.print(f"[dim]{result['scenario_description']}[/dim]")
console.print(f"{t('cmd.scenario.label.balance')}: {_fmt(balance, target_curr, symbol)}")
console.print(f"{t('cmd.scenario.label.income')}: {_fmt(income, target_curr, symbol)}")
console.print(f"{t('cmd.scenario.label.expenses')}: {_fmt(expenses, target_curr, symbol)}")
console.print(f"Баланс: ${result['total_balance']:.2f}")
console.print(f"Доход: ${result['total_income']:.2f}")
console.print(f"Расход: ${result['total_expenses']:.2f}")
@app.command()
def whatif(
income_mult: float = typer.Option(1.0, "--income", "-i", help=t("cmd.whatif.opt.income")),
expense_mult: float = typer.Option(1.0, "--expense", "-e", help=t("cmd.whatif.opt.expense")),
growth_mult: float = typer.Option(1.0, "--growth", "-g", help=t("cmd.whatif.opt.growth")),
months: int = typer.Option(12, "--months", "-m", help=t("cmd.whatif.opt.months")),
currency: str | None = CURRENCY_OPTION,
income_mult: float = typer.Option(1.0, "--income", "-i", help="Множитель дохода"),
expense_mult: float = typer.Option(1.0, "--expense", "-e", help="Множитель расхода"),
growth_mult: float = typer.Option(1.0, "--growth", "-g", help="Множитель роста активов"),
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
) -> None:
"""What-if анализ с произвольными множителями"""
model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ScenarioService(model)
result = service.what_if(income_mult, expense_mult, growth_mult, months)
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
console.print(f"[bold]{t('cmd.whatif.title')}[/bold]")
console.print(t("cmd.whatif.params", income=income_mult, expense=expense_mult, growth=growth_mult))
console.print(f"{t('cmd.scenario.label.balance')}: {_fmt(balance, target_curr, symbol)}")
console.print(f"{t('cmd.scenario.label.income')}: {_fmt(income, target_curr, symbol)}")
console.print(f"{t('cmd.scenario.label.expenses')}: {_fmt(expenses, target_curr, symbol)}")
console.print("[bold]What-if анализ[/bold]")
console.print(f"Доход x{income_mult} | Расход x{expense_mult} | Рост x{growth_mult}")
console.print(f"Баланс: ${result['total_balance']:.2f}")
console.print(f"Доход: ${result['total_income']:.2f}")
console.print(f"Расход: ${result['total_expenses']:.2f}")
@app.command()
def compare(
months: int = typer.Option(12, "--months", "-m", help=t("cmd.compare.opt.months")),
currency: str | None = CURRENCY_OPTION,
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
) -> None:
"""Сравнить все сценарии"""
model = _load_model()
target_curr, symbol = _resolve_currency(model, currency)
converter = _get_converter(model)
base_curr = model.base_currency
service = ScenarioService(model)
results = service.compare(months)
table = Table(title=t("cmd.compare.title", currency=target_curr))
table.add_column(t("cmd.compare.col.scenario"), style="cyan")
table.add_column(t("cmd.forecast.col.balance"), justify="right")
table.add_column(t("cmd.forecast.col.income"), justify="right")
table.add_column(t("cmd.forecast.col.expenses"), justify="right")
table = Table(title="Сравнение сценариев")
table.add_column("Сценарий", style="cyan")
table.add_column("Баланс", justify="right")
table.add_column("Доход", justify="right")
table.add_column("Расход", justify="right")
for name, r in results.items():
b = _convert_value(converter, r["total_balance"], base_curr, target_curr)
i = _convert_value(converter, r["total_income"], base_curr, target_curr)
e = _convert_value(converter, r["total_expenses"], base_curr, target_curr)
fb = _fmt(b, target_curr, symbol)
fi = _fmt(i, target_curr, symbol)
fe = _fmt(e, target_curr, symbol)
table.add_row(name, fb, fi, fe)
table.add_row(
name,
f"${r['total_balance']:.2f}",
f"${r['total_income']:.2f}",
f"${r['total_expenses']:.2f}",
)
console.print(table)
@app.command()
def import_xlsx(
path: str = typer.Argument(..., help=t("cmd.import.arg.path")),
path: str = typer.Argument(..., help="Путь к .xlsx файлу"),
) -> None:
"""Импорт данных из Excel"""
sync = ExcelSync()
try:
model = sync.import_model(path)
_save_model(model)
console.print(t("cmd.import.ok",
accounts=len(model.accounts),
transactions=len(model.transactions),
recurring=len(model.recurring),
assets=len(model.assets),
liabilities=len(model.liabilities)))
console.print(t("cmd.import.base_currency", currency=model.base_currency))
console.print(f"[green]OK[/green] Импортировано: {len(model.accounts)} счетов, "
f"{len(model.transactions)} транзакций, "
f"{len(model.recurring)} регулярных платежей, "
f"{len(model.assets)} активов, "
f"{len(model.liabilities)} обязательств")
except Exception as e:
console.print(f"[red]{t('cmd.import.err', error=str(e))}[/red]")
console.print(f"[red]Ошибка импорта: {e}[/red]")
raise typer.Exit(1)
@app.command()
def export_xlsx(
path: str = typer.Argument("exports/forecast.xlsx", help=t("cmd.export.arg.path")),
path: str = typer.Argument("exports/forecast.xlsx", help="Путь для .xlsx файла"),
) -> None:
"""Экспорт модели в Excel"""
model = _load_model()
sync = ExcelSync()
try:
sync.export_model(model, path)
console.print(t("cmd.export.ok", path=path))
console.print(f"[green]OK[/green] Модель экспортирована в {path}")
except Exception as e:
console.print(f"[red]{t('cmd.export.err', error=str(e))}[/red]")
console.print(f"[red]Ошибка экспорта: {e}[/red]")
raise typer.Exit(1)
@app.command()
def analyze(
months: int = typer.Option(12, "--months", "-m", help=t("cmd.analyze.opt.months")),
currency: str | None = CURRENCY_OPTION,
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев для анализа"),
) -> None:
"""AI-анализ финансовой модели"""
model = _load_model()
target_curr, _ = _resolve_currency(model, currency)
converter = _get_converter(model)
assistant = AssistantService(model, converter=converter, display_currency=target_curr)
assistant = AssistantService(model)
result = assistant.analyze(months)
console.print(f"[bold]{t('cmd.analyze.title.prompt')}:[/bold]")
console.print("[bold]Промпт для AI:[/bold]")
console.print(result["prompt"][:500] + "...\n")
console.print(f"[bold]{t('cmd.analyze.title.summary')}:[/bold]")
console.print(t("cmd.analyze.base_currency", currency=model.base_currency))
console.print(f"\n[yellow]{t('cmd.analyze.stub')}[/yellow]")
console.print("[bold]Сводка:[/bold]")
s = result["summary"]
console.print(f"Баланс: ${s['total_balance']:.2f}")
console.print(f"Доход: ${s['total_income']:.2f}")
console.print(f"Расход: ${s['total_expenses']:.2f}")
console.print(
"\n[yellow]AI-ответ: заглушка. Подключите реальный API в ai/assistant.py[/yellow]"
)
@app.command()
def info() -> None:
"""Сводка всей финансовой модели"""
model = _load_model()
converter = _get_converter(model)
symbol = converter.get_symbol(model.base_currency)
console.print(f"[bold]{t('cmd.info.metadata')}[/bold]")
console.print(f" {t('cmd.info.base_currency', currency=model.base_currency, symbol=symbol)}")
console.print(f" {t('cmd.info.accounts', count=len(model.accounts))}")
console.print(f" {t('cmd.info.transactions', count=len(model.transactions))}")
console.print(f" {t('cmd.info.recurring', count=len(model.recurring))}")
console.print(f" {t('cmd.info.assets', count=len(model.assets))}")
console.print(f" {t('cmd.info.liabilities', count=len(model.liabilities))}")
console.print(f" {t('cmd.info.rates', count=len(model.exchange_rates))}")
if model.accounts:
tbl = Table(title=t("table.accounts.title"))
tbl.add_column(t("table.accounts.col.name"), style="cyan")
tbl.add_column(t("table.accounts.col.currency"))
tbl.add_column(t("table.accounts.col.balance"), justify="right")
for a in model.accounts:
tbl.add_row(a.name, a.currency, f"{a.balance:,.2f}")
console.print(tbl)
if model.transactions:
tbl = Table(title=t("table.transactions.title"))
tbl.add_column(t("table.transactions.col.date"))
tbl.add_column(t("table.transactions.col.category"))
tbl.add_column(t("table.transactions.col.amount"), justify="right")
tbl.add_column(t("table.transactions.col.description"))
for tx in model.transactions[:10]:
tbl.add_row(tx.date, tx.category, f"{tx.amount:,.2f}", tx.description)
if len(model.transactions) > 10:
console.print(tbl)
console.print(t("cmd.info.more", count=len(model.transactions) - 10))
else:
console.print(tbl)
if model.recurring:
tbl = Table(title=t("table.recurring.title"))
tbl.add_column(t("table.recurring.col.period"))
tbl.add_column(t("table.recurring.col.amount"), justify="right")
tbl.add_column(t("table.recurring.col.category"))
for r in model.recurring:
tbl.add_row(r.frequency, f"{r.amount:,.2f}", r.category)
console.print(tbl)
if model.assets:
tbl = Table(title=t("table.assets.title"))
tbl.add_column(t("table.assets.col.name"), style="cyan")
tbl.add_column(t("table.assets.col.value"), justify="right")
tbl.add_column(t("table.assets.col.growth"), justify="right")
for a in model.assets:
tbl.add_row(a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
console.print(tbl)
if model.liabilities:
tbl = Table(title=t("table.liabilities.title"))
tbl.add_column(t("table.liabilities.col.name"), style="cyan")
tbl.add_column(t("table.liabilities.col.balance"), justify="right")
tbl.add_column(t("table.liabilities.col.rate"), justify="right")
tbl.add_column(t("table.liabilities.col.payment"), justify="right")
for li in model.liabilities:
tbl.add_row(li.name, f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}")
console.print(tbl)
app.add_typer(config_app)
if __name__ == "__main__":
app()
BIN
View File
Binary file not shown.
+6 -95
View File
@@ -1,97 +1,8 @@
{
"base_currency": "RUB",
"accounts": [
{
"id": "cfff5fa0-d42d-46d8-81ec-f460234e4123",
"name": "Основной счёт",
"currency": "RUB",
"balance": 2000
}
],
"transactions": [
],
"recurring": [
{
"id": "479a9960-d7ec-476b-9621-888ceaa1b671",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": 30000,
"category": "Зарплата"
},
{
"id": "eb565346-679b-4560-89d1-7b0c80bbcb5a",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -10000,
"category": "Аренда"
},
{
"id": "30c168a3-cbe0-4d8e-a842-d430d98a80b6",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -3000,
"category": "Коммуналка"
},
{
"id": "f37ef9d3-76a8-4eb3-9a12-e20817d87079",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -8000,
"category": "Продукты"
},
{
"id": "f8ef073e-a8ca-4637-bd63-e4faeb1b282f",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -2000,
"category": "Транспорт"
},
{
"id": "ae8f6ad5-2c66-40d9-b102-64076e9be978",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -500,
"category": "Телефон"
},
{
"id": "fb1197de-727e-49db-9fb8-c5b2554b0504",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -500,
"category": "Подписки"
},
{
"id": "d8e3392a-4015-4920-b27c-e72f99e6bbb7",
"start_date": "2026-01-01",
"end_date": "",
"frequency": "monthly",
"amount": -3000,
"category": "Досуг"
}
],
"assets": [
{
"id": "bd5080c0-45a9-40b1-9dc4-c1ec41157bd7",
"name": "Вклад в банке",
"value": 35000,
"growth_rate": 12
}
],
"liabilities": [
],
"scenarios": [],
"exchange_rates": [
{
"from_currency": "USD",
"to_currency": "RUB",
"rate": 80
}
]
"accounts": [],
"transactions": [],
"recurring": [],
"assets": [],
"liabilities": [],
"scenarios": []
}
+21 -24
View File
@@ -17,33 +17,17 @@ class ForecastService:
results = []
for account in self.model.accounts:
balance = account.balance
monthly = self._project_account(account, months)
for m in range(months):
balance = monthly[m]["balance"]
results.append({
"account": account.name,
"month": m + 1,
"balance": monthly[m]["balance"],
"income": monthly[m]["income"],
"expenses": monthly[m]["expenses"],
"balance": round(balance, 2),
"income": round(monthly[m]["income"], 2),
"expenses": round(monthly[m]["expenses"], 2),
})
# Asset growth — once per month, distributed across accounts proportionally
for m in range(months):
total_growth = sum(
a.value * a.growth_rate / 100 / 12
for a in self.model.assets
)
month_rows = [r for r in results if r["month"] == m + 1]
total_bal = sum(r["balance"] for r in month_rows) or 1
for r in month_rows:
share = r["balance"] / total_bal
r["income"] = round(r["income"] + total_growth * share, 2)
r["balance"] = round(r["balance"] + total_growth * share, 2)
# Compound asset values for next month
for a in self.model.assets:
a.value += a.value * a.growth_rate / 100 / 12
return results
def _project_account(self, account: Account, months: int) -> list[dict]:
@@ -66,17 +50,30 @@ class ForecastService:
else:
expenses += abs(r.amount)
income += self._asset_income(account)
expenses += self._liability_cost(account)
balance += income - expenses
asset_growth = sum(
a.value * a.growth_rate / 12
for a in self.model.assets
)
balance += asset_growth
monthly.append({
"balance": round(balance, 2),
"income": round(income, 2),
"expenses": round(expenses, 2),
"balance": balance,
"income": income,
"expenses": expenses,
})
return monthly
def _asset_income(self, account: Account) -> float:
return sum(
a.value * a.growth_rate / 12
for a in self.model.assets
)
def _liability_cost(self, account: Account) -> float:
total = 0.0
for liability in self.model.liabilities:
+1 -1
View File
@@ -21,7 +21,7 @@ include = ["cashflow_model*", "sync*", "engine*", "ai*", "cli*"]
[tool.ruff]
target-version = "py311"
line-length = 120
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
+1 -28
View File
@@ -3,15 +3,7 @@ from uuid import UUID
from openpyxl import Workbook, load_workbook
from cashflow_model import (
Account,
Asset,
ExchangeRate,
FinancialModel,
Liability,
RecurringCashflow,
Transaction,
)
from cashflow_model import Account, Asset, FinancialModel, Liability, RecurringCashflow, Transaction
class SyncError(Exception):
@@ -39,10 +31,6 @@ _SHEET_CONFIG = {
"fields": ["id", "name", "balance", "interest", "payment"],
"cls": Liability,
},
"ExchangeRates": {
"fields": ["from_currency", "to_currency", "rate"],
"cls": ExchangeRate,
},
}
@@ -54,7 +42,6 @@ class ExcelSync:
wb = load_workbook(path, read_only=True, data_only=True)
model = FinancialModel()
model.exchange_rates = []
for sheet_name, config in _SHEET_CONFIG.items():
if sheet_name not in wb.sheetnames:
@@ -75,13 +62,6 @@ class ExcelSync:
data[header] = str(val) if not isinstance(val, (int, float)) else val
self._add_to_model(model, sheet_name, data)
if "ModelInfo" in wb.sheetnames:
ws = wb["ModelInfo"]
rows = list(ws.iter_rows(values_only=True))
for row in rows:
if row[0] and str(row[0]).strip().lower() == "base_currency" and len(row) > 1:
model.base_currency = str(row[1]).strip()
wb.close()
return model
@@ -96,7 +76,6 @@ class ExcelSync:
"Recurring": model.recurring,
"Assets": model.assets,
"Liabilities": model.liabilities,
"ExchangeRates": model.exchange_rates,
}
for sheet_name, items in collections.items():
@@ -111,10 +90,6 @@ class ExcelSync:
]
ws.append(row)
ws_info = wb.create_sheet(title="ModelInfo")
ws_info.append(["Property", "Value"])
ws_info.append(["base_currency", model.base_currency])
wb.save(path)
def _add_to_model(self, model: FinancialModel, sheet_name: str, data: dict) -> None:
@@ -129,7 +104,5 @@ class ExcelSync:
model.assets.append(Asset.from_dict(data))
elif sheet_name == "Liabilities":
model.liabilities.append(Liability.from_dict(data))
elif sheet_name == "ExchangeRates":
model.exchange_rates.append(ExchangeRate.from_dict(data))
except Exception as e:
raise SyncError(f"Failed to parse row in {sheet_name}: {e}") from e
+2 -13
View File
@@ -5,8 +5,6 @@ import pytest
from cashflow_model import (
Account,
Asset,
CurrencyConverter,
ExchangeRate,
FinancialModel,
Liability,
RecurringCashflow,
@@ -18,10 +16,9 @@ from cashflow_model import (
def sample_model() -> FinancialModel:
acc_id = uuid4()
return FinancialModel(
base_currency="RUB",
accounts=[
Account(id=acc_id, name="Основной счёт", currency="RUB", balance=5000.0),
Account(name="Сбережения", currency="RUB", balance=10000.0),
Account(id=acc_id, name="Основной счёт", currency="USD", balance=5000.0),
Account(name="Сбережения", currency="USD", balance=10000.0),
],
transactions=[
Transaction(
@@ -51,17 +48,9 @@ def sample_model() -> FinancialModel:
liabilities=[
Liability(name="Кредит", balance=20000.0, interest=5.0, payment=500.0),
],
exchange_rates=[
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
],
)
@pytest.fixture
def empty_model() -> FinancialModel:
return FinancialModel()
@pytest.fixture
def sample_converter() -> CurrencyConverter:
return CurrencyConverter([ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)])
+1 -144
View File
@@ -1,35 +1,21 @@
from typer.testing import CliRunner
from cli.main import MODEL_PATH, app
from cli.main import app
runner = CliRunner()
def _cleanup():
if MODEL_PATH.exists():
MODEL_PATH.unlink()
class TestCli:
def test_init(self):
_cleanup()
result = runner.invoke(app, ["init"])
assert result.exit_code == 0
assert "Пустая модель" in result.stdout
def test_forecast_after_init(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["forecast", "--months", "3"])
assert result.exit_code == 0
def test_forecast_with_currency(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["forecast", "--months", "3", "--currency", "USD"])
assert result.exit_code == 0
assert "$" in result.stdout
def test_unknown_scenario(self):
result = runner.invoke(app, ["scenario", "unknown"])
assert result.exit_code != 0
@@ -37,132 +23,3 @@ class TestCli:
def test_help(self):
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
def test_info_empty(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["info"])
assert result.exit_code == 0
assert "Базовая валюта" in result.stdout
def test_config_base_currency_set(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["config", "base-currency", "EUR"])
assert result.exit_code == 0
assert "EUR" in result.stdout
info = runner.invoke(app, ["info"])
assert "EUR" in info.stdout
def test_config_base_currency_show(self):
_cleanup()
runner.invoke(app, ["init"])
result = runner.invoke(app, ["config", "base-currency"])
assert result.exit_code == 0
assert "RUB" in result.stdout
def test_config_account_crud(self):
_cleanup()
runner.invoke(app, ["init"])
args = ["config", "account-add", "--name", "Тестовый", "--balance", "1000"]
add = runner.invoke(app, args)
assert add.exit_code == 0
assert "Тестовый" in add.stdout
lst = runner.invoke(app, ["config", "account-list"])
assert "Тестовый" in lst.stdout
remove = runner.invoke(app, ["config", "account-remove", "Тестовый"])
assert remove.exit_code == 0
lst2 = runner.invoke(app, ["config", "account-list"])
assert "Тестовый" not in lst2.stdout
def test_config_transaction_crud(self):
_cleanup()
runner.invoke(app, ["init"])
runner.invoke(app, ["config", "account-add", "--name", "Счёт", "--balance", "0"])
add = runner.invoke(app, [
"config", "transaction-add",
"--account", "Счёт",
"--amount", "5000",
"--category", "income",
"--description", "Зарплата",
])
assert add.exit_code == 0
assert "доход" in add.stdout
lst = runner.invoke(app, ["config", "transaction-list"])
assert "Зарплата" in lst.stdout
def test_config_asset_crud(self):
_cleanup()
runner.invoke(app, ["init"])
add = runner.invoke(app, [
"config", "asset-add",
"--name", "Квартира",
"--value", "5000000",
"--growth", "5",
])
assert add.exit_code == 0
assert "Квартира" in add.stdout
lst = runner.invoke(app, ["config", "asset-list"])
assert "Квартира" in lst.stdout
def test_config_liability_crud(self):
_cleanup()
runner.invoke(app, ["init"])
add = runner.invoke(app, [
"config", "liability-add",
"--name", "Кредит",
"--balance", "100000",
"--interest", "10",
"--payment", "5000",
])
assert add.exit_code == 0
assert "Кредит" in add.stdout
def test_config_recurring_crud(self):
_cleanup()
runner.invoke(app, ["init"])
add = runner.invoke(app, [
"config", "recurring-add",
"--amount", "-500",
"--category", "аренда",
"--frequency", "monthly",
])
assert add.exit_code == 0
lst = runner.invoke(app, ["config", "recurring-list"])
assert "аренда" in lst.stdout
def test_config_rate_crud(self):
_cleanup()
runner.invoke(app, ["init"])
set_r = runner.invoke(app, ["config", "rate-set", "EUR", "RUB", "90"])
assert set_r.exit_code == 0
lst = runner.invoke(app, ["config", "rate-list"])
assert "EUR" in lst.stdout
assert "90" in lst.stdout
def test_info_with_data(self):
_cleanup()
runner.invoke(app, ["init"])
acc_args = ["config", "account-add", "--name", "Основной", "--balance", "50000"]
runner.invoke(app, acc_args)
asset_args = ["config", "asset-add", "--name", "Акции",
"--value", "100000", "--growth", "8.0"]
runner.invoke(app, asset_args)
result = runner.invoke(app, ["info"])
assert "Основной" in result.stdout
assert "Акции" in result.stdout
-72
View File
@@ -1,72 +0,0 @@
import pytest
from cashflow_model import CurrencyConverter, CurrencyError, ExchangeRate
class TestExchangeRate:
def test_to_dict_roundtrip(self):
rate = ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)
d = rate.to_dict()
r2 = ExchangeRate.from_dict(d)
assert r2.from_currency == "USD"
assert r2.to_currency == "RUB"
assert r2.rate == 80.0
def test_defaults(self):
r = ExchangeRate()
assert r.from_currency == "USD"
assert r.to_currency == "RUB"
assert r.rate == 80.0
class TestCurrencyConverter:
def test_convert_usd_to_rub(self, sample_converter):
result = sample_converter.convert(100, "USD", "RUB")
assert result == 8000.0
def test_convert_rub_to_usd(self, sample_converter):
result = sample_converter.convert(8000, "RUB", "USD")
assert result == 100.0
def test_same_currency(self, sample_converter):
result = sample_converter.convert(500, "USD", "USD")
assert result == 500.0
def test_unknown_pair(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.convert(100, "USD", "RUB")
def test_negative_rate(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.set_rate("USD", "RUB", -1)
def test_zero_rate(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.set_rate("USD", "RUB", 0)
def test_inverse_auto(self):
rate = ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0)
converter = CurrencyConverter([rate])
assert converter.convert(900, "RUB", "EUR") == 10.0
def test_get_symbol(self, sample_converter):
assert sample_converter.get_symbol("USD") == "$"
assert sample_converter.get_symbol("RUB") == ""
assert sample_converter.get_symbol("XYZ") == "XYZ"
def test_with_defaults(self):
converter = CurrencyConverter.with_defaults()
assert converter.convert(10, "USD", "RUB") == 800.0
def test_multiple_rates(self):
rates = [
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0),
]
converter = CurrencyConverter(rates)
assert converter.convert(10, "USD", "RUB") == 800.0
assert converter.convert(10, "EUR", "RUB") == 900.0
assert converter.convert(900, "RUB", "EUR") == 10.0
-10
View File
@@ -22,16 +22,6 @@ class TestExcelSync:
assert len(loaded.recurring) == len(sample_model.recurring)
assert len(loaded.assets) == len(sample_model.assets)
assert len(loaded.liabilities) == len(sample_model.liabilities)
assert len(loaded.exchange_rates) == len(sample_model.exchange_rates)
def test_roundtrip_preserves_base_currency(self, tmp_path: Path):
sync = ExcelSync()
model = FinancialModel(base_currency="EUR")
p = tmp_path / "eur_model.xlsx"
sync.export_model(model, p)
loaded = sync.import_model(p)
assert loaded.base_currency == "EUR"
def test_import_missing_file(self):
sync = ExcelSync()
-44
View File
@@ -1,44 +0,0 @@
from cli.i18n import Translator, get_lang, set_lang, setup_i18n, t
class TestTranslator:
def test_ru_by_default(self):
setup_i18n()
assert get_lang() == "ru"
def test_ru_returns_russian(self):
tr = Translator("ru")
assert "финансовую" in tr.t("cmd.init.help")
def test_en_fallback_to_ru(self):
tr = Translator("en")
result = tr.t("cmd.init.help")
assert "финансовую" in result
def test_unknown_key_returns_key(self):
tr = Translator("ru")
assert tr.t("nonexistent.key") == "nonexistent.key"
def test_set_lang(self):
setup_i18n()
set_lang("en")
assert get_lang() == "en"
def test_t_function(self):
setup_i18n()
result = t("cmd.init.ok", path="data/model.json")
assert "data/model.json" in result
assert "Пустая" in result
def test_global_ok(self):
result = t("global.ok")
assert result == "OK"
def test_prompt_analyze(self):
result = t("prompt.analyze")
assert "AI-ассистент" in result
def test_custom_lang_init(self):
tr = Translator("de")
result = tr.t("cmd.init.help")
assert "финансовую" in result # fallback to ru
-31
View File
@@ -3,7 +3,6 @@ from pathlib import Path
from cashflow_model import (
Account,
Asset,
ExchangeRate,
FinancialModel,
ForecastScenario,
Liability,
@@ -43,32 +42,6 @@ class TestTransaction:
class TestFinancialModel:
def test_default_base_currency(self):
model = FinancialModel()
assert model.base_currency == "RUB"
def test_base_currency_roundtrip(self, tmp_path: Path):
model = FinancialModel(base_currency="EUR")
p = tmp_path / "model.json"
model.save(p)
loaded = FinancialModel.load(p)
assert loaded.base_currency == "EUR"
def test_base_currency_backward_compat(self, tmp_path: Path):
import json
p = tmp_path / "legacy.json"
with open(p, "w") as f:
json.dump({"accounts": []}, f)
loaded = FinancialModel.load(p)
assert loaded.base_currency == "RUB"
def test_exchange_rates_default(self):
model = FinancialModel()
assert len(model.exchange_rates) == 1
assert model.exchange_rates[0].from_currency == "USD"
assert model.exchange_rates[0].to_currency == "RUB"
assert model.exchange_rates[0].rate == 80.0
def test_save_load(self, tmp_path: Path):
model = FinancialModel()
model.accounts.append(Account(name="Test", balance=100.0))
@@ -88,8 +61,6 @@ class TestFinancialModel:
d = model.to_dict()
assert d["accounts"] == []
assert d["transactions"] == []
assert d["base_currency"] == "RUB"
assert "exchange_rates" in d
def test_all_entities_roundtrip(self, tmp_path: Path):
model = FinancialModel(
@@ -99,7 +70,6 @@ class TestFinancialModel:
assets=[Asset(name="Stock", value=1000.0)],
liabilities=[Liability(name="Loan", balance=500.0, interest=5.0, payment=100.0)],
scenarios=[ForecastScenario(name="test")],
exchange_rates=[ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)],
)
p = tmp_path / "full.json"
model.save(p)
@@ -110,4 +80,3 @@ class TestFinancialModel:
assert len(loaded.assets) == 1
assert len(loaded.liabilities) == 1
assert len(loaded.scenarios) == 1
assert len(loaded.exchange_rates) == 1