12 Commits
Author SHA1 Message Date
oqyude 7443ec059f v1.1.5 2026-07-24 22:45:07 +03:00
oqyude 055edc7750 metaagent integration 2026-07-22 01:38:31 +03:00
oqyude 09069047a6 md update 2026-07-11 14:56:35 +03:00
oqyude 00cc10b90a v1.1.4 2026-07-11 14:46:30 +03:00
oqyude ec75fd9d72 v1.1.3 2026-07-11 14:46:16 +03:00
oqyude 6ff146eaaf review and new module 2026-07-11 14:33:05 +03:00
oqyude 4cf5e9da4a fix with paragraphs 2026-07-11 03:43:26 +03:00
oqyude d546947e61 some automation
Release / release (push) Has been cancelled
2026-07-11 03:26:31 +03:00
oqyude 801fdec71e workflow and fixes
Release / release (push) Has been cancelled
2026-07-11 03:22:18 +03:00
oqyude e5f1061db7 fix 2026-07-11 03:10:39 +03:00
oqyude 23cf742569 license and fixup 2026-07-11 03:05:42 +03:00
oqyude 2dec5660f6 ru + en lang 2026-07-11 02:54:10 +03:00
53 changed files with 4318 additions and 231 deletions
+85
View File
@@ -0,0 +1,85 @@
# Analysis Report
## Session
- **Session ID:** a1b2c3d4-e5f6-7890-abcd-ef1234567890
- **Target repo:** S:\Git\zeroq-qol-modules
- **Date:** 2026-07-22
- **Project type:** existing
## 1. Общая информация
- **README:** Obsidian-плагин с модульной архитектурой. Каждая QoL-функция — отдельный модуль, включаемый/отключаемый в настройках. Сборка: `npm run build`.
- **Лицензия:** MIT
- **CI/CD:** GitHub Actions — `release.yml` (сборка и релиз при пуше тега v*)
- **Точка входа:** `src/main.ts` → компилируется в `main.js` через esbuild
- **Система сборки:** npm + esbuild (0.17.x), TypeScript 5.x
## 2. Стек технологий
| Компонент | Значение |
|---|---|
| Язык | TypeScript (5.x) |
| Фреймворк | Obsidian API (plugin) |
| База данных | — |
| Тестовый раннер | нет |
| Пакетный менеджер | npm |
| Линтер/форматтер | нет (не настроен) |
## 3. Архитектура
```
src/
├── main.ts # Plugin class onload/onunload
├── settings.ts # ZeroQSettingTab — единая панель настроек
├── types.ts # QoLModule, ZeroQSettings, ModuleSettings
├── locales/
│ ├── en.ts # Locale interface + English
│ ├── ru.ts # Russian translation
│ └── index.ts # Language auto-detect + export locale
└── modules/
├── index.ts # MODULES registry (static array)
├── base-module.ts # BaseModule abstract class
├── attachment-clean-paste/
│ └── index.ts # Paste/drop handler — ![[embed]] → [[path|name]]
└── preserve-link-aliases/
└── index.ts # Rename handler — preserves aliases in links
```
**Паттерн:** Модульный монолит (Plugin → Module Registry → Modules)
**Ключевые модули:**
| Модуль | Описание |
|---|---|
| `main.ts` | Жизненный цикл плагина: загрузка настроек, bootstrap модулей |
| `settings.ts` | Единая панель настроек: рендерит toggle + inline-настройки для каждого модуля |
| `types.ts` | Интерфейсы: `QoLModule`, `ZeroQSettings`, `ModuleSettings` |
| `locales/` | Локализация: автоопределение языка из `<html lang>`, fallback на en |
| `base-module.ts` | Абстрактный класс с `registerCleanup()` для автоматической очистки |
| `attachment-clean-paste` | Обработка paste/drop: копирует файл в attachments, вставляет `[[path|name]]` |
| `preserve-link-aliases` | Обработка rename: сохраняет алиасы `[[path|oldname]]` при переименовании |
## 4. Конвенции
- **Стиль:** tabs для отступов, kebab-case для файлов, PascalCase для классов
- **Импорты:** `import type` для type-only импортов; абсолютные пути из Obsidian, относительные внутри проекта
- **Типизация:** строгая — `noImplicitAny: true`, `strictNullChecks: true`; интерфейсы для настроек модулей
- **Обработка ошибок:** try/catch в асинхронных операциях, `new Notice()` для user-facing ошибок, `console.error` для логирования
- **Логирование:** `console.error` в исключениях, без отдельного логгера
## 5. Тесты
- **Команда запуска:** нет
- **Всего тестов:** 0
- Тесты отсутствуют в проекте.
## 6. Базовая проверка
- **Сборка:** успешно (`npm run build` — main.js 17.1kb)
- **Запуск:** невозможно (Obsidian-плагин, требуется среда Obsidian)
- **Git status:** чисто (ветка dev, untracked только .agent/)
## 8. Примечания
Существующий проект с 2 модулями. Все модули следуют единому паттерну: BaseModule → onload/onunload/renderInlineSettings. Архитектура спроектирована для лёгкого добавления новых модулей через регистрацию в `MODULES` и добавление переводов. Есть подробная документация по добавлению модуля в `review.md` (197 строк). Проект не имеет тестов, линтера, CI (кроме релизного workflow).
+6
View File
@@ -0,0 +1,6 @@
{
"version": "1.1.0",
"archived_at": "2026-07-22T00:00:00.000Z",
"tasks": [],
"checkpoints": []
}
+39
View File
@@ -0,0 +1,39 @@
{
"metaagent_version": "1.1.0",
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"target_repo": "S:\\Git\\zeroq-qol-modules",
"goal": "Проанализировать существующий Obsidian-плагин zeroq-qol-modules (v1.1.4) и подготовить манифест задач для исполнительного агента.",
"project_type": "existing",
"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": "completed",
"design": "skipped",
"red_team": "skipped",
"decomposition": "completed",
"environment": "completed",
"handoff": "completed"
},
"tasks": [
{ "id": "T1", "title": "Настроить ESLint и Prettier", "type": "config", "status": "pending", "depends_on": [] },
{ "id": "T2", "title": "Написать тесты для BaseModule", "type": "test", "status": "pending", "depends_on": [] },
{ "id": "T3", "title": "Написать тесты для Attachment Clean Paste модуля", "type": "test", "status": "pending", "depends_on": ["T2"] },
{ "id": "T4", "title": "Написать тесты для Preserve Link Aliases модуля", "type": "test", "status": "pending", "depends_on": ["T2"] },
{ "id": "T5", "title": "Добавить CI workflow для тестов и линтинга", "type": "config", "status": "pending", "depends_on": ["T1", "T3", "T4"] }
],
"last_updated": "2026-07-22T00:00:00.000Z",
"handoff_completed": true
}
+96
View File
@@ -0,0 +1,96 @@
# Handoff Summary
## Session Info
- **Session ID:** `a1b2c3d4-e5f6-7890-abcd-ef1234567890`
- **Target Repo:** S:\Git\zeroq-qol-modules
- **Goal:** Проанализировать существующий Obsidian-плагин zeroq-qol-modules (v1.1.4) и подготовить манифест задач для исполнительного агента.
- **Date:** 2026-07-22
- **Depth:** 4 (Light)
- **Config:** analysis=yes, design=skipped(existing), decomposition=yes, setup=yes, handoff=yes
## Repo Summary
Obsidian-плагин с модульной архитектурой. TypeScript, esbuild. 2 модуля: Attachment Clean Paste и Preserve Link Aliases. Система настроек с inline-тогглами. Локализация en/ru. Тесты и линтер отсутствуют.
## Project Type
- **Type:** existing
- **Design report:** — (existing project)
## ADR Summary
Не применимо (depth=4, adr=no)
## Risk Register
Не применимо (depth=4, risk_register=no)
## Environment Status
- **Build:** SUCCESS (esbuild, 17.1kb, 11ms)
- **Tests:** 0 total (тест-раннер не настроен)
- **Baseline log:** `.agent/baseline-test-report.log`
- **Dependencies:** установлены (npm)
## Task Overview
| Status | Count |
|---|---|
| Total | 5 |
| Pending | 5 |
| In Progress | 0 |
| Completed | 0 |
| Failed/Skipped | 0 |
**Task by type:**
- config: 2
- test: 3
## Tasks (ordered)
### T1: Настроить ESLint и Prettier
- Type: config
- Depends on: —
- Files: package.json, .eslintrc.json, .prettierrc.json
- Status: pending
### T2: Написать тесты для BaseModule
- Type: test
- Depends on: —
- Files: src/modules/base-module.ts, tests/modules/base-module.test.ts, package.json, tsconfig.json
- Status: pending
### T3: Написать тесты для Attachment Clean Paste модуля
- Type: test
- Depends on: T2
- Files: src/modules/attachment-clean-paste/index.ts, tests/modules/attachment-clean-paste.test.ts
- Status: pending
### T4: Написать тесты для Preserve Link Aliases модуля
- Type: test
- Depends on: T2
- Files: src/modules/preserve-link-aliases/index.ts, tests/modules/preserve-link-aliases.test.ts
- Status: pending
### T5: Добавить CI workflow для тестов и линтинга
- Type: config
- Depends on: T1, T3, T4
- Files: .github/workflows/ci.yml
- Status: pending
## Next Steps
Исполнительный агент начинает с задачи **T1** (ESLint + Prettier) или **T2** (BaseModule тесты) — обе независимы и могут выполняться параллельно.
## Caveats
- Проект не собирается через tsc --noEmit (4 type errors в коде модулей, ошибки в obsidian.d.ts)
- esbuild-сборка работает корректно
- Рекомендуется начать с T1 (линтер уловит существующие type issues)
- В проекте нет тестового раннера — T2 включает его настройку
## Checkpoints
Файл: `.agent/checkpoints.json`
Актуальное состояние чекпоинтов прилагается.
+30
View File
@@ -0,0 +1,30 @@
# MetaAgent Request
# Auto-generated from user interview on 2026-07-22
## Параметры сессии
| Функция | Вкл | Аргументы |
|---|---|---|
| ANALYSIS | ✓ | — |
| DESIGN | ✗ | (existing project) |
| RED_TEAM | ✗ | — |
| RISK_REGISTER | ✗ | — |
| DECOMPOSITION | ✓ | invariant_tests=no |
| SETUP | ✓ | — |
| HANDOFF | ✓ | layer_structure=no |
## Глубина проработки
**Значение:** 4 (Light)
| Уровень | Название | Описание |
|---|---|---|
| 1-2 | Scaffold | Только структура проекта + пустые модули |
| 3-4 | Light | (default) Быстрый дизайн + задачи без расширений |
| 5-6 | Standard | Полный ANALYSIS→DESIGN→DECOMP→SETUP→HANDOFF |
| 7-8 | Deep | Standard + ADR, Risk Register, Alternative Architecture |
| 9-10 | Maximum | Deep + Red Team Review, Executable Invariants |
## Цель
Проанализировать существующий Obsidian-плагин `zeroq-qol-modules` (v1.1.4) и подготовить манифест задач для исполнительного агента.
+17
View File
@@ -0,0 +1,17 @@
# Project Rules
Правила, которым агент обязан следовать во всех фазах.
Добавляйте сюда условия, которые должны соблюдаться всегда — они будут прочитаны
перед началом каждой фазы и учтены при декомпозиции и реализации.
## Обязательные правила
- (укажите правила, например: «Всегда использовать tabs для отступов»)
## Запреты
- (укажите запреты, например: «Не трогать CI/CD конфигурацию»)
## Конвенции проекта
- (укажите конвенции, например: «Имена классов в PascalCase, функции в snake_case»)
+39
View File
@@ -0,0 +1,39 @@
# BOUNDARIES — Рамки и границы
Что мета-агенту **разрешено**, **запрещено** и в каких случаях **нужно остановиться**.
## Разрешено
| Действие | Примечание |
|---|---|
| Читать любые файлы в целевом репозитории | Все файлы, включая .git, конфиги, историю |
| Создавать/изменять файлы в `.agent/` | Единственная директория для артефактов |
| Устанавливать/обновлять зависимости | Только через штатный пакетный менеджер проекта |
| Изменять конфигурационные файлы | Только если это необходимо для сборки/тестов (например, добавить requirements.txt) |
| Запускать сборку и тесты | Для верификации окружения |
| Читать документацию, issue, PRs | Для понимания контекста |
| Запрашивать уточнения у пользователя | Если не хватает информации для декомпозиции |
| Копировать исходники MetaAgent в `.agent/src/` целевого проекта | Только на фазе INIT, без перезаписи существующих файлов |
| Создавать/обновлять `AGENTS.md` в корне целевого проекта | Только если файла не существует |
| **Обязательно:** читать `.agent/rules/project-rules.md` перед каждой фазой | Исполнение правил пользователя — приоритет выше стандартных протоколов |
| Перемещать завершённые артефакты в `.agent/archive/` | Только на фазе HANDOFF, только для completed/failed артефактов |
## Запрещено
| Действие | Почему |
|---|---|
| Писать production-код | Это работа исполнительного агента |
| Рефакторить существующий код | Мета-агент не меняет логику |
| Удалять файлы | Если файл мешает — нужно сообщить пользователю |
| Коммитить в main/master | Коммиты делает исполнительный агент по задачам |
| Менять удалённые настройки CI/CD | Если CI сломан — сообщить пользователю |
| Пул-реквесты | Исполнительный агент создаёт PR после выполнения задач |
| Модифицировать код, не связанный с задачей | Только то, что нужно для окружения |
## Когда остановиться
1. **Репозиторий не собирается** — сообщить пользователю с логом ошибки, не продолжать
2. **Неясна цель** — запросить уточнение, не гадать
3. **Обнаружены секреты/токены** — не копировать, сообщить пользователю
4. **Цель выходит за рамки одной сессии** — разбить, запросить приоритет
5. **Проект не использует известные технолологии** — запросить у пользователя инструкцию по сборке
+336
View File
@@ -0,0 +1,336 @@
# META_AGENT_GUIDE — Главная инструкция
## Жизненный цикл сессии
```
.agent/metaagent-request.md
INIT → ANALYSE → [DESIGN] → [RED_TEAM] → DECOMPOSITION → SETUP → (CHECKPOINT)* → HANDOFF → EXIT
│ │
▼ ▼
ADR (опц.) Invariant Tasks (опц.)
Alt.Arch (опц.)
Risk Register (опц.)
```
Фазы выполняются **строго последовательно**. Фаза DESIGN — только если project_type = greenfield/scaffold.
Фаза RED_TEAM — только если config.red_team = yes.
Все артефакты размещаются в `.agent/` целевого репозитория (с layer-структурой или плоские, в зависимости от config).
---
## Конфигурация сессии (.agent/metaagent-request.md)
Перед запуском сессии пользователь заполняет `.agent/metaagent-request.md` (см. `TEMPLATES/metaagent-request.md`). Файл должен находиться в директории `.agent/` целевого репозитория.
Ключевые параметры:
### Шкала глубины (depth 1-10)
| Уровень | Название | Что выполняется |
|---|---|---|
| 1-2 | Scaffold | INIT → ANALYSIS → SETUP (только структура, без реализации) |
| 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 |
### Функции (таблица вкл/выкл)
| Функция | Фаза | Глубина | Описание |
|---|---|---|---|
| adr | DESIGN | >=7 | Создание ADR для каждого ключевого решения |
| alternative_arch | DESIGN | >=7 | Обязательное описание альтернативной архитектуры |
| 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/` в корне целевого репозитория (если нет)
- **Установить исходники 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` (если не существовал)
```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: 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 (уже получен на 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"` → далее фаза DESIGN
- `project_type = "existing"` → DESIGN пропускается, сразу DECOMPOSITION
---
## Фаза 2: DESIGN (условная)
**Вход:** analysis-report.md, checkpoints.json (analysis: completed, project_type: greenfield/scaffold).
**Протокол:** `PROTOCOLS/02_DESIGN.md`
**Действия:**
- **Прочитать `.agent/rules/project-rules.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"`
**Ветвление:**
- Если config.red_team = yes → следующая фаза RED_TEAM
- Иначе → сразу DECOMPOSITION
**Выход:** `.agent/design-report.md`, опционально `.agent/layer-1/adr/*.md`, `.agent/layer-1/risk-register.md`
---
## Фаза 2b: RED_TEAM (опциональная)
**Вход:** design-report.md, ADR (опционально), checkpoints.json (design: completed).
**Протокол:** `PROTOCOLS/02b_REDTEAM.md`
**Действия:**
- **Прочитать `.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/layer-1/red-team-report.md`
---
## Фаза 3: DECOMPOSITION
**Вход:** analysis-report.md + design-report.md (опционально) + ADR (опционально) + checkpoints.json.
**Протокол:** `PROTOCOLS/03_DECOMPOSITION.md`
**Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Разбить цель (и дизайн) на атомарные задачи
- Если config.decomposition.invariant_tests: создать задачи-инварианты для каждого ADR
- Записать манифест в `.agent/task-manifest.json` и `.agent/task-manifest.md`
- Обновить checkpoints.json: `phases.decomposition = "completed"`, заполнить `tasks`
**Выход:** `.agent/task-manifest.json`, `.agent/task-manifest.md`
---
## Фаза 4: SETUP
**Вход:** analysis-report.md, design-report.md (опционально), task-manifest.json, checkpoints.json (decomposition: completed).
**Протокол:** `PROTOCOLS/04_ENVIRONMENT_SETUP.md`
**Действия:**
- **Прочитать `.agent/rules/project-rules.md`** — учесть пользовательские правила
- Выполнить настройку окружения по протоколу (ветка A для existing, ветка B для greenfield)
- Записать результат проверки в `.agent/baseline-test-report.log` и `.agent/setup-report.log`
- Обновить checkpoints.json: `phases.environment = "completed"`
**Выход:** рабочее окружение + `.agent/baseline-test-report.log`
---
## Фаза 5: CHECKPOINT (сквозная)
**Вход:** любая фаза.
**Протокол:** обновлять checkpoints.json после каждого значимого шага.
**Архивирование перед сохранением чекпоинта:**
- Если checkpoints.json уже существует — сохранить предыдущую версию в `.agent/archive/checkpoints/<last_updated>.json`
**Формат:**
```json
{
"metaagent_version": "1.1.0",
"session_id": "<uuid>",
"target_repo": "<path>",
"goal": "<цель>",
"project_type": "existing | greenfield | scaffold",
"config": {
"depth": 6,
"design": { "adr": true, "alternative_arch": true },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": true },
"handoff": { "layer_structure": true }
},
"phases": {
"analysis": "completed",
"design": "completed",
"red_team": "skipped",
"decomposition": "in_progress",
"environment": "pending",
"handoff": "pending"
},
"tasks": [
{ "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/
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
setup-report.log
```
Исполнительный агент всегда начинает с layer-0 (checkpoints + session-summary),
затем при необходимости обращается к layer-1 (ADR для понимания "почему"),
layer-2 (детали дизайна), layer-3 (что было сделано).
+145
View File
@@ -0,0 +1,145 @@
# Протокол 00: Конфигурация сессии (CONFIG)
## Цель
Определить параметры сессии MetaAgent: глубину проработки, набор функций, тип проекта. Выполняется на фазе INIT.
## Вход
- `VERSION` — текущая версия MetaAgent
- Запрос пользователя (цель)
- Опционально: `.agent/metaagent-request.md` (в директории `.agent/` целевого репозитория)
## Шаги
### 0.1. Проверить наличие .agent/metaagent-request.md
Если файл существует — распарсить, провалидировать и использовать.
Если нет — перейти к интервью (шаг 0.2).
### 0.2. Интервью с пользователем
Задать пользователю серию вопросов для сбора конфигурации.
**Сценарий интервью:**
```
MetaAgent: .agent/metaagent-request.md не найден. Давайте настроим сессию.
(или ответьте "default" — я выберу depth=4, light)
Q1: Это новый проект (greenfield) или работа с существующим кодом (existing)?
Варианты: new / existing / scaffold / default
Q2: Глубина проработки?
1-2: Scaffold — только структура, пустые модули
3-4: Light — быстрый дизайн + задачи, без расширений (рекомендуется default)
5-6: Standard — полный цикл с acceptance criteria
7-8: Deep — + ADR, risk register, alternative architecture
9-10: Maximum — + Red Team review, executable invariants
Варианты: число 1-10 / default
Q3 (если глубина >= 7): Нужны ADR (Architecture Decision Records)?
Варианты: yes / no / default
Q4 (если глубина >= 7): Нужен Risk Register?
Варианты: yes / no / default
Q5 (если глубина >= 9): Нужен Red Team Review?
Варианты: yes / no / default
```
**Правила обработки ответов:**
- Если пользователь ответил `default` или не ответил — применить значение по умолчанию для этого поля
- Если пользователь ответил `new``project_type = greenfield`
- Если `existing``project_type = existing`
### 0.3. Default config
```json
{
"depth": 4,
"design": {
"adr": false,
"alternative_arch": false
},
"red_team": false,
"risk_register": false,
"decomposition": {
"invariant_tests": false
},
"handoff": {
"layer_structure": false
}
}
```
Depth=4 (Light) означает:
- ANALYSIS — полный (определение типа проекта, извлечение требований)
- DESIGN — выполняется (если greenfield), но **без** ADR, Alternative Architecture, Risk Register
- DECOMPOSITION — задачи с acceptance criteria, **без** invariant-тестов
- SETUP — полный
- HANDOFF — плоский `.agent/` (без layer-структуры)
### 0.4. Запись .agent/metaagent-request.md
Если файла не было, создать его по результатам интервью с пометкой `Auto-generated`:
```markdown
# MetaAgent Request
# Auto-generated from user interview on {{ date }}
## Параметры сессии
| Функция | Вкл | Аргументы |
|---|---|---|
| ANALYSIS | ✓ | — |
| DESIGN | ✓ | adr={{ adr }}, alternative_arch={{ alt_arch }} |
| RED_TEAM | {{ red_team }} | — |
| RISK_REGISTER | {{ risk_register }} | — |
| DECOMPOSITION | ✓ | invariant_tests={{ invariant_tests }} |
| SETUP | ✓ | — |
| HANDOFF | ✓ | layer_structure={{ layer_structure }} |
## Глубина проработки
**Значение:** {{ depth }}
## Цель
{{ goal }}
```
### 0.5. Создание .agent/rules/
Создать директорию `.agent/rules/` в корне целевого проекта (если не существует).
Если `.agent/rules/project-rules.md` не существует — создать из шаблона `.agent/src/TEMPLATES/project-rules.md`:
```markdown
# Project Rules
Добавляйте сюда правила, которым агент обязан следовать во всех фазах.
```
### 0.6. Валидация config
Проверить совместимость параметров с depth:
```
depth < 3 → DESIGN пропускается (даже для greenfield)
depth < 7 → adr=false, alternative_arch=false, risk_register=false, invariant_tests=false
depth < 9 → red_team=false
```
Если depth несовместим с включёнными функциями — понизить функции до максимума, разрешённого depth.
## Выход
- `.agent/metaagent-request.md` (создан или подтверждён)
- config — словарь параметров для записи в checkpoints.json
## Критерии завершения
- [ ] `.agent/metaagent-request.md` существует (создан или найден)
- [ ] Config содержит depth, design.*, red_team, risk_register, decomposition.*, handoff.*
- [ ] Config совместим с depth (доп. функции отключены для малых depth)
- [ ] При отсутствии файла — проведено интервью, файл создан
+124
View File
@@ -0,0 +1,124 @@
# Протокол 00b: Миграция артефактов (MIGRATE)
## Цель
Обеспечить совместимость артефактов `.agent/` при изменении версии MetaAgent.
Позволяет обновлять проекты, созданные старой версией, без потери данных.
## Вход
- `VERSION` — текущая версия MetaAgent
- `.agent/checkpoints.json` — артефакты целевого проекта
- `.agent/` — остальные артефакты
## Шаги
### M1. Определить версию артефактов
Прочитать `.agent/checkpoints.json`:
```python
stored_version = checkpoints.get("metaagent_version", None)
current_version = read("VERSION").strip()
```
- Если `metaagent_version` отсутствует → артефакт создан **v0.x** (доверсионный)
- Если `metaagent_version` == `current_version` → пропустить миграцию
- Если `metaagent_version` < `current_version` → требуется миграция
### M2. Сравнение версий (SemVer)
Версии сравниваются по семантическому версионированию (`MAJOR.MINOR.PATCH`).
```python
def needs_migration(stored, current):
if stored is None:
return True
return parse_semver(stored) < parse_semver(current)
```
### M3. Матрица миграций
Каждая строка — набор шагов для перехода с одной версии на следующую.
| Из версии | В версию | Шаги миграции |
|---|---|---|
| v0.x (нет поля) | v1.0.0 | M3.1 — M3.4 |
| v1.0.0 | v1.1.0 | M3.5 — M3.6 (см. ниже) |
### M4. Шаги миграции v0.x → v1.0.0
... (шаги миграции остаются без изменений)
### M5. Шаги миграции v1.0.0 → v1.1.0
M3.5: Создать `.agent/rules/` с шаблоном `project-rules.md` (если не существует).
M3.6: Создать `.agent/archive/` (если не существует).
#### M3.1. Добавить metaagent_version
Записать в checkpoints.json:
```json
"metaagent_version": "1.1.0"
```
#### M3.2. Добавить config (default)
Если поля `config` нет в checkpoints.json — добавить config по умолчанию:
```json
"config": {
"depth": 4,
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
}
```
#### M3.3. Добавить фазу red_team
Если в `phases` нет ключа `red_team`:
```json
"red_team": "skipped"
```
#### M3.4. Создать layer-1/ (опционально, только если config.handoff.layer_structure)
Если включена layer_structure:
```bash
mkdir -p .agent/layer-1/adr
touch .agent/layer-1/adr/.gitkeep
```
Если `risk-register.md` уже существует на верхнем уровне — переместить в `.agent/layer-1/risk-register.md`.
### M4. После миграции — резюме
Записать в `.agent/migration-report.log`:
```
[MIGRATE] {{ timestamp }}
From: {{ from_version }}
To: {{ to_version }}
Steps applied: {{ step_list }}
Status: OK
```
## Выход
- Обновлённый `.agent/checkpoints.json` (metaagent_version + config)
- Опционально: `.agent/layer-1/` структура
- `.agent/migration-report.log`
## Критерии завершения
- [ ] metaagent_version в checkpoints.json == текущей версии из VERSION
- [ ] config присутствует в checkpoints.json
- [ ] phases.red_team присутствует (skipped, если не нужен)
- [ ] migration-report.log создан
- [ ] Все старые данные сохранены (ничего не удалено)
+139
View File
@@ -0,0 +1,139 @@
# Протокол 01: Анализ репозитория (ANALYSIS)
## Цель
Составить полную картину целевого репозитория: тип проекта, архитектура, стек, конвенции, состояние тестов, требования.
## Вход
- Целевой репозиторий (локальная копия)
- `.agent/metaagent-request.md` (конфигурация сессии: глубина, функции) — или auto-generated
- `.agent/checkpoints.json` (фаза analysis: pending)
## Шаги
### 0.0. Чтение конфигурации сессии
Прочитать config из checkpoints.json (установлен на фазе INIT через `00_CONFIG.md`).
Если config отсутствует или неполный — применить default:
```json
{
"depth": 4,
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
}
```
Записать (или подтвердить) конфигурацию в checkpoints.json:
```json
"config": {
"depth": 6,
"design": { "adr": true, "alternative_arch": true },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": true },
"handoff": { "layer_structure": true }
}
```
Если `.agent/metaagent-request.md` не найден — использовать значения по умолчанию (depth=6, все базовые функции=true, расширенные=false).
### 1.0. Определение типа проекта
Просканировать корень репозитория и определить:
- **`existing`** — есть исходный код, тесты, система сборки (файлы `.py`, `.js`, `.ts`, `.rs`, `.go` и т.д. помимо конфигов и README)
- **`greenfield`** — репозиторий пуст или содержит только README, LICENSE, .gitignore
- **`scaffold`** — есть базовая структура (pyproject.toml/package.json), но нет значимого кода
Записать тип в analysis-report.md.
**Правило:** если проект `existing` — разделы 1.1–1.6 выполняются полностью. Если `greenfield` — разделы 1.2–1.5 заменяются на 1.7 (извлечение требований из README).
### 1.1. Общая информация
Прочитать и зафиксировать:
- **README** — описание проекта, how to build/test/run
- **Лицензия** — какой LICENSE
- **CI/CD** — `.github/workflows/`, `.gitlab-ci.yml`, `Jenkinsfile`, `Makefile` и т.д.
- **Главные точки входа** — `main.py`, `index.js`, `cmd/` и т.д.
- **Система сборки** — `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `CMakeLists.txt`
### 1.2. Стек технологий (только для existing/scaffold)
Определить:
- **Язык(и)** — Python, TypeScript, Go, Rust и т.д.
- **Фреймворк** — FastAPI, Next.js, React, Actix и т.д.
- **База данных** — PostgreSQL, SQLite, MongoDB и т.д.
- **Тестовый раннер** — pytest, jest, vitest, go test
- **Пакетный менеджер** — pip/poetry, npm/yarn/pnpm, cargo, go modules
- **Линтер/форматтер** — ruff, eslint, prettier, rustfmt, gofmt
### 1.3. Архитектура (только для existing/scaffold)
- **Структура директорий** — записать схему (можно `tree /F`, но не более 3 уровней глубины)
- **Архитектурный паттерн** — MVC, Clean Architecture, модульный монолит, микросервисы
- **Ключевые модули/пакеты** — перечислить с кратким описанием
- **Внешние зависимости** — основные библиотеки
### 1.4. Конвенции кода (только для existing/scaffold)
- **Стиль кода** — судя по линтеру и примерам: именование, импорты, типизация
- **Паттерны** — как организованы роуты, хендлеры, модели, тесты
- **Обработка ошибок** — как принято обрабатывать ошибки в проекте
- **Логирование** — используется ли логгер, какой уровень
### 1.5. Тесты (только для existing/scaffold)
- **Какие тесты есть** — unit, integration, e2e
- **Где лежат** — `tests/`, `__tests__/`, рядом с модулями
- **Запуск** — команда для запуска всех тестов
- **Текущее состояние** — запустить тесты, записать результат (сколько всего, сколько пройдено/упало)
- **Покрытие** — есть ли метрики покрытия
### 1.6. Базовая проверка (только для existing/scaffold)
- **Собирается ли проект?** — запустить сборку
- **Запускается ли проект?** — если возможно, проверить старт
- **Чистый ли git status?** — нет ли незакоммиченных изменений
### 1.7. Извлечение требований (только для greenfield/scaffold)
Если README содержит описание будущего проекта — извлечь и структурировать:
**Функциональные требования:**
- Пользовательские истории (user stories)
- Основные сценарии использования
- Входные/выходные данные системы
**Нефункциональные требования:**
- Технологические предпочтения (язык, фреймворк, БД)
- Требования к производительности, безопасности
- Ограничения (сроки, платформа, окружение)
**Бизнес-контекст:**
- Цель системы (зачем)
- Целевая аудитория
- Ключевые метрики успеха
**Сомнительные/неясные требования:**
- Вопросы, которые нужно задать пользователю перед проектированием
- Противоречия в README
## Выход
`.agent/analysis-report.md` по шаблону `TEMPLATES/analysis-report.md`.
Обновить checkpoints.json: `phases.analysis = "completed"`. Если проект `greenfield`, также установить `project_type = "greenfield"`.
## Критерии завершения фазы
- [ ] Тип проекта определён (existing / greenfield / scaffold)
- [ ] Все соответствующие разделы (1.1–1.7) выполнены
- [ ] `.agent/analysis-report.md` создан и заполнен
- [ ] checkpoints.json обновлён
+173
View File
@@ -0,0 +1,173 @@
# Протокол 02: Архитектурное проектирование (DESIGN)
## Цель
Спроектировать архитектуру, модули, данные и интерфейсы для greenfield/scaffold-проекта на основе требований из analysis-report.
## Вход
- `.agent/analysis-report.md` (project_type: greenfield или scaffold)
- `.agent/metaagent-request.md` (конфигурация сессии: adr, alternative_arch, risk_register)
- `.agent/checkpoints.json` (фаза design: pending)
## Правила
1. **Реалистичность** — архитектура должна быть реализуема исполнительным агентом за 1 сессию (до 10 задач)
2. **Документируемость** — каждый модуль, модель и интерфейс описывается в design-report.md
3. **Тестируемость** — каждый компонент проектируется с учётом того, как его тестировать
4. **Итеративность** — первая версия должна быть минимально рабочей (MVP), расширения — отдельными задачами
## Шаги
### 2.1. Технологический стек
Если стек не указан в README — предложить обоснованный выбор. Если указан — зафиксировать.
Для каждого компонента указать:
- Язык и версия
- Фреймворк / библиотека
- База данных (движок, схема)
- Инфраструктура (Docker, CI/CD, хостинг)
### 2.2. High-level архитектура
Описать общую структуру системы:
- **Архитектурный паттерн** — монолит, модульный монолит, микросервисы, слоистая, луковая и т.д.
- **Компоненты и их ответственность** — что делает каждый модуль/сервис
- **Схема взаимодействия** — текстовое описание потоков данных
Формат (text diagram):
```
[Client] → HTTP → [API Gateway] → [Auth Service]
[Core Service] → [Database]
[External API] → [3rd Party]
```
### 2.3. Модули проекта
Разбить систему на модули/пакеты. Для каждого:
| Поле | Описание |
|---|---|
| **Имя модуля** | `app/services/cashflow.py` |
| **Ответственность** | Что делает |
| **Ключевые классы/функции** | Только сигнатуры (без реализации) |
| **Зависимости** | Какие модули нужны этому |
| **Контракт** | Что экспортирует/предоставляет |
### 2.4. Модели данных
Описать основные сущности, их поля и связи:
```json
{
"entity": "Transaction",
"fields": [
{"name": "id", "type": "UUID", "pk": true},
{"name": "amount", "type": "Decimal"},
{"name": "date", "type": "datetime"},
{"name": "category_id", "type": "UUID", "fk": "Category"}
]
}
```
Если используется ORM — указать аннотации/декораторы.
Если БД — схему таблиц, индексы, ключи.
### 2.5. API интерфейсы
Если проектируется API — описать эндпоинты:
| Метод | Путь | Описание | Request | Response | Статусы |
|---|---|---|---|---|---|
| GET | /transactions | Список транзакций | ?page, ?limit | [Transaction] | 200 |
| POST | /transactions | Создать транзакцию | CreateTransactionDTO | Transaction | 201, 400 |
Если GUI — описать ключевые страницы/экраны.
Если CLI — описать команды.
### 2.6. Обработка ошибок
- Стратегия ошибок: исключения, Result-тип, коды ошибок
- Формат ошибок в API: `{ "error": "...", "code": "...", "details": {} }`
- Логирование: какой уровень для каких событий
### 2.7. Стратегия тестирования
- Какие тесты нужны (unit, integration, e2e)
- Как изолировать зависимости (mocks, fakes, testcontainers)
- Команда запуска тестов
### 2.8. Alternative Architecture (если config.alternative_arch = yes)
Описать **минимум одну принципиально иную архитектуру** и причину отказа:
| Критерий | Выбранная архитектура | Альтернатива |
|---|---|---|
| Название | Модульный монолит | Микросервисы / Событийная / и т.д. |
| Сложность реализации | Низкая | Высокая (3+ сервиса) |
| Масштабирование | Вертикальное | Горизонтальное |
| Почему не выбрана | — | Избыточно для MVP |
Это снижает риск архитектурной инерции: решение становится осознанным, а не единственным возможным.
### 2.9. ADR (если config.adr = yes)
Для каждого ключевого архитектурного решения (стек, БД, паттерн, структура модулей) создать отдельный ADR-файл:
```
.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/layer-1/risk-register.md` по шаблону `TEMPLATES/risk-register.md`:
| # | Assumption | Impact if wrong | Mitigation | Review trigger |
|---|---|---|---|---|
Задокументировать **неявные допущения**, на которых держится архитектура. Это даёт future-агентам знать, что можно пересматривать в первую очередь.
### 2.11. Группировка в задачи
На основе спроектированных модулей и моделей предварительно наметить группировку в задачи (по модулям). Это будет входом для DECOMPOSITION.
```
T1: Инициализация проекта + зависимости
T2: Модель данных (сущности, миграции)
T3: Cashflow Service (core logic)
T4: API endpoints
...и т.д.
```
## Выход
- `.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"`.
## Критерии завершения фазы
- [ ] Технологический стек определён
- [ ] High-level архитектура описана
- [ ] Модули и их ответственность описаны
- [ ] Модели данных спроектированы
- [ ] API/интерфейсы описаны (если применимо)
- [ ] Стратегия тестирования определена
- [ ] Alternative Architecture описана (если config требует)
- [ ] ADR созданы (если config требует)
- [ ] Risk Register создан (если config требует)
- [ ] Задачи предварительно сгруппированы
- [ ] `.agent/design-report.md` создан
- [ ] checkpoints.json обновлён
+80
View File
@@ -0,0 +1,80 @@
# Протокол 02b: Red Team Review (опционально)
## Цель
Преднамеренно попытаться разрушить спроектированную архитектуру, чтобы найти скрытые проблемы до начала реализации.
## Вход
- `.agent/design-report.md`
- `.agent/layer-1/adr/*.md` (если созданы)
- `.agent/metaagent-request.md` (глубина проработки >= 9)
## Когда выполняется
Только если `config.red_team = yes` (глубина 9-10). Выполняется **после** DESIGN, **до** DECOMPOSITION.
## Шаги
### RT1. Поиск скрытых зависимостей
Проверить каждый модуль на наличие неявных связей:
- Есть ли циклические зависимости между модулями?
- Есть ли модуль, который знает слишком много о других?
- Есть ли скрытый vendor lock-in (БД, облачный провайдер, внешний API)?
### RT2. Точки отказа
Для каждого внешнего интерфейса (API, БД, файловая система):
- Что произойдёт при отказе компонента?
- Есть ли fallback?
- Что произойдёт при невалидных входных данных?
### RT3. Масштабирование
Оценить поведение системы при:
- 10x рост данных
- 100x рост данных
- Добавлении нового пользователя / клиента
### RT4. Security (если применимо)
- Какие данные передаются по сети?
- Есть ли аутентификация?
- Хранятся ли секреты в коде?
### RT5. Consistency
Проверить design-report и ADR на противоречия:
- Одна сущность описана по-разному в двух местах?
- API-контракт не соответствует модели данных?
- Технологический стек противоречит нефункциональным требованиям?
## Выход
`.agent/layer-1/red-team-report.md` с секциями:
```
## Найденные проблемы
| # | Проблема | Серьёзность | Рекомендация |
|---|---|---|---|
## Отклонённые атаки (что пытались сломать — но не сломалось)
| # | Гипотеза | Почему не подтвердилась |
|---|---|---|
```
Обновить risk-register.md (если существует) новыми рисками.
## Критерии завершения
- [ ] Все 5 секций (RT1-RT5) проверены
- [ ] Найденные проблемы записаны в red-team-report.md
- [ ] Если найдены критические проблемы — design-report должен быть исправлен
- [ ] Risk Register дополнен (если существует)
+133
View File
@@ -0,0 +1,133 @@
# Протокол 03: Декомпозиция задач (DECOMPOSITION)
## Цель
Разбить цель пользователя (и архитектурный план, если есть) на атомарные, независимо выполнимые задачи и записать их в манифест.
## Вход
- `.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)
## Правила декомпозиции
### 3.1. Принципы
1. **Атомарность** — одна задача = одна логическая единица работы, которую можно выполнить и проверить за один подход
2. **Независимость (макс.)** — минимизировать зависимости между задачами
3. **Тестируемость** — каждая задача имеет измеримые acceptance criteria
4. **Границы** — задача не должна выходить за пределы, указанные в `BOUNDARIES.md`
5. **Порядок** — задачи с зависимостями выполняются строго последовательно
### 3.2. Размер задачи
Задача должна укладываться в **1-2 часа работы исполнительного агента**. Если задача крупнее — разбить на подзадачи.
Признак слишком крупной задачи:
- Нельзя сформулировать acceptance criteria одной строкой
- Затрагивает 5+ файлов
- Содержит союзы "и", "а также", "после чего"
### 3.3. Структура задачи
Каждая задача содержит:
| Поле | Описание | Пример |
|---|---|---|
| `id` | Уникальный идентификатор | `T1`, `T2` |
| `title` | Заголовок (что сделать) | "Добавить модель User" |
| `description` | Описание (как и зачем) | "Создать SQLAlchemy модель..." |
| `type` | Тип задачи | `feature`, `refactor`, `test`, `fix`, `config`, `design`, `docs` |
| `status` | Статус задачи | `pending`, `in_progress`, `completed`, `failed`, `archived` |
| `files` | Список файлов, которые нужно создать/изменить | `["app/models/user.py"]` |
| `depends_on` | ID задач, от которых зависит | `[]` или `["T0"]` |
| `acceptance_criteria` | Список критериев приёмки (3-5 пунктов) | `["Модель проходит миграцию"]` |
| `context` | Доп. информация (ссылки на доки, примеры, релевантные секции из design-report) | `"Смотри app/models/base.py"` |
### 3.4. Типы задач
| Тип | Описание |
|---|---|
| `config` | Настройка окружения, зависимостей, CI, инициализация проекта |
| `design` | Архитектурное/дизайнерское решение без кода |
| `feature` | Новая функциональность |
| `refactor` | Изменение структуры без изменения поведения |
| `test` | Добавление/исправление тестов |
| `fix` | Исправление бага |
| `docs` | Документация |
| `invariant` | Тест, проверяющий архитектурный инвариант (см. 3.7) |
### 3.5. Зелёная декомпозиция (для greenfield/scaffold)
Если есть `.agent/design-report.md` — задачи формируются на основе группировки из дизайна:
1. **T1: init** — инициализация проекта, зависимости, конфиги, scaffold
2. **T2..Tn: features** — модули/функциональность по одному
3. **Tn+1: tests** — тесты на каждый модуль (можно в составе feature-задачи)
4. **Tn+2: polish** — документация, форматирование, финальная проверка
### 3.6. Сортировка
Задачи в манифесте располагаются в порядке выполнения:
1. Сначала задачи без зависимостей
2. Потом те, чьи зависимости уже выполнены
3. Последними — задачи с наибольшим числом зависимостей
### 3.7. Executable Invariants (если config.invariant_tests = yes)
Для каждого ADR (из layer-1/adr/) создать задачу типа `invariant` — тест, проверяющий архитектурное правило.
**Правила превращения ADR в инварианты:**
| ADR | Инвариант-тест |
|---|---|
| "Модуль X не зависит от Y" | `test_x_does_not_import_y.py` — import test |
| "Слой Model не знает о CLI" | `test_model_layer_imports.py` — проверка import graph |
| "Все исключения кастомные" | `test_custom_exceptions.py` — проверка hierarchy |
| "Интерфейс репозитория не泄漏 implementation details" | `test_repository_interface.py` — ABC check |
**Формат задачи-инварианта:**
```json
{
"id": "I1",
"title": "Инвариант: model не импортирует cli",
"type": "invariant",
"files": ["tests/invariants/test_layer_imports.py"],
"depends_on": ["T2"],
"acceptance_criteria": [
"Тест проверяет, что cashflow_model не импортирует cli, sync, engine",
"Тест проходит на пустом проекте (до реализации функциональности)"
]
}
```
Инварианты размещаются в `tests/invariants/` и запускаются вместе с основными тестами.
## Выход
- `.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/`, в манифесте остаётся
> one-liner с `"status": "archived"`.
## Критерии завершения фазы
- [ ] Цель разбита на атомарные задачи
- [ ] Для каждой задачи указаны acceptance criteria
- [ ] Для каждой задачи указаны affected files
- [ ] Зависимости между задачами корректны (нет циклов)
- [ ] Invariant-задачи созданы для каждого ADR (если config требует)
- [ ] `.agent/task-manifest.json` и `.agent/task-manifest.md` созданы
- [ ] checkpoints.json обновлён
@@ -0,0 +1,126 @@
# Протокол 04: Настройка окружения (SETUP)
## Цель
Обеспечить рабочее окружение, в котором исполнительный агент может сразу выполнять задачи.
## Вход
- `.agent/analysis-report.md`
- `.agent/design-report.md` (опционально, для greenfield)
- `.agent/task-manifest.json`
- `.agent/checkpoints.json` (фаза environment: pending)
## Поведение в зависимости от типа проекта
Фаза SETUP работает по-разному для `existing` и `greenfield/scaffold` проектов.
---
## Ветка A: existing/scaffold проект
### 4A.1. Зависимости
- Установить все зависимости согласно документации проекта
- Если есть `requirements.txt`, `pyproject.toml`, `package.json`, `Cargo.toml` и т.д. — выполнить установку
- Если в проекте используется виртуальное окружение (venv, .venv, conda) — активировать или создать
- Если в проекте используется Docker — проверить, что образ собирается
**Правило:** если установка зависимостей требует нестандартных шагов, описанных в README — строго следовать им. Если шаги не описаны — запросить у пользователя.
### 4A.2. Конфигурация
- Проверить наличие конфигурационных файлов (`.env.example`, `.env`, `config.yaml`)
- Если есть `.env.example`, скопировать в `.env` с настройками по умолчанию
- Если проекту требуется БД — проверить строку подключения, при необходимости создать БД или использовать SQLite для разработки
- Настроить pre-commit хуки, если они есть в проекте
### 4A.3. Линтеры и форматтеры
- Запустить линтер на всём проекте: записать результат
- Если линтер выдаёт ошибки — не исправлять, только зафиксировать в отчёте
- Убедиться, что исполнительный агент может запускать линтер (записать команду)
### 4A.4. Baseline-тесты
- Запустить все тесты проекта
- Записать в `.agent/baseline-test-report.log`:
- Команда запуска
- Общее количество тестов
- Пройдено / упало / пропущено
- Время выполнения
- Список упавших тестов (если есть)
- Если тесты не проходят — указать это в отчёте, но **не исправлять**
### 4A.5. Сборка проекта
- Выполнить полную сборку/компиляцию проекта
- Записать результат (успех/ошибка с логом)
- Сборка должна проходить без ошибок. Если не собирается — остановиться, сообщить пользователю.
---
## Ветка B: greenfield проект
### 4B.1. Инициализация проекта
- Создать базовую структуру директорий согласно design-report.md
- Инициализировать пакетный менеджер:
- Python: `pyproject.toml` (poetry, pdm, hatch) или `requirements.txt`
- Node: `package.json` и `npm init` / `yarn init`
- Go: `go mod init`
- Rust: `cargo init`
- Настроить базовый конфиг: `.env.example`, `config/` и т.д.
- Настроить линтер/форматтер: `ruff`, `eslint`, `gofmt` и т.д.
### 4B.2. Scaffold-код
Создать пустые заглушки для модулей, описанных в design-report:
```python
# app/services/cashflow.py — заглушка
class CashflowService:
"""TBD — реализация в задаче T3"""
pass
```
Назначение: фиксировать структуру, чтобы исполнительный агент не думал о ней, а сразу писал реализацию.
### 4B.3. Установка зависимостей
- Установить базовые зависимости согласно стеку из design-report
- Если проект использует БД — установить драйвер/ORM
- Если проект использует API — установить фреймворк (FastAPI, Express и т.д.)
- Установить dev-зависимости: линтер, тестовый раннер, type stubs
### 4B.4. Базовые тесты (scaffold)
- Создать пустой тестовый файл для каждого модуля
- Настроить тестовый раннер (pytest, jest и т.д.)
- Записать в `.agent/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
### 4B.5. Проверка сборки
- Убедиться, что проект импортируется без ошибок
- Убедиться, что линтер проходит (без кода он должен проходить)
- Убедиться, что тестовый раннер запускается (0 tests, exit code 0)
---
## Выход
- Работоспособное окружение / инициализированный проект
- `.agent/baseline-test-report.log` — результат прогона тестов
- `.agent/setup-report.log` — лог установки зависимостей и сборки
Обновить checkpoints.json: `phases.environment = "completed"`.
## Критерии завершения фазы
- [ ] Зависимости установлены / проект инициализирован
- [ ] Проект собирается / импортируется без ошибок
- [ ] Baseline-тесты запущены, результат записан
- [ ] `.agent/baseline-test-report.log` и `.agent/setup-report.log` созданы
- [ ] checkpoints.json обновлён
Если проект не собирается — **фаза считается проваленной**, checkpoints.json отмечает `phases.environment = "failed"`, управление возвращается пользователю.
+188
View File
@@ -0,0 +1,188 @@
# Протокол 05: Передача исполнительному агенту (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)
## Шаги
### 5.1. Архивация завершённых артефактов
Перед валидацией и передачей выполнить архивирование.
**Архивировать завершённые задачи:**
Для каждой задачи в `task-manifest.json` со статусом `completed`:
1. Создать `.agent/archive/tasks/<id>.json` — перенести полное описание задачи (все поля)
2. В `task-manifest.json` заменить задачу на one-liner:
```json
{ "id": "<id>", "title": "<title>", "status": "archived" }
```
**Архивировать чекпоинты:**
Если `checkpoints.json` уже существует — сохранить предыдущую версию в `.agent/archive/checkpoints/<last_updated>.json`.
**Создать индекс архива:**
```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>" }
]
}
```
### 5.2. Валидация
Перед передачей проверить:
- [ ] Все фазы отмечены как `completed` в checkpoints.json
- [ ] `.agent/` содержит все обязательные файлы:
- `checkpoints.json`
- `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`
- `archive/index.json`
- [ ] Для greenfield: `design-report.md` присутствует
- [ ] В task-manifest.json нет циклических зависимостей
- [ ] Все acceptance criteria сформулированы измеримо
- [ ] Для каждой задачи указаны affected files
- [ ] В репозитории нет незакоммиченных изменений (кроме `.agent/`)
- [ ] `.agent/src/` содержит актуальные исходники MetaAgent (META_AGENT_GUIDE.md, PROTOCOLS/, TEMPLATES/, BOUNDARIES.md, VERSION)
- [ ] `AGENTS.md` присутствует в корне репозитория
- [ ] `.agent/rules/` содержит `project-rules.md`
**Дополнительные проверки (если config включает):**
- [ ] ADR присутствуют (если adr=yes)
- [ ] Risk Register заполнен (если risk_register=yes)
- [ ] Red Team Report есть (если red_team=yes)
- [ ] Invariant-задачи в манифесте (если invariant_tests=yes)
### 5.3. Layer-структура .agent/
Если `config.layer_structure = yes`, организовать артефакты по слоям:
```
.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
```
Если `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>
Config: depth=<N>, adr=<yes|no>, red_team=<yes|no>, ...
Tasks: <count> tasks ready
Исполнительный агент может начинать с задачи <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/layer-0/session-summary.md` (если layer_structure=yes)
- `.agent/layer-3/handoff-summary.md`
- `.agent/layer-0/checkpoints.json` (финальный)
- `.agent/archive/index.json` (создаётся при архивации)
## Критерии завершения
- [ ] Все артефакты на месте (с учётом 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 финализирован
- [ ] Сигнал отправлен пользователю/оркестратору
+23
View File
@@ -0,0 +1,23 @@
# ADR-NNNN: <Заголовок решения>
**Статус:** proposed | accepted | deprecated | superseded
**Дата:** {{ date }}
**Контекст:** почему возникла необходимость в решении, какая проблема решается.
**Рассматриваемые альтернативы:**
1. Вариант A — описание
2. Вариант B — описание
3. Вариант C — описание
**Решение:** выбран вариант <A/B/C>.
**Обоснование:** почему выбран именно этот вариант (критерии: сложность, поддерживаемость, производительность, совместимость).
**Последствия:**
- Позитивные: ...
- Негативные: ...
- Риски: ...
**Invariant (если применимо):** ключевое правило, которое не должен нарушать исполнительный агент. Если можно — ссылка на тест, проверяющий invariant.
+101
View File
@@ -0,0 +1,101 @@
# Analysis Report
## Session
- **Session ID:** `{{ session_id }}`
- **Target repo:** `{{ target_repo }}`
- **Date:** {{ date }}
- **Project type:** `{{ project_type }}` (existing / greenfield / scaffold)
## 1. Общая информация
- **README:** {{ readme_summary }}
- **Лицензия:** {{ license }}
- **CI/CD:** {{ ci_cd }}
- **Точка входа:** {{ entry_point }}
- **Система сборки:** {{ build_system }}
{% if project_type == "existing" or project_type == "scaffold" %}
## 2. Стек технологий
| Компонент | Значение |
|---|---|
| Язык | {{ language }} |
| Фреймворк | {{ framework }} |
| База данных | {{ database }} |
| Тестовый раннер | {{ test_runner }} |
| Пакетный менеджер | {{ package_manager }} |
| Линтер/форматтер | {{ linter }} |
## 3. Архитектура
```
{{ directory_tree }}
```
**Паттерн:** {{ architecture_pattern }}
**Ключевые модули:**
| Модуль | Описание |
|---|---|
| {{ module }} | {{ description }} |
## 4. Конвенции
- **Стиль:** {{ code_style }}
- **Импорты:** {{ import_style }}
- **Типизация:** {{ typing_usage }}
- **Обработка ошибок:** {{ error_handling }}
- **Логирование:** {{ logging }}
## 5. Тесты
- **Команда запуска:** `{{ test_command }}`
- **Всего тестов:** {{ total_tests }}
- **Пройдено:** {{ passed }}
- **Упало:** {{ failed }}
- **Пропущено:** {{ skipped }}
- **Упавшие тесты:**
{% for test in failed_tests %}
- `{{ test }}`
{% endfor %}
## 6. Базовая проверка
- **Сборка:** {{ build_status }}
- **Запуск:** {{ run_status }}
- **Git status:** {{ git_status }}
{% endif %}
{% if project_type == "greenfield" or project_type == "scaffold" %}
## 7. Требования (из README)
### Функциональные требования
{% for req in functional_requirements %}
- {{ req }}
{% endfor %}
### Нефункциональные требования
{% for req in non_functional_requirements %}
- {{ req }}
{% endfor %}
### Бизнес-контекст
{% for item in business_context %}
- {{ item }}
{% endfor %}
### Неясные моменты / Вопросы
{% for question in open_questions %}
- {{ question }}
{% endfor %}
{% endif %}
## 8. Примечания
{{ notes }}
+120
View File
@@ -0,0 +1,120 @@
# Design Report
## Session
- **Session ID:** `{{ session_id }}`
- **Target repo:** `{{ target_repo }}`
- **Date:** {{ date }}
## 1. Технологический стек
| Компонент | Выбор | Обоснование |
|---|---|---|
| Язык | {{ language }} | {{ language_rationale }} |
| Фреймворк | {{ framework }} | {{ framework_rationale }} |
| База данных | {{ database }} | {{ database_rationale }} |
| Инфраструктура | {{ infrastructure }} | {{ infrastructure_rationale }} |
## 2. High-Level архитектура
**Паттерн:** {{ architecture_pattern }}
```
{{ architecture_diagram }}
```
**Поток данных:**
1. {{ data_flow_step_1 }}
2. {{ data_flow_step_2 }}
3. {{ data_flow_step_3 }}
## 3. Модули
| Модуль | Ответственность | Ключевые компоненты | Зависит от |
|---|---|---|---|
| `{{ module_path }}` | {{ responsibility }} | {{ components }} | {{ dependencies }} |
## 4. Модели данных
### Сущности
{% for entity in entities %}
### {{ entity.name }}
| Поле | Тип | Ограничения | Описание |
|---|---|---|---|
{% for field in entity.fields %}
| {{ field.name }} | {{ field.type }} | {{ field.constraints }} | {{ field.description }} |
{% endfor %}
**Связи:** {{ entity.relationships }}
{% endfor %}
## 5. API / Интерфейсы
{% if has_api %}
| Метод | Путь | Описание | Request | Response |
|---|---|---|---|---|
{% for endpoint in api_endpoints %}
| {{ endpoint.method }} | {{ endpoint.path }} | {{ endpoint.description }} | {{ endpoint.request }} | {{ endpoint.response }} |
{% endfor %}
{% endif %}
{% if has_gui %}
**Экраны:** {{ gui_screens }}
{% endif %}
{% if has_cli %}
**Команды:** {{ cli_commands }}
{% endif %}
## 6. Обработка ошибок
- **Стратегия:** {{ error_strategy }}
- **Формат ошибок:** {{ error_format }}
- **Логирование:** {{ logging_strategy }}
## 7. Тестирование
- **Unit-тесты:** {{ unit_test_strategy }}
- **Integration-тесты:** {{ integration_test_strategy }}
- **Mock-стратегия:** {{ mock_strategy }}
- **Команда запуска:** `{{ test_command }}`
## 8. Alternative Architecture (если применимо)
| Критерий | Выбранная архитектура | Альтернатива |
|---|---|---|
| Название | {{ chosen_arch }} | {{ alt_arch }} |
| Сложность | {{ chosen_complexity }} | {{ alt_complexity }} |
| Почему не выбрана | — | {{ alt_rejection_reason }} |
## 9. ADR Reference (если применимо)
| ID | Решение | Файл |
|---|---|---|
{% for adr in adr_list %}
| {{ adr.id }} | {{ adr.title }} | `{{ adr.path }}` |
{% endfor %}
## 10. Risk Register (если применимо)
| # | Assumption | Impact | Mitigation |
|---|---|---|---|
{% for risk in risk_list %}
| {{ risk.id }} | {{ risk.assumption }} | {{ risk.impact }} | {{ risk.mitigation }} |
{% endfor %}
## 11. Предварительная группировка задач
| Задача | Описание | Тип |
|---|---|---|
| T1 | {{ task_1 }} | config |
| T2 | {{ task_2 }} | feature |
| T3 | {{ task_3 }} | feature |
| T4 | {{ task_4 }} | test |
## 12. Примечания
{{ notes }}
+84
View File
@@ -0,0 +1,84 @@
# Handoff Summary
## Session Info
- **Session ID:** `{{ session_id }}`
- **Target Repo:** `{{ target_repo }}`
- **Goal:** {{ goal }}
- **Date:** {{ date }}
- **Duration:** {{ duration }}
- **Depth:** {{ depth }}
- **Config:** {{ config_summary }}
## Repo Summary
{{ repo_summary }}
## Project Type
- **Type:** {{ project_type }}
- **Design report:** {% if project_type == "greenfield" or project_type == "scaffold" %}`.agent/design-report.md`{% else %}—{% endif %}
## ADR Summary (если применимо)
{% if adr_count > 0 %}
Создано ADR: {{ adr_count }}
{% for adr in adr_list %}
- `{{ adr.path }}` — {{ adr.title }}
{% endfor %}
{% endif %}
## Risk Register (если применимо)
{% if risk_count > 0 %}
Задокументировано допущений: {{ risk_count }}
Наиболее критичное: {{ top_risk }}
{% endif %}
## Environment Status
- **Build:** {{ build_status }}
- **Tests:** {{ tests_passed }}/{{ tests_total }} passed
- **Baseline log:** `.agent/baseline-test-report.log`
- **Dependencies:** {{ deps_status }}
## Task Overview
| Status | Count |
|---|---|
| Total | {{ total }} |
| Pending | {{ pending }} |
| In Progress | {{ in_progress }} |
| Completed | {{ completed }} |
| Failed/Skipped | {{ failed }} |
**Task by type:**
{% for type, count in tasks_by_type %}
- {{ type }}: {{ count }}
{% endfor %}
## Tasks (ordered)
{% for task in tasks %}
### {{ task.id }}: {{ task.title }}
- Type: {{ task.type }}
- Depends on: {{ task.depends_on | default("—") }}
- Files: {{ task.files | join(", ") }}
- Status: {{ task.status }}
{% endfor %}
## Next Steps
Исполнительный агент начинает с задачи **{{ first_task }}**.
## Caveats
{% for caveat in caveats %}
- {{ caveat }}
{% endfor %}
## Checkpoints
Файл: `.agent/checkpoints.json`
Актуальное состояние чекпоинтов прилагается.
+38
View File
@@ -0,0 +1,38 @@
# MetaAgent Request
# Для ручного заполнения перед запуском MetaAgent.
# Поместите этот файл в .agent/metaagent-request.md целевого репозитория.
# Если файл отсутствует — MetaAgent проведёт интервью (PROTOCOLS/00_CONFIG.md).
# Ответьте "default" на любой вопрос — будет использовано значение по умолчанию.
## Параметры сессии
| Функция | Вкл | Аргументы |
|---|---|---|
| ANALYSIS | ✓ | — |
| DESIGN | ✓ | adr=yes, alternative_arch=yes |
| RED_TEAM | ✗ | — |
| RISK_REGISTER | ✗ | — |
| DECOMPOSITION | ✓ | invariant_tests=yes |
| SETUP | ✓ | — |
| HANDOFF | ✓ | layer_structure=yes |
## Глубина проработки
**Значение:** 6 (1-10)
| Уровень | Название | Описание |
|---|---|---|
| 1-2 | Scaffold | Только структура проекта + пустые модули |
| 3-4 | Light | (default) Быстрый дизайн + задачи без расширений |
| 5-6 | Standard | Полный ANALYSIS→DESIGN→DECOMP→SETUP→HANDOFF |
| 7-8 | Deep | Standard + ADR, Risk Register, Alternative Architecture |
| 9-10 | Maximum | Deep + Red Team Review, Executable Invariants |
## Цель
Сформулируйте задачу для MetaAgent.
## Дополнительно
- **Boundaries:** (опционально) ограничения, которые нельзя нарушать
- **Target:** путь к репозиторию или URL
+17
View File
@@ -0,0 +1,17 @@
# Project Rules
Правила, которым агент обязан следовать во всех фазах.
Добавляйте сюда условия, которые должны соблюдаться всегда — они будут прочитаны
перед началом каждой фазы и учтены при декомпозиции и реализации.
## Обязательные правила
- (укажите правила, например: «Всегда использовать tabs для отступов»)
## Запреты
- (укажите запреты, например: «Не трогать CI/CD конфигурацию»)
## Конвенции проекта
- (укажите конвенции, например: «Имена классов в PascalCase, функции в snake_case»)
+7
View File
@@ -0,0 +1,7 @@
# Risk Register
| # | Assumption | Impact if wrong | Mitigation | Review trigger |
|---|---|---|---|---|
| R1 | Пользователи имеют Python 3.11+ | Проект не запускается на старых версиях | Указать требование в README, CI-проверка | При жалобе на установку |
| R2 | JSON-файлы не превышают 10MB | Деградация производительности | Добавить лимит в model.py | При первом замедлении |
| R3 | ... | ... | ... | ... |
+35
View File
@@ -0,0 +1,35 @@
# Session Summary
**Session:** {{ session_id }}
**Target:** {{ target_repo }}
**Depth:** {{ depth }}
**Date:** {{ date }}
## Configuration
| Функция | Статус |
|---|---|
| ADR | {{ adr_enabled }} |
| Alternative Architecture | {{ alt_arch_enabled }} |
| Red Team | {{ red_team_enabled }} |
| Risk Register | {{ risk_register_enabled }} |
| Invariant Tests | {{ invariant_tests_enabled }} |
| Layer Structure | {{ layer_structure_enabled }} |
## Phase Status
| Phase | Status |
|---|---|
| ANALYSIS | {{ analysis_status }} |
| DESIGN | {{ design_status }} |
| RED_TEAM | {{ red_team_status }} |
| DECOMPOSITION | {{ decomposition_status }} |
| SETUP | {{ setup_status }} |
| HANDOFF | {{ handoff_status }} |
## Quick Links
- Task Manifest: `.agent/task-manifest.json`
- Handoff Summary: `.agent/handoff-summary.md`
- Design Report: `.agent/analysis-report.md`
- ADR: `.agent/layer-1/adr/` (если есть)
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "metaagent-task-manifest",
"version": "1.0",
"session_id": "{{ session_id }}",
"goal": "{{ goal }}",
"created_at": "{{ timestamp }}",
"tasks": [
{
"id": "T1",
"title": "{{ task_title }}",
"description": "{{ task_description }}",
"type": "feature|refactor|test|fix|config|docs",
"files": ["path/to/file1.py", "path/to/file2.py"],
"depends_on": [],
"acceptance_criteria": [
"Критерий 1: ...",
"Критерий 2: ..."
],
"context": "Дополнительная информация",
"status": "pending"
}
]
}
+42
View File
@@ -0,0 +1,42 @@
# Task Manifest
**Session:** {{ session_id }}
**Goal:** {{ goal }}
**Date:** {{ timestamp }}
---
## Task Overview
| ID | Title | Type | Depends On | Status |
|---|---|---|---|---|
| T1 | {{ title }} | {{ type }} | — | pending |
| T2 | {{ title }} | {{ type }} | T1 | pending |
**Total tasks:** {{ count }}
---
## Task Details
### T1: {{ title }}
**Type:** {{ type }}
**Description:** {{ description }}
**Files:**
- `{{ file_path }}`
**Depends on:**
**Acceptance Criteria:**
- [ ] {{ criterion }}
- [ ] {{ criterion }}
**Context:** {{ context }}
---
### T2: {{ title }}
...
+1
View File
@@ -0,0 +1 @@
1.1.0
+356
View File
@@ -0,0 +1,356 @@
# WORKFLOW — Сквозной пример сессии
---
## Сценарий A: Existing проект
**Цель:** Добавить в существующий FastAPI-проект ручку GET /health с тестами.
**Целевой репозиторий:** `github.com/example/fastapi-app`
**Пользователь:** "Добавь health-check endpoint и тесты к нему"
---
### Фаза INIT
Мета-агент читает `.agent/metaagent-request.md`, клонирует репозиторий, создаёт `.agent/`, пишет начальный чекпоинт:
```json
{
"metaagent_version": "1.1.0",
"session_id": "ses_abc123",
"target_repo": "/tmp/fastapi-app",
"goal": "Добавить GET /health с тестами",
"project_type": "existing",
"config": {
"depth": 6,
"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": "2026-07-12T15:00:00Z"
}
```
---
### Фаза ANALYSE
Мета-агент выполняет `PROTOCOLS/01_ANALYSIS.md`. Определяет тип проекта: `existing`.
Результат `.agent/analysis-report.md`:
```markdown
## 2. Стек технологий
| Язык | Python 3.12 |
| Фреймворк | FastAPI |
| Тестовый раннер | pytest + httpx |
| Пакетный менеджер | pip + requirements.txt |
## 3. Архитектура
├── app/
│ ├── main.py
│ ├── routers/
│ │ └── users.py
│ ├── models/
│ │ └── user.py
│ └── schemas/
│ └── user.py
├── tests/
│ └── test_users.py
```
Тесты запущены: **12 passed, 0 failed**.
Чекпоинт обновлён: `analysis = "completed"`, `project_type = "existing"`.
Фаза DESIGN пропускается.
---
### Фаза DECOMPOSITION
Мета-агент выполняет `PROTOCOLS/03_DECOMPOSITION.md`.
Декомпозиция цели "Добавить GET /health с тестами":
| ID | Задача | Тип | Зависит от | AC |
|---|---|---|---|---|
| T1 | Создать health-check router | feature | — | Ручка возвращает 200 + {"status":"ok"} |
| T2 | Подключить router в main.py | config | T1 | Ручка доступна по /health |
| T3 | Написать тесты для /health | test | T2 | Тесты проверяют 200 и структуру ответа |
Создан `.agent/task-manifest.json` и `.agent/task-manifest.md`.
Чекпоинт обновлён: `decomposition = "completed"`. Tasks: T1-T3 со статусом `pending`.
---
### Фаза SETUP
Мета-агент выполняет `PROTOCOLS/04_ENVIRONMENT_SETUP.md` (ветка A: existing).
- `pip install -r requirements.txt` — OK
- Запуск pytest — OK, 12 passed (базовый тест)
- Результат в `.agent/baseline-test-report.log`
Чекпоинт обновлён: `environment = "completed"`.
---
### Фаза HANDOFF
Мета-агент выполняет `PROTOCOLS/05_HANDOFF.md`.
Создан `.agent/handoff-summary.md`:
```markdown
## Next Steps
Исполнительный агент начинает с задачи T1: "Создать health-check router".
## Caveats
- Придерживаться стиля существующего роутера users.py
- Не менять существующие тесты
- Убедиться, что response model соответствует JSON: {"status": "ok"}
```
Чекпоинт финализирован:
```json
{
"metaagent_version": "1.1.0",
"session_id": "ses_abc123",
"goal": "Добавить GET /health с тестами",
"project_type": "existing",
"config": {
"depth": 6,
"design": { "adr": false, "alternative_arch": false },
"red_team": false,
"risk_register": false,
"decomposition": { "invariant_tests": false },
"handoff": { "layer_structure": false }
},
"phases": {
"analysis": "completed",
"design": "skipped",
"red_team": "skipped",
"decomposition": "completed",
"environment": "completed",
"handoff": "completed"
},
"tasks": [
{ "id": "T1", "title": "Создать health-check router", "status": "pending" },
{ "id": "T2", "title": "Подключить router в main.py", "status": "pending" },
{ "id": "T3", "title": "Написать тесты для /health", "status": "pending" }
],
"last_updated": "2026-07-12T15:15:00Z"
}
```
Сигнал пользователю:
```
HANDOFF COMPLETE
Session: ses_abc123
Target: /tmp/fastapi-app
Type: existing
Tasks: 3 tasks ready
Исполнительный агент может начинать с задачи T1.
```
---
## Сценарий B: Greenfield проект (Cashflow Forecasting)
**Цель:** Спроектировать и реализовать MVP сервиса прогнозирования денежных потоков.
**Целевой репозиторий:** `github.com/example/cashflow-app`
**README:** README содержит описание:
> Сервис для прогнозирования движения денежных средств (cashflow forecasting).
> Пользователь загружает CSV с транзакциями, сервис строит прогноз на N дней вперёд.
> Стек: Python, FastAPI, SQLite, matplotlib для графиков.
---
### Фаза INIT
```json
{
"metaagent_version": "1.1.0",
"session_id": "ses_def456",
"target_repo": "/tmp/cashflow-app",
"goal": "Спроектировать и реализовать MVP сервиса прогнозирования денежных потоков",
"project_type": "greenfield",
"config": {
"depth": 7,
"design": { "adr": true, "alternative_arch": true },
"red_team": false,
"risk_register": true,
"decomposition": { "invariant_tests": true },
"handoff": { "layer_structure": true }
},
"phases": {
"analysis": "pending",
"design": "pending",
"red_team": "pending",
"decomposition": "pending",
"environment": "pending",
"handoff": "pending"
},
"tasks": [],
"last_updated": "2026-07-12T16:00:00Z"
}
```
---
### Фаза ANALYSE
Мета-агент выполняет `PROTOCOLS/01_ANALYSIS.md`. Определяет тип проекта: `greenfield`.
Сканирование корня: пусто (кроме README.md, LICENSE, .gitignore).
Извлечение требований из README:
| Тип | Требование |
|---|---|
| Функциональное | Загрузка CSV с транзакциями |
| Функциональное | Прогноз на N дней вперёд |
| Нефункциональное | Python, FastAPI |
| Нефункциональное | SQLite |
| Нефункциональное | matplotlib для графиков |
Чекпоинт: `analysis = "completed"`, `project_type = "greenfield"`.
Так как проект greenfield — мета-агент переходит к фазе DESIGN.
---
### Фаза DESIGN
Мета-агент выполняет `PROTOCOLS/02_DESIGN.md`.
Результат `.agent/design-report.md`:
```markdown
## 1. Технологический стек
| Язык | Python 3.12 |
| Фреймворк | FastAPI + Pydantic |
| БД | SQLite + SQLAlchemy |
| Визуализация | matplotlib |
| Тесты | pytest |
## 2. Архитектура
[Client] → HTTP → [FastAPI] → [CashflowService] → [SQLite]
[ForecastEngine] → [matplotlib]
## 3. Модули
| Модуль | Ответственность |
|---|---|
| app/main.py | Точка входа, роуты |
| app/models/transaction.py | Модель транзакции |
| app/services/cashflow.py | Бизнес-логика |
| app/services/forecast.py | Алгоритм прогноза |
| app/services/upload.py | Парсинг CSV |
| app/schemas/ | Pydantic схемы |
## 4. Модели
Transaction: id, date, amount, category, description
## 5. API
POST /upload — загрузить CSV
GET /forecast?days=30 — прогноз + график
## 6. Задачи (pre-grouped)
T1: init — проект, зависимости, scaffold
T2: models — модели + миграции
T3: upload — загрузка CSV
T4: forecast — алгоритм прогноза
T5: API — endpoints
T6: tests — тесты
```
Чекпоинт: `design = "completed"`.
---
### Фаза DECOMPOSITION
Мета-агент выполняет `PROTOCOLS/03_DECOMPOSITION.md`, используя design-report.
Итоговые задачи:
| ID | Задача | Тип | Зависит от |
|---|---|---|---|
| T1 | Инициализация проекта + зависимости | config | — |
| T2 | Модель Transaction + SQLAlchemy + SQLite | feature | T1 |
| T3 | Сервис загрузки и парсинга CSV | feature | T2 |
| T4 | ForecastEngine — алгоритм прогноза | feature | T2 |
| T5 | API endpoints + документация | feature | T3, T4 |
| T6 | Тесты (unit + integration) | test | T5 |
---
### Фаза SETUP
Мета-агент выполняет `PROTOCOLS/04_ENVIRONMENT_SETUP.md` (ветка B: greenfield).
- `poetry init` + создание pyproject.toml
- Установка fastapi, uvicorn, sqlalchemy, matplotlib, pytest
- Создание scaffold-структуры: `app/models/`, `app/services/`, `app/schemas/`, `tests/`
- Пустые заглушки модулей
- `.agent/baseline-test-report.log`: "0 tests — greenfield, scaffold готов"
---
### Фаза HANDOFF
```markdown
HANDOFF COMPLETE
Session: ses_def456
Target: /tmp/cashflow-app
Type: greenfield
Config: depth=7, adr=yes, risk_register=yes, invariant_tests=yes
Tasks: 6 tasks ready
Исполнительный агент может начинать с задачи T1 (init).
Архитектурный план: .agent/design-report.md
ADR: .agent/layer-1/adr/
```
---
## После HANDOFF: работа исполнительного агента
Исполнительный агент читает `.agent/handoff-summary.md`, `.agent/task-manifest.json`, выполняет задачи по порядку, обновляя checkpoints.json после каждой.
После завершения всех задач:
```
ALL TASKS COMPLETE
Session: ses_def456
Tasks: 6/6 completed
T1: Инициализация проекта ✓
T2: Модель Transaction ✓
T3: Сервис загрузки CSV ✓
T4: ForecastEngine ✓
T5: API endpoints ✓
T6: Тесты ✓
Все тесты проходят: 24/24 passed.
```
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env pwsh
# MetaAgent — установка исходников в целевой проект
# Usage: .\install.ps1 [[-Path] target_path] [-Update]
param(
[string]$Path = "",
[switch]$Update,
[switch]$Help
)
$MetaAgentSrc = Split-Path -Parent $MyInvocation.MyCommand.Path
function Show-Usage {
@"
Usage: install.ps1 [[-Path] target_path] [-Update] [-Help]
Install MetaAgent sources into <target>/.agent/src/
Options:
-Path Path to target project (default: interactive prompt)
-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 -Update
"@
exit 0
}
if ($Help) { Show-Usage }
$TargetPath = $Path
if (-not $TargetPath) {
$TargetPath = Read-Host "Enter path to target project"
}
$TargetPath = $TargetPath.Trim()
if (-not (Test-Path $TargetPath -PathType Container)) {
Write-Error "Directory '$TargetPath' does not exist."
exit 1
}
$TargetPath = (Resolve-Path $TargetPath).Path
$AgentDir = Join-Path $TargetPath ".agent"
$SrcDir = Join-Path $AgentDir "src"
$RulesDir = Join-Path $AgentDir "rules"
$ArchiveDir = Join-Path $AgentDir "archive"
$VersionFile = Join-Path $MetaAgentSrc "VERSION"
$Version = if (Test-Path $VersionFile) { Get-Content $VersionFile -Raw | ForEach-Object { $_.Trim() } } else { "?" }
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
if (-not (Test-Path $Src -PathType Leaf)) {
Write-Host " [skip] $name (not found)"
return
}
$dst = Join-Path $DstDir $name
if ($Update -or -not (Test-Path $dst)) {
Copy-Item $Src $dst -Force
Write-Host " [copy] $name"
} else {
Write-Host " [skip] $name (exists, use -Update to overwrite)"
}
}
function Copy-Dir {
param([string]$Src, [string]$DstDir)
$name = Split-Path $Src -Leaf
if (-not (Test-Path $Src -PathType Container)) {
Write-Host " [skip] $name/ (not found)"
return
}
$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-Host " [copy] $name/"
}
Copy-File (Join-Path $MetaAgentSrc "META_AGENT_GUIDE.md") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "BOUNDARIES.md") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "WORKFLOW.md") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "VERSION") $SrcDir
Copy-Dir (Join-Path $MetaAgentSrc "PROTOCOLS") $SrcDir
Copy-Dir (Join-Path $MetaAgentSrc "TEMPLATES") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.sh") $SrcDir
Copy-File (Join-Path $MetaAgentSrc "install.ps1") $SrcDir
# --- 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)) {
New-AgentsMd $AgentsMd | Out-File -FilePath $AgentsMd -Encoding utf8
Write-Host " [create] AGENTS.md"
} elseif ($Update) {
New-AgentsMd $AgentsMd | Out-File -FilePath $AgentsMd -Encoding utf8
Write-Host " [update] AGENTS.md"
} else {
Write-Host " [skip] AGENTS.md (exists, use -Update to overwrite)"
}
Write-Host ""
Write-Host "Done! MetaAgent v$Version installed at $SrcDir"
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# MetaAgent — установка исходников в целевой проект
# Usage: ./install.sh [--update] [target_path]
set -euo pipefail
METAAGENT_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
usage() {
cat <<EOF
Usage: $0 [--update] [target_path]
Install MetaAgent sources into <target>/.agent/src/
Options:
--update, -u Overwrite existing files in .agent/src/
--help, -h Show this help
Examples:
$0
$0 /path/to/project
$0 --update /path/to/project
EOF
exit 0
}
UPDATE=false
TARGET_PATH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--update|-u) UPDATE=true; shift ;;
--help|-h) usage ;;
--*) echo "Unknown option: $1"; usage ;;
*) TARGET_PATH="$1"; shift ;;
esac
done
if [[ -z "$TARGET_PATH" ]]; then
read -r -p "Enter path to target project: " TARGET_PATH
fi
TARGET_PATH="${TARGET_PATH/#\~/$HOME}"
TARGET_PATH="$(cd "$TARGET_PATH" 2>/dev/null && pwd)" || {
echo "Error: Directory '$TARGET_PATH' does not exist."
exit 1
}
AGENT_DIR="$TARGET_PATH/.agent"
SRC_DIR="$AGENT_DIR/src"
VERSION="$(cat "$METAAGENT_SRC/VERSION" 2>/dev/null || echo '?')"
RULES_DIR="$AGENT_DIR/rules"
ARCHIVE_DIR="$AGENT_DIR/archive"
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")"
if [[ ! -f "$src" ]]; then
echo " [skip] $name (not found)"
return
fi
if [[ "$UPDATE" == true ]] || [[ ! -f "$dst_dir/$name" ]]; then
cp "$src" "$dst_dir/$name"
echo " [copy] $name"
else
echo " [skip] $name (exists, use --update to overwrite)"
fi
}
copy_dir() {
local src="$1" dst_dir="$2"
local name; name="$(basename "$src")"
if [[ ! -d "$src" ]]; then
echo " [skip] $name/ (not found)"
return
fi
mkdir -p "$dst_dir/$name"
if [[ "$UPDATE" == true ]]; then
cp -rf "$src"/* "$dst_dir/$name/" 2>/dev/null || true
else
cp -rn "$src"/* "$dst_dir/$name/" 2>/dev/null || true
fi
echo " [copy] $name/"
}
copy_file "$METAAGENT_SRC/META_AGENT_GUIDE.md" "$SRC_DIR"
copy_file "$METAAGENT_SRC/BOUNDARIES.md" "$SRC_DIR"
copy_file "$METAAGENT_SRC/WORKFLOW.md" "$SRC_DIR"
copy_file "$METAAGENT_SRC/VERSION" "$SRC_DIR"
copy_dir "$METAAGENT_SRC/PROTOCOLS" "$SRC_DIR"
copy_dir "$METAAGENT_SRC/TEMPLATES" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.sh" "$SRC_DIR"
copy_file "$METAAGENT_SRC/install.ps1" "$SRC_DIR"
# --- create / update AGENTS.md in root of target ---
AGENTS_MD="$TARGET_PATH/AGENTS.md"
create_agents_md() {
cat > "$1" << AGENTS_EOF
# 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 до актуальной версии.
AGENTS_EOF
}
if [[ ! -f "$AGENTS_MD" ]]; then
create_agents_md "$AGENTS_MD"
echo " [create] AGENTS.md"
elif [[ "$UPDATE" == true ]]; then
create_agents_md "$AGENTS_MD"
echo " [update] AGENTS.md"
else
echo " [skip] AGENTS.md (exists, use --update to overwrite)"
fi
echo ""
echo "Done! MetaAgent v$VERSION installed at $SRC_DIR"
+106
View File
@@ -0,0 +1,106 @@
{
"$schema": "metaagent-task-manifest",
"version": "1.0",
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"goal": "Проанализировать существующий Obsidian-плагин zeroq-qol-modules (v1.1.4) и подготовить манифест задач для исполнительного агента.",
"created_at": "2026-07-22T00:00:00.000Z",
"tasks": [
{
"id": "T1",
"title": "Настроить ESLint и Prettier",
"description": "Добавить конфигурацию ESLint (typescript-eslint) и Prettier для единого стиля кода. Настроить скрипты lint/format в package.json.",
"type": "config",
"files": [
"package.json",
".eslintrc.json",
".prettierrc.json"
],
"depends_on": [],
"acceptance_criteria": [
"ESLint настроен с typescript-eslint, проходит без ошибок на src/",
"Prettier настроен с tabs, проходит без ошибок на src/",
"Скрипты lint и format добавлены в package.json"
],
"context": "В проекте используются tabs для отступов (см. review.md). Линтер/форматтер отсутствует.",
"status": "pending"
},
{
"id": "T2",
"title": "Написать тесты для BaseModule",
"description": "Добавить unit-тесты для BaseModule: проверка registerCleanup, onunload (очистка всех функций), defaultSettings.",
"type": "test",
"files": [
"src/modules/base-module.ts",
"tests/modules/base-module.test.ts",
"package.json",
"tsconfig.json"
],
"depends_on": [],
"acceptance_criteria": [
"Тест проверяет, что registerCleanup добавляет функцию в очередь",
"Тест проверяет, что onunload вызывает все зарегистрированные cleanup-функции",
"Тест проверяет, что onunload очищает очередь после выполнения",
"Тест проверяет, что defaultSettings возвращает пустой объект",
"Все тесты проходят (npm test)"
],
"context": "BaseModule — абстрактный класс-основа для всех модулей. Находится в src/modules/base-module.ts. Для тестов нужно настроить тестовый раннер (jest или vitest) и mock-объект Plugin из obsidian.",
"status": "pending"
},
{
"id": "T3",
"title": "Написать тесты для Attachment Clean Paste модуля",
"description": "Добавить unit-тесты для AttachmentCleanPasteModule: проверка shouldProcess (фильтрация расширений), обработка paste/drop событий.",
"type": "test",
"files": [
"src/modules/attachment-clean-paste/index.ts",
"tests/modules/attachment-clean-paste.test.ts"
],
"depends_on": ["T2"],
"acceptance_criteria": [
"Тест проверяет shouldProcess с разными расширениями и настройками",
"Тест проверяет, что обработчик paste вызывает preventDefault для файлов с подходящими расширениями",
"Тест проверяет, что модуль корректно создаёт ссылку [[path|name]]",
"Все тесты проходят (npm test)"
],
"context": "Attachment Clean Paste обрабатывает editor-paste и editor-drop события. Основная логика: shouldProcess (фильтрация по extensions) и processFile (копирование + создание ссылки).",
"status": "pending"
},
{
"id": "T4",
"title": "Написать тесты для Preserve Link Aliases модуля",
"description": "Добавить unit-тесты для PreserveLinkAliasesModule: проверка processContent (замена алиасов), getFilesToProcess (фильтрация файлов), обработка rename.",
"type": "test",
"files": [
"src/modules/preserve-link-aliases/index.ts",
"tests/modules/preserve-link-aliases.test.ts"
],
"depends_on": ["T2"],
"acceptance_criteria": [
"Тест проверяет processContent: замена алиаса при переименовании",
"Тест проверяет processContent: игнорирование ссылок без алиаса",
"Тест проверяет processContent: учёт флага processEmbeds",
"Тест проверяет getFilesToProcess: включение canvas-файлов при processCanvas=true",
"Все тесты проходят (npm test)"
],
"context": "Preserve Link Aliases обрабатывает vault rename. Основная логика: processContent (регулярное выражение для вики-ссылок) и getFilesToProcess (сбор markdown + опционально canvas).",
"status": "pending"
},
{
"id": "T5",
"title": "Добавить CI workflow для тестов и линтинга",
"description": "Добавить GitHub Actions workflow для автоматического запуска тестов и ESLint на push/PR в main/dev.",
"type": "config",
"files": [
".github/workflows/ci.yml"
],
"depends_on": ["T1", "T3", "T4"],
"acceptance_criteria": [
"Workflow запускается на push и pull_request в main и dev",
"Workflow выполняет npm ci, npm run lint, npm test",
"Workflow завершается с ошибкой при падении тестов или линтера"
],
"context": "Существующий release.yml в .github/workflows/ — можно использовать как шаблон. Node 20, ubuntu-latest.",
"status": "pending"
}
]
}
+128
View File
@@ -0,0 +1,128 @@
# Task Manifest
**Session:** a1b2c3d4-e5f6-7890-abcd-ef1234567890
**Goal:** Проанализировать существующий Obsidian-плагин zeroq-qol-modules (v1.1.4) и подготовить манифест задач для исполнительного агента.
**Date:** 2026-07-22T00:00:00.000Z
---
## Task Overview
| ID | Title | Type | Depends On | Status |
|---|---|---|---|---|
| T1 | Настроить ESLint и Prettier | config | — | pending |
| T2 | Написать тесты для BaseModule | test | — | pending |
| T3 | Написать тесты для Attachment Clean Paste модуля | test | T2 | pending |
| T4 | Написать тесты для Preserve Link Aliases модуля | test | T2 | pending |
| T5 | Добавить CI workflow для тестов и линтинга | config | T1, T3, T4 | pending |
**Total tasks:** 5
---
## Task Details
### T1: Настроить ESLint и Prettier
**Type:** config
**Description:** Добавить конфигурацию ESLint (typescript-eslint) и Prettier для единого стиля кода. Настроить скрипты lint/format в package.json.
**Files:**
- `package.json`
- `.eslintrc.json`
- `.prettierrc.json`
**Depends on:**
**Acceptance Criteria:**
- [ ] ESLint настроен с typescript-eslint, проходит без ошибок на src/
- [ ] Prettier настроен с tabs, проходит без ошибок на src/
- [ ] Скрипты lint и format добавлены в package.json
**Context:** В проекте используются tabs для отступов (см. review.md). Линтер/форматтер отсутствует.
---
### T2: Написать тесты для BaseModule
**Type:** test
**Description:** Добавить unit-тесты для BaseModule: проверка registerCleanup, onunload (очистка всех функций), defaultSettings.
**Files:**
- `src/modules/base-module.ts`
- `tests/modules/base-module.test.ts`
- `package.json`
- `tsconfig.json`
**Depends on:**
**Acceptance Criteria:**
- [ ] Тест проверяет, что registerCleanup добавляет функцию в очередь
- [ ] Тест проверяет, что onunload вызывает все зарегистрированные cleanup-функции
- [ ] Тест проверяет, что onunload очищает очередь после выполнения
- [ ] Тест проверяет, что defaultSettings возвращает пустой объект
- [ ] Все тесты проходят (npm test)
**Context:** BaseModule — абстрактный класс-основа для всех модулей. Находится в src/modules/base-module.ts. Для тестов нужно настроить тестовый раннер (jest или vitest) и mock-объект Plugin из obsidian.
---
### T3: Написать тесты для Attachment Clean Paste модуля
**Type:** test
**Description:** Добавить unit-тесты для AttachmentCleanPasteModule: проверка shouldProcess (фильтрация расширений), обработка paste/drop событий.
**Files:**
- `src/modules/attachment-clean-paste/index.ts`
- `tests/modules/attachment-clean-paste.test.ts`
**Depends on:** T2
**Acceptance Criteria:**
- [ ] Тест проверяет shouldProcess с разными расширениями и настройками
- [ ] Тест проверяет, что обработчик paste вызывает preventDefault для файлов с подходящими расширениями
- [ ] Тест проверяет, что модуль корректно создаёт ссылку [[path|name]]
- [ ] Все тесты проходят (npm test)
**Context:** Attachment Clean Paste обрабатывает editor-paste и editor-drop события. Основная логика: shouldProcess (фильтрация по extensions) и processFile (копирование + создание ссылки).
---
### T4: Написать тесты для Preserve Link Aliases модуля
**Type:** test
**Description:** Добавить unit-тесты для PreserveLinkAliasesModule: проверка processContent (замена алиасов), getFilesToProcess (фильтрация файлов), обработка rename.
**Files:**
- `src/modules/preserve-link-aliases/index.ts`
- `tests/modules/preserve-link-aliases.test.ts`
**Depends on:** T2
**Acceptance Criteria:**
- [ ] Тест проверяет processContent: замена алиаса при переименовании
- [ ] Тест проверяет processContent: игнорирование ссылок без алиаса
- [ ] Тест проверяет processContent: учёт флага processEmbeds
- [ ] Тест проверяет getFilesToProcess: включение canvas-файлов при processCanvas=true
- [ ] Все тесты проходят (npm test)
**Context:** Preserve Link Aliases обрабатывает vault rename. Основная логика: processContent (регулярное выражение для вики-ссылок) и getFilesToProcess (сбор markdown + опционально canvas).
---
### T5: Добавить CI workflow для тестов и линтинга
**Type:** config
**Description:** Добавить GitHub Actions workflow для автоматического запуска тестов и ESLint на push/PR в main/dev.
**Files:**
- `.github/workflows/ci.yml`
**Depends on:** T1, T3, T4
**Acceptance Criteria:**
- [ ] Workflow запускается на push и pull_request в main и dev
- [ ] Workflow выполняет npm ci, npm run lint, npm test
- [ ] Workflow завершается с ошибкой при падении тестов или линтера
**Context:** Существующий release.yml в .github/workflows/ — можно использовать как шаблон. Node 20, ubuntu-latest.
-63
View File
@@ -1,63 +0,0 @@
name: Draft Release
on:
push:
branches: [master]
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: zip -j zeroq-qol-modules.zip main.js manifest.json styles.css
- name: Create or update draft release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -e
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
TAG="latest-build"
SHA="${GITHUB_SHA}"
EXISTING=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/releases" \
| jq '.[] | select(.tag_name == "'"${TAG}"'" and .draft == true) | .id')
if [ -z "$EXISTING" ]; then
echo "Creating new draft release..."
RESP=$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "'"${TAG}"'",
"name": "Latest Build from master",
"body": "Автоматическая сборка из ветки `master`.\n\nКоммит: '"${SHA}"'",
"draft": true,
"prerelease": true
}' "${API}/releases")
RELEASE_ID=$(echo "$RESP" | jq -r '.id')
else
echo "Updating existing draft release (id: ${EXISTING})..."
RELEASE_ID=$EXISTING
curl -s -X PATCH -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"body": "Автоматическая сборка из ветки `master`.\n\nКоммит: '"${SHA}"'"
}' "${API}/releases/${RELEASE_ID}"
ASSETS=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/releases/${RELEASE_ID}/assets" | jq -r '.[].id')
for AID in $ASSETS; do
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/releases/${RELEASE_ID}/assets/${AID}"
done
fi
echo "Uploading artifact..."
curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@zeroq-qol-modules.zip" \
"${API}/releases/${RELEASE_ID}/assets"
echo "Done."
+42
View File
@@ -0,0 +1,42 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Verify version
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
MANIFEST_VERSION=$(node -p "require('./manifest.json').version")
if [ "$TAG_VERSION" != "$MANIFEST_VERSION" ]; then
echo "❌ Tag ($TAG_VERSION) != manifest ($MANIFEST_VERSION)"
exit 1
fi
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
files: |
main.js
manifest.json
styles.css
+35
View File
@@ -0,0 +1,35 @@
# MetaAgent
Этот проект использует [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/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 до актуальной версии.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
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.
+22 -71
View File
@@ -1,90 +1,41 @@
# ZeroQ QoL Modules
Модульный плагин для **Obsidian** с набором QoL-функций, которые можно включать и отключать в настройках.
A modular **Obsidian** plugin with toggleable QoL (quality-of-life) modules. Each module can be independently enabled or disabled in the settings.
## Установка
### Modules
1. Скачайте `main.js`, `manifest.json` и `styles.css` из [релизов](https://github.com/your/repo/releases)
2. Поместите их в папку `.obsidian/plugins/zeroq-qol-modules/` вашего хранилища
3. В Obsidian: **Настройки****Community plugins****Включить**
4. Активируйте плагин **ZeroQ QoL Modules**
- **Attachment Clean Paste** — Replaces `![[embed]]` with `[[path/file|name]]` for specified file extensions on paste/drop
- **Preserve Link Aliases** — Preserves original link aliases when files are renamed
## Модули
### Build
| Модуль | Описание |
|--------|----------|
| **Attachment Clean Paste** | Заменяет `![[вложение]]` на `[[вложение|имя]]` при вставке/перетаскивании файлов с указанными расширениями |
Каждый модуль включается/отключается в настройках плагина.
## Разработка
### Добавление нового модуля
1. Создайте папку `src/modules/my-module/`
2. Создайте класс, унаследованный от `BaseModule`:
```typescript
import { BaseModule } from '../base-module';
export class MyModule extends BaseModule {
id = 'my-module';
name = 'My Module';
description = 'Описание модуля';
get defaultSettings() {
return { /* настройки по умолчанию */ };
}
onload(plugin, moduleSettings) {
// инициализация: регистрация событий, команд, настроек
}
onunload(plugin) {
// очистка (cleanup происходит автоматически через registerCleanup)
}
}
```bash
npm run build # production
npm run dev # dev with sourcemaps
```
3. Зарегистрируйте модуль в `src/modules/index.ts`:
### License
```typescript
import { MyModule } from './my-module';
MIT
export const MODULES: QoLModule[] = [
new AttachmentCleanPasteModule(),
new MyModule(),
];
```
---
4. Если модулю нужна своя вкладка настроек — создайте `PluginSettingTab` и добавьте её через `plugin.addSettingTab()` в `onload`
# ZeroQ QoL Modules
Модульный плагин для **Obsidian** с набором QoL-функций, которые можно независимо включать и отключать в настройках.
### Модули
- **Attachment Clean Paste** — заменяет `![[вложение]]` на `[[путь/файл|имя]]` для указанных расширений при вставке/перетаскивании
- **Preserve Link Aliases** — сохраняет оригинальные алиасы ссылок при переименовании файлов
### Сборка
```bash
npm run build # production-сборка
npm run dev # dev-сборка с sourcemap
npm run build # production
npm run dev # dev со sourcemap
```
### Архитектура
```
src/
├── main.ts # точка входа, Plugin class
├── settings.ts # общая панель настроек
├── types.ts # базовые типы
└── modules/
├── index.ts # реестр модулей (статический импорт)
├── base-module.ts # абстрактный класс модуля
└── attachment-clean-paste/
└── index.ts # реализация модуля
```
**Принцип работы:**
- `main.ts` загружает настройки, итерирует `MODULES` и вызывает `onload()` для включённых
- Каждый модуль через `registerCleanup()` регистрирует функции отчистки, которые вызываются при выключении модуля или выгрузке плагина
- В настройках отображаются все модули с чекбоксами вкл/выкл
## Лицензия
### Лицензия
MIT
+6
View File
@@ -1,5 +1,6 @@
import esbuild from 'esbuild';
import process from 'process';
import fs from 'fs';
import builtins from 'builtin-modules';
const banner = `/*
@@ -9,6 +10,11 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
const prod = process.argv[2] === 'production';
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const manifest = JSON.parse(fs.readFileSync('./manifest.json', 'utf8'));
manifest.version = pkg.version;
fs.writeFileSync('./manifest.json', JSON.stringify(manifest, null, '\t') + '\n');
esbuild
.build({
banner: { js: banner },
+451
View File
@@ -0,0 +1,451 @@
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => ZeroQoLModulesPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian4 = require("obsidian");
// src/modules/attachment-clean-paste/index.ts
var import_obsidian = require("obsidian");
// src/modules/base-module.ts
var BaseModule = class {
constructor() {
this.cleanupFns = [];
}
get defaultSettings() {
return {};
}
onunload(_plugin) {
for (const fn of this.cleanupFns) {
fn();
}
this.cleanupFns = [];
}
renderInlineSettings(_containerEl, _settings, _saveSettings) {
}
registerCleanup(fn) {
this.cleanupFns.push(fn);
}
};
// src/locales/en.ts
var en = {
settings: {
title: "ZeroQ QoL Modules",
description: "Enable or disable modules as you wish."
},
modules: {
"attachment-clean-paste": {
name: "Attachment Clean Paste",
description: "Replaces ![[embed]] with [[path/file|name]] for configured file extensions on paste/drop",
descriptionShort: "Replaces ![[embed]] with [[path/file|name]] for specified file extensions.",
extensionsLabel: "File extensions",
extensionsDesc: "Comma-separated extensions (e.g.: png, jpg, pdf, mp3). Files with these extensions will be inserted as [[path/file|name]] instead of ![[path/file]].",
extensionsPlaceholder: "png, jpg, jpeg, gif, svg, webp, pdf"
},
"preserve-link-aliases": {
name: "Preserve Link Aliases",
description: "Preserves original link aliases when attachments are renamed",
processEmbedsLabel: "Process embed links",
processEmbedsDesc: "Also process ![[embed]] links (disabled by default)",
processCanvasLabel: "Process Canvas files",
processCanvasDesc: "Also process links in Canvas (.canvas) files"
}
}
};
// src/locales/ru.ts
var ru = {
settings: {
title: "ZeroQ QoL Modules",
description: "\u0412\u043A\u043B\u044E\u0447\u0430\u0439\u0442\u0435 \u0438 \u043E\u0442\u043A\u043B\u044E\u0447\u0430\u0439\u0442\u0435 \u043C\u043E\u0434\u0443\u043B\u0438 \u043F\u043E \u0441\u0432\u043E\u0435\u043C\u0443 \u0443\u0441\u043C\u043E\u0442\u0440\u0435\u043D\u0438\u044E."
},
modules: {
"attachment-clean-paste": {
name: "Attachment Clean Paste",
description: "\u0417\u0430\u043C\u0435\u043D\u044F\u0435\u0442 ![[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435]] \u043D\u0430 [[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435|\u0438\u043C\u044F]] \u0434\u043B\u044F \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0445 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0439 \u043F\u0440\u0438 \u0432\u0441\u0442\u0430\u0432\u043A\u0435/\u043F\u0435\u0440\u0435\u0442\u0430\u0441\u043A\u0438\u0432\u0430\u043D\u0438\u0438",
descriptionShort: "\u0417\u0430\u043C\u0435\u043D\u044F\u0435\u0442 ![[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435]] \u043D\u0430 [[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435|\u0438\u043C\u044F]] \u0434\u043B\u044F \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0445 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0439.",
extensionsLabel: "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0444\u0430\u0439\u043B\u043E\u0432",
extensionsDesc: "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u044F\u0442\u0443\u044E (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: png, jpg, pdf, mp3). \u0424\u0430\u0439\u043B\u044B \u0441 \u044D\u0442\u0438\u043C\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F\u043C\u0438 \u0431\u0443\u0434\u0443\u0442 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043A\u0430\u043A [[path/file|name]] \u0432\u043C\u0435\u0441\u0442\u043E ![[path/file]].",
extensionsPlaceholder: "png, jpg, jpeg, gif, svg, webp, pdf"
},
"preserve-link-aliases": {
name: "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u0430\u043B\u0438\u0430\u0441\u043E\u0432 \u0441\u0441\u044B\u043B\u043E\u043A",
description: "\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u0442 \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0435 \u0430\u043B\u0438\u0430\u0441\u044B \u0441\u0441\u044B\u043B\u043E\u043A \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0438 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0439",
processEmbedsLabel: "\u041E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C embed-\u0441\u0441\u044B\u043B\u043A\u0438",
processEmbedsDesc: "\u0422\u0430\u043A\u0436\u0435 \u043E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C ![[embed]] \u0441\u0441\u044B\u043B\u043A\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E)",
processCanvasLabel: "\u041E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C Canvas-\u0444\u0430\u0439\u043B\u044B",
processCanvasDesc: "\u0422\u0430\u043A\u0436\u0435 \u043E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0438 \u0432 Canvas (.canvas) \u0444\u0430\u0439\u043B\u0430\u0445"
}
}
};
// src/locales/index.ts
var locales = { en, ru };
function detectLocale() {
const lang = (document.documentElement.lang || "en").slice(0, 2);
return locales[lang] || en;
}
var locale = detectLocale();
// src/modules/attachment-clean-paste/index.ts
var MODULE_ID = "attachment-clean-paste";
var AttachmentCleanPasteModule = class extends BaseModule {
constructor() {
super(...arguments);
this.id = MODULE_ID;
this.name = locale.modules["attachment-clean-paste"].name;
this.description = locale.modules["attachment-clean-paste"].description;
}
get defaultSettings() {
return {
extensions: "png, jpg, jpeg, gif, svg, webp, bmp, pdf"
};
}
onload(plugin, moduleSettings) {
this.app = plugin.app;
const rawSettings = moduleSettings;
const pasteRef = plugin.app.workspace.on(
"editor-paste",
this.createPasteHandler(rawSettings)
);
this.registerCleanup(() => plugin.app.workspace.offref(pasteRef));
const dropRef = plugin.app.workspace.on(
"editor-drop",
this.createDropHandler(rawSettings)
);
this.registerCleanup(() => plugin.app.workspace.offref(dropRef));
}
renderInlineSettings(containerEl, settings, saveSettings) {
const loc = locale.modules["attachment-clean-paste"];
const extSettings = settings;
new import_obsidian.Setting(containerEl).setName(loc.extensionsLabel).setDesc(loc.extensionsDesc).addTextArea((textarea) => {
var _a;
textarea.setPlaceholder(loc.extensionsPlaceholder).setValue((_a = extSettings == null ? void 0 : extSettings.extensions) != null ? _a : "").onChange(async (value) => {
settings.extensions = value;
await saveSettings();
});
textarea.inputEl.rows = 4;
textarea.inputEl.style.width = "100%";
});
}
async processAll(files, editor, settings, evt) {
const matchingFiles = Array.from(files).filter(
(f) => this.shouldProcess(f.name, settings)
);
if (matchingFiles.length === 0)
return;
evt.preventDefault();
evt.stopPropagation();
const links = [];
for (const file of matchingFiles) {
const link = await this.processFile(file, settings);
if (link)
links.push(link);
}
if (links.length === 0)
return;
editor.replaceSelection(links.join("\n\n"));
}
createPasteHandler(settings) {
return async (evt, editor, _info) => {
var _a;
const files = (_a = evt.clipboardData) == null ? void 0 : _a.files;
if (!files || files.length === 0)
return;
await this.processAll(files, editor, settings, evt);
};
}
createDropHandler(settings) {
return async (evt, editor, _info) => {
var _a;
const files = (_a = evt.dataTransfer) == null ? void 0 : _a.files;
if (!files || files.length === 0)
return;
await this.processAll(files, editor, settings, evt);
};
}
shouldProcess(filename, settings) {
const dotIndex = filename.lastIndexOf(".");
if (dotIndex === -1)
return false;
const ext = filename.slice(dotIndex + 1).toLowerCase();
const exts = settings.extensions.split(",").map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0);
return exts.some((e) => {
const cleanExt = e.startsWith(".") ? e.slice(1) : e;
return cleanExt === ext;
});
}
async processFile(file, _settings) {
try {
const arrayBuffer = await file.arrayBuffer();
const filename = file.name;
const nameWithoutExt = filename.replace(/\.[^/.]+$/, "");
let attachmentFolder = this.app.vault.getConfig(
"attachmentFolderPath"
);
if (!attachmentFolder)
attachmentFolder = "attachments";
const folderPath = (0, import_obsidian.normalizePath)(attachmentFolder);
if (!await this.app.vault.adapter.exists(folderPath)) {
await this.app.vault.createFolder(folderPath);
}
const ext = filename.slice(filename.lastIndexOf(".") + 1);
const basePath = (0, import_obsidian.normalizePath)(`${folderPath}/${nameWithoutExt}`);
const availablePath = await this.app.vault.getAvailablePath(
basePath,
ext
);
await this.app.vault.createBinary(availablePath, arrayBuffer);
return `[[${availablePath}|${nameWithoutExt}]]`;
} catch (e) {
new import_obsidian.Notice(`Clean Paste error: ${e.message}`);
console.error("Attachment Clean Paste:", e);
}
return null;
}
};
// src/modules/preserve-link-aliases/index.ts
var import_obsidian2 = require("obsidian");
var MODULE_ID2 = "preserve-link-aliases";
var PreserveLinkAliasesModule = class extends BaseModule {
constructor() {
super(...arguments);
this.id = MODULE_ID2;
this.name = locale.modules["preserve-link-aliases"].name;
this.description = locale.modules["preserve-link-aliases"].description;
}
get defaultSettings() {
return {
processEmbeds: false,
processCanvas: false
};
}
onload(plugin, moduleSettings) {
this.vault = plugin.app.vault;
const rawSettings = moduleSettings;
const renameRef = plugin.app.vault.on(
"rename",
(file, oldPath) => this.handleRename(file, oldPath, rawSettings)
);
this.registerCleanup(() => plugin.app.vault.offref(renameRef));
}
renderInlineSettings(containerEl, settings, saveSettings) {
const loc = locale.modules["preserve-link-aliases"];
const s = settings;
new import_obsidian2.Setting(containerEl).setName(loc.processEmbedsLabel).setDesc(loc.processEmbedsDesc).addToggle(
(toggle) => toggle.setValue(s.processEmbeds).onChange(async (value) => {
s.processEmbeds = value;
await saveSettings();
})
);
new import_obsidian2.Setting(containerEl).setName(loc.processCanvasLabel).setDesc(loc.processCanvasDesc).addToggle(
(toggle) => toggle.setValue(s.processCanvas).onChange(async (value) => {
s.processCanvas = value;
await saveSettings();
})
);
}
async handleRename(file, oldPath, settings) {
var _a;
const oldName = (_a = oldPath.split("/").pop()) != null ? _a : "";
const newName = file.name;
const oldBasename = oldName.replace(/\.[^/.]+$/, "");
const newBasename = newName.replace(/\.[^/.]+$/, "");
if (oldBasename === newBasename)
return;
const files = this.getFilesToProcess(settings);
for (const f of files) {
const content = await this.vault.read(f);
const updated = this.processContent(content, newName, newBasename, oldBasename, settings);
if (updated !== content) {
await this.vault.modify(f, updated);
}
}
}
getFilesToProcess(settings) {
const files = [];
for (const file of this.vault.getMarkdownFiles()) {
files.push(file);
}
if (settings.processCanvas) {
const abstractFiles = this.vault.getFiles();
for (const file of abstractFiles) {
if (file.extension === "canvas") {
files.push(file);
}
}
}
return files;
}
processContent(content, newFileName, newBasename, oldBasename, settings) {
const wikiLinkRe = /(!?)\[\[([^\]]*?)]]/g;
return content.replace(wikiLinkRe, (fullMatch, prefix, inner) => {
if (prefix === "!" && !settings.processEmbeds) {
return fullMatch;
}
const pipeIndex = inner.indexOf("|");
if (pipeIndex === -1)
return fullMatch;
const linkPath = inner.slice(0, pipeIndex);
const alias = inner.slice(pipeIndex + 1);
if (alias !== newBasename)
return fullMatch;
const linkFilename = linkPath.split("/").pop();
if (linkFilename !== newFileName)
return fullMatch;
return `${prefix}[[${linkPath}|${oldBasename}]]`;
});
}
};
// src/modules/index.ts
var MODULES = [
new AttachmentCleanPasteModule(),
new PreserveLinkAliasesModule()
];
// src/settings.ts
var import_obsidian3 = require("obsidian");
var MODULE_DEFAULTS = MODULES.reduce(
(acc, mod) => {
acc[mod.id] = {
enabled: true,
settings: { ...mod.defaultSettings }
};
return acc;
},
{}
);
var DEFAULT_SETTINGS = {
modules: MODULE_DEFAULTS
};
function getModuleSettings(settings, moduleId) {
var _a, _b;
return (_b = (_a = settings.modules[moduleId]) == null ? void 0 : _a.settings) != null ? _b : {};
}
var ZeroQSettingTab = class extends import_obsidian3.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h1", { text: locale.settings.title });
containerEl.createEl("p", {
text: locale.settings.description,
attr: { style: "color: var(--text-muted); margin-bottom: 20px;" }
});
for (const module2 of MODULES) {
const moduleCfg = this.plugin.settings.modules[module2.id];
new import_obsidian3.Setting(containerEl).setName(module2.name).setDesc(module2.description).addToggle(
(toggle) => {
var _a;
return toggle.setValue((_a = moduleCfg == null ? void 0 : moduleCfg.enabled) != null ? _a : true).onChange(async (value) => {
if (!this.plugin.settings.modules[module2.id]) {
this.plugin.settings.modules[module2.id] = {
enabled: value,
settings: { ...module2.defaultSettings }
};
} else {
this.plugin.settings.modules[module2.id].enabled = value;
}
await this.plugin.saveSettings();
if (value) {
this.plugin.loadModule(module2);
} else {
this.plugin.unloadModule(module2);
}
});
}
);
if (moduleCfg) {
module2.renderInlineSettings(
containerEl,
moduleCfg.settings,
() => this.plugin.saveSettings()
);
}
}
}
};
// src/main.ts
var ZeroQoLModulesPlugin = class extends import_obsidian4.Plugin {
constructor() {
super(...arguments);
this.loadedModules = /* @__PURE__ */ new Set();
}
async onload() {
await this.loadSettings();
this.addSettingTab(new ZeroQSettingTab(this.app, this));
for (const module2 of MODULES) {
const moduleCfg = this.settings.modules[module2.id];
if (moduleCfg == null ? void 0 : moduleCfg.enabled) {
this.loadModule(module2);
}
}
}
onunload() {
for (const module2 of MODULES) {
this.unloadModule(module2);
}
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
for (const module2 of MODULES) {
if (!this.settings.modules[module2.id]) {
this.settings.modules[module2.id] = {
enabled: true,
settings: { ...module2.defaultSettings }
};
}
}
}
async saveSettings() {
await this.saveData(this.settings);
}
loadModule(module2) {
if (this.loadedModules.has(module2.id))
return;
const moduleSettings = getModuleSettings(this.settings, module2.id);
module2.onload(this, moduleSettings);
this.loadedModules.add(module2.id);
}
unloadModule(module2) {
if (!this.loadedModules.has(module2.id))
return;
module2.onunload(this);
this.loadedModules.delete(module2.id);
}
};
+3 -3
View File
@@ -1,9 +1,9 @@
{
"id": "zeroq-qol-modules",
"name": "ZeroQ QoL Modules",
"version": "1.0.0",
"version": "1.1.5",
"minAppVersion": "0.15.0",
"description": "Набор QoL-модулей для Obsidian: clean paste, авто-линковка и другое",
"author": "Custom",
"description": "QoL Modules",
"author": "oqyude",
"isDesktopOnly": false
}
+7 -3
View File
@@ -1,13 +1,17 @@
{
"name": "zeroq-qol-modules",
"version": "1.0.0",
"version": "1.1.5",
"description": "Modular Obsidian plugin with toggleable QoL modules",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production"
"build": "node esbuild.config.mjs production",
"release": "bash scripts/release.sh"
},
"keywords": ["obsidian", "plugin"],
"keywords": [
"obsidian",
"plugin"
],
"author": "",
"license": "MIT",
"devDependencies": {
+197
View File
@@ -0,0 +1,197 @@
# Project Review: zeroq-qol-modules
## Overview
Obsidian plugin with a **modular architecture**. Each feature is a separate module that can be toggled on/off in settings.
**Plugin ID:** `zeroq-qol-modules`
**Entry point:** `src/main.ts` → compiles to `main.js` (esbuild)
**Version source:** `package.json` → auto-synced to `manifest.json` on build
**Current modules (2):** `attachment-clean-paste`, `preserve-link-aliases`
## Stack
- **Language:** TypeScript (5.x)
- **Bundler:** esbuild (0.17.x)
- **Obsidian API:** `obsidian` latest (type defs only, excluded from bundle)
- **Format:** CommonJS (`format: 'cjs'`), target ES2018
## Directory Structure
```
src/
├── main.ts # Plugin class — loads settings, bootstraps modules
├── settings.ts # ZeroQSettingTab — single settings pane, renders all modules
├── types.ts # QoLModule interface, ZeroQSettings, ModuleSettings
├── locales/
│ ├── en.ts # Locale type definition + English dictionary
│ ├── ru.ts # Russian dictionary (same Locale shape)
│ └── index.ts # Auto-detects language from <html lang>, exports locale
└── modules/
├── index.ts # Static registry: array of all module instances
├── base-module.ts # BaseModule abstract class
├── attachment-clean-paste/
│ └── index.ts # Handles paste/drop — replaces ![[embed]] → [[path|name]]
└── preserve-link-aliases/
└── index.ts # Preserves aliases on vault rename events
```
## Architecture
### Plugin Lifecycle (`main.ts`)
```
onload()
→ loadSettings() # merge DEFAULT_SETTINGS + saved data
→ addSettingTab() # register ZeroQSettingTab
→ for each module:
if enabled → loadModule(module)
→ module.onload(this, moduleSettings)
onunload()
→ for each module:
unloadModule(module)
→ module.onunload(this)
```
### Module Lifecycle
Each module is a **singleton** implementing `QoLModule` (or extending `BaseModule`):
| Method | Purpose |
|--------|---------|
| `onload(plugin, moduleSettings)` | Initialize: register events, commands, setting tab |
| `onunload(plugin)` | Cleanup (auto-runs `registerCleanup` callbacks) |
| `renderInlineSettings(containerEl, settings, saveSettings)` | Render module-specific settings inline (no separate tab) |
| `defaultSettings` | Default module settings object |
### Cleanup pattern
Modules use `this.registerCleanup(fn)` to queue cleanup callbacks. They auto-run in `onunload()`. Example:
```ts
const ref = plugin.app.workspace.on('editor-paste', handler);
this.registerCleanup(() => plugin.app.workspace.offref(ref));
```
### Settings architecture
- **ZeroQSettingTab** (`settings.ts`) — single Obsidian settings pane
- Iterates `MODULES`, renders each with:
1. Toggle (enable/disable) — toggling calls `loadModule` / `unloadModule` at runtime
2. `module.renderInlineSettings()` — module-specific controls rendered directly below the toggle
- **No separate PluginSettingTab per module** — all in one block
### Settings data shape
```ts
interface ZeroQSettings {
modules: Record<string, {
enabled: boolean;
settings: Record<string, unknown>; // module-specific, typed per module
}>;
}
```
### Localization (`locales/`)
- **Locale type** defined in `en.ts` as the interface `Locale`
- **Language detection:** reads `<html lang>` attribute (set by Obsidian per its own language setting)
- Fallback to `'en'` if language is not in the dictionary
- Usage: `import { locale } from '../../locales'``locale.settings.title`, `locale.modules['xxx'].name`
**To add a language:**
1. Create `src/locales/de.ts` with `Locale` shape
2. Import it in `src/locales/index.ts` and add to `locales` record
## Adding a New Module
### Step 1: Create the module class
Extend `BaseModule` in `src/modules/my-module/index.ts`:
```ts
import { Plugin } from 'obsidian';
import { BaseModule } from '../base-module';
import { locale } from '../../locales';
export class MyModule extends BaseModule {
id = 'my-module';
name = locale.modules['my-module'].name;
description = locale.modules['my-module'].description;
get defaultSettings(): Record<string, unknown> {
return { myOption: 'default' };
}
onload(plugin: Plugin, moduleSettings: Record<string, unknown>): void {
// register events/commands via plugin.registerEvent / plugin.addCommand
// use this.registerCleanup(fn) for teardown
}
renderInlineSettings(
containerEl: HTMLElement,
settings: Record<string, unknown>,
saveSettings: () => Promise<void>,
): void {
// render controls into containerEl
// mutate settings object directly, then call saveSettings()
new Setting(containerEl)
.setName('My option')
.addText(text => text
.setValue(String(settings.myOption ?? ''))
.onChange(async v => {
settings.myOption = v;
await saveSettings();
}));
}
}
```
### Step 2: Add translations
In `src/locales/en.ts`, add to the `Locale` interface and the `en` object:
```ts
'modules': {
'my-module': {
name: string;
description: string;
// any other strings this module needs
};
}
```
Do the same in `src/locales/ru.ts`.
### Step 3: Register in the registry
In `src/modules/index.ts`:
```ts
import { MyModule } from './my-module';
export const MODULES: QoLModule[] = [
new AttachmentCleanPasteModule(),
new MyModule(),
];
```
## Build
```bash
npm run build # production build (minified, no sourcemaps)
npm run dev # dev build (inline sourcemaps)
```
`esbuild.config.mjs` also syncs `version` from `package.json` to `manifest.json` before building.
## Conventions
- **No comments** in source code
- **No emojis** in code or UI
- **Indentation:** tabs (Obsidian convention)
- **Imports:** `import type` for type-only imports to avoid circular dependencies
- **File naming:** `kebab-case` for files, `PascalCase` for classes
- **Russian labels** for Russian locale, **English labels** for all other locales
- Module `id` is `kebab-case` and matches the key in the locales `modules` object
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
BUMP="${1:-patch}"
CURRENT_VERSION=$(node -p "require('./manifest.json').version")
if [[ "$BUMP" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
NEW_VERSION="$BUMP"
elif [[ "$BUMP" == "patch" ]]; then
NEW_VERSION=$(node -p "
const [a,b,c] = '$CURRENT_VERSION'.split('.').map(Number);
\`\${a}.\${b}.\${c+1}\`
")
elif [[ "$BUMP" == "minor" ]]; then
NEW_VERSION=$(node -p "
const [a,b] = '$CURRENT_VERSION'.split('.').map(Number);
\`\${a}.\${b+1}.0\`
")
elif [[ "$BUMP" == "major" ]]; then
NEW_VERSION=$(node -p "
const [a] = '$CURRENT_VERSION'.split('.').map(Number);
\`\${a+1}.0.0\`
")
else
echo "Usage: $0 [patch|minor|major|<semver>]"
exit 1
fi
echo "Current: $CURRENT_VERSION"
echo "New: $NEW_VERSION"
read -p "Proceed? [y/N] " CONFIRM
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
echo "Aborted."; exit 1
fi
node -e "
const fs = require('fs');
const m = require('./manifest.json'); m.version = '$NEW_VERSION';
fs.writeFileSync('./manifest.json', JSON.stringify(m, null, '\t') + '\n');
const p = require('./package.json'); p.version = '$NEW_VERSION';
fs.writeFileSync('./package.json', JSON.stringify(p, null, '\t') + '\n');
"
npm run build
git add -f manifest.json package.json main.js styles.css
git commit -m "v$NEW_VERSION"
git tag "v$NEW_VERSION"
echo ""
echo "✔ Release v$NEW_VERSION ready."
echo "Run: git push && git push --tags"
+53
View File
@@ -0,0 +1,53 @@
export interface Locale {
settings: {
title: string;
description: string;
};
modules: {
'attachment-clean-paste': {
name: string;
description: string;
descriptionShort: string;
extensionsLabel: string;
extensionsDesc: string;
extensionsPlaceholder: string;
};
'preserve-link-aliases': {
name: string;
description: string;
processEmbedsLabel: string;
processEmbedsDesc: string;
processCanvasLabel: string;
processCanvasDesc: string;
};
};
}
export const en: Locale = {
settings: {
title: 'ZeroQ QoL Modules',
description: 'Enable or disable modules as you wish.',
},
modules: {
'attachment-clean-paste': {
name: 'Attachment Clean Paste',
description:
'Replaces ![[embed]] with [[path/file|name]] for configured file extensions on paste/drop',
descriptionShort:
'Replaces ![[embed]] with [[path/file|name]] for specified file extensions.',
extensionsLabel: 'File extensions',
extensionsDesc:
'Comma-separated extensions (e.g.: png, jpg, pdf, mp3). Files with these extensions will be inserted as [[path/file|name]] instead of ![[path/file]].',
extensionsPlaceholder: 'png, jpg, jpeg, gif, svg, webp, pdf',
},
'preserve-link-aliases': {
name: 'Preserve Link Aliases',
description:
'Preserves original link aliases when attachments are renamed',
processEmbedsLabel: 'Process embed links',
processEmbedsDesc: 'Also process ![[embed]] links (disabled by default)',
processCanvasLabel: 'Process Canvas files',
processCanvasDesc: 'Also process links in Canvas (.canvas) files',
},
},
};
+12
View File
@@ -0,0 +1,12 @@
import { en, Locale } from './en';
import { ru } from './ru';
const locales: Record<string, Locale> = { en, ru };
function detectLocale(): Locale {
const lang = (document.documentElement.lang || 'en').slice(0, 2);
return locales[lang] || en;
}
export const locale = detectLocale();
export type { Locale };
+30
View File
@@ -0,0 +1,30 @@
import { Locale } from './en';
export const ru: Locale = {
settings: {
title: 'ZeroQ QoL Modules',
description: 'Включайте и отключайте модули по своему усмотрению.',
},
modules: {
'attachment-clean-paste': {
name: 'Attachment Clean Paste',
description:
'Заменяет ![[вложение]] на [[вложение|имя]] для указанных расширений при вставке/перетаскивании',
descriptionShort:
'Заменяет ![[вложение]] на [[вложение|имя]] для указанных расширений.',
extensionsLabel: 'Расширения файлов',
extensionsDesc:
'Расширения через запятую (например: png, jpg, pdf, mp3). Файлы с этими расширениями будут вставляться как [[path/file|name]] вместо ![[path/file]].',
extensionsPlaceholder: 'png, jpg, jpeg, gif, svg, webp, pdf',
},
'preserve-link-aliases': {
name: 'Сохранение алиасов ссылок',
description:
'Сохраняет оригинальные алиасы ссылок при переименовании вложений',
processEmbedsLabel: 'Обрабатывать embed-ссылки',
processEmbedsDesc: 'Также обрабатывать ![[embed]] ссылки (по умолчанию выключено)',
processCanvasLabel: 'Обрабатывать Canvas-файлы',
processCanvasDesc: 'Также обрабатывать ссылки в Canvas (.canvas) файлах',
},
},
};
+59 -84
View File
@@ -4,12 +4,12 @@ import {
MarkdownFileInfo,
MarkdownView,
Notice,
PluginSettingTab,
Setting,
normalizePath,
} from 'obsidian';
import type ZeroQoLModulesPlugin from '../../main';
import { BaseModule } from '../base-module';
import { locale } from '../../locales';
const MODULE_ID = 'attachment-clean-paste';
@@ -19,9 +19,8 @@ interface AttachmentCleanPasteSettings {
export class AttachmentCleanPasteModule extends BaseModule {
id = MODULE_ID;
name = 'Attachment Clean Paste';
description =
'Заменяет ![[вложение]] на [[вложение|имя]] для указанных расширений при вставке/перетаскивании';
name = locale.modules['attachment-clean-paste'].name;
description = locale.modules['attachment-clean-paste'].description;
private app: App;
@@ -33,25 +32,69 @@ export class AttachmentCleanPasteModule extends BaseModule {
onload(plugin: ZeroQoLModulesPlugin, moduleSettings: Record<string, unknown>): void {
this.app = plugin.app;
const settings = moduleSettings as unknown as AttachmentCleanPasteSettings;
plugin.addSettingTab(
new AttachmentCleanPasteSettingTab(plugin.app, plugin, MODULE_ID),
);
const rawSettings = moduleSettings as AttachmentCleanPasteSettings;
const pasteRef = plugin.app.workspace.on(
'editor-paste',
this.createPasteHandler(settings),
this.createPasteHandler(rawSettings),
);
this.registerCleanup(() => plugin.app.workspace.offref(pasteRef));
const dropRef = plugin.app.workspace.on(
'editor-drop',
this.createDropHandler(settings),
this.createDropHandler(rawSettings),
);
this.registerCleanup(() => plugin.app.workspace.offref(dropRef));
}
renderInlineSettings(
containerEl: HTMLElement,
settings: Record<string, unknown>,
saveSettings: () => Promise<void>,
): void {
const loc = locale.modules['attachment-clean-paste'];
const extSettings = settings as unknown as AttachmentCleanPasteSettings;
new Setting(containerEl)
.setName(loc.extensionsLabel)
.setDesc(loc.extensionsDesc)
.addTextArea((textarea) => {
textarea
.setPlaceholder(loc.extensionsPlaceholder)
.setValue(extSettings?.extensions ?? '')
.onChange(async (value) => {
settings.extensions = value;
await saveSettings();
});
textarea.inputEl.rows = 4;
textarea.inputEl.style.width = '100%';
});
}
private async processAll(
files: FileList,
editor: Editor,
settings: AttachmentCleanPasteSettings,
evt: ClipboardEvent | DragEvent,
): Promise<void> {
const matchingFiles = Array.from(files).filter((f) =>
this.shouldProcess(f.name, settings),
);
if (matchingFiles.length === 0) return;
evt.preventDefault();
evt.stopPropagation();
const links: string[] = [];
for (const file of matchingFiles) {
const link = await this.processFile(file, settings);
if (link) links.push(link);
}
if (links.length === 0) return;
editor.replaceSelection(links.join('\n\n'));
}
private createPasteHandler(settings: AttachmentCleanPasteSettings) {
return async (
evt: ClipboardEvent,
@@ -61,17 +104,7 @@ export class AttachmentCleanPasteModule extends BaseModule {
const files = evt.clipboardData?.files;
if (!files || files.length === 0) return;
const matchingFiles = Array.from(files).filter((f) =>
this.shouldProcess(f.name, settings),
);
if (matchingFiles.length === 0) return;
evt.preventDefault();
evt.stopPropagation();
for (const file of matchingFiles) {
await this.processFile(file, editor, settings);
}
await this.processAll(files, editor, settings, evt);
};
}
@@ -84,17 +117,7 @@ export class AttachmentCleanPasteModule extends BaseModule {
const files = evt.dataTransfer?.files;
if (!files || files.length === 0) return;
const matchingFiles = Array.from(files).filter((f) =>
this.shouldProcess(f.name, settings),
);
if (matchingFiles.length === 0) return;
evt.preventDefault();
evt.stopPropagation();
for (const file of matchingFiles) {
await this.processFile(file, editor, settings);
}
await this.processAll(files, editor, settings, evt);
};
}
@@ -119,9 +142,8 @@ export class AttachmentCleanPasteModule extends BaseModule {
private async processFile(
file: File,
editor: Editor,
_settings: AttachmentCleanPasteSettings,
): Promise<void> {
): Promise<string | null> {
try {
const arrayBuffer = await file.arrayBuffer();
const filename = file.name;
@@ -147,58 +169,11 @@ export class AttachmentCleanPasteModule extends BaseModule {
await this.app.vault.createBinary(availablePath, arrayBuffer);
const link = `[[${availablePath}|${nameWithoutExt}]]`;
editor.replaceSelection(link);
return `[[${availablePath}|${nameWithoutExt}]]`;
} catch (e) {
new Notice(`Clean Paste error: ${e.message}`);
console.error('Attachment Clean Paste:', e);
}
}
}
class AttachmentCleanPasteSettingTab extends PluginSettingTab {
private plugin: ZeroQoLModulesPlugin;
private moduleId: string;
constructor(app: App, plugin: ZeroQoLModulesPlugin, moduleId: string) {
super(app, plugin);
this.plugin = plugin;
this.moduleId = moduleId;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Attachment Clean Paste' });
containerEl.createEl('p', {
text: 'Заменяет ![[вложение]] на [[вложение|имя]] для указанных расширений.',
attr: { style: 'color: var(--text-muted); margin-bottom: 20px;' },
});
const rawSettings = this.plugin.settings.modules[this.moduleId]?.settings as
| AttachmentCleanPasteSettings
| undefined;
new Setting(containerEl)
.setName('Расширения файлов')
.setDesc(
'Расширения через запятую (например: png, jpg, pdf, mp3). Файлы с этими расширениями будут вставляться как [[path/file|name]] вместо ![[path/file]].',
)
.addTextArea((textarea) => {
textarea
.setPlaceholder('png, jpg, jpeg, gif, svg, webp, pdf')
.setValue(rawSettings?.extensions ?? '')
.onChange(async (value) => {
if (this.plugin.settings.modules[this.moduleId]) {
this.plugin.settings.modules[this.moduleId].settings = {
extensions: value,
};
}
await this.plugin.saveSettings();
});
textarea.inputEl.rows = 4;
textarea.inputEl.style.width = '100%';
});
return null;
}
}
+6
View File
@@ -21,6 +21,12 @@ export abstract class BaseModule implements QoLModule {
this.cleanupFns = [];
}
renderInlineSettings(
_containerEl: HTMLElement,
_settings: Record<string, unknown>,
_saveSettings: () => Promise<void>,
): void {}
protected registerCleanup(fn: () => void): void {
this.cleanupFns.push(fn);
}
+5 -1
View File
@@ -1,4 +1,8 @@
import { AttachmentCleanPasteModule } from './attachment-clean-paste';
import { PreserveLinkAliasesModule } from './preserve-link-aliases';
import { QoLModule } from '../types';
export const MODULES: QoLModule[] = [new AttachmentCleanPasteModule()];
export const MODULES: QoLModule[] = [
new AttachmentCleanPasteModule(),
new PreserveLinkAliasesModule(),
];
+143
View File
@@ -0,0 +1,143 @@
import { Plugin, TFile, Vault, Setting } from 'obsidian';
import type ZeroQoLModulesPlugin from '../../main';
import { BaseModule } from '../base-module';
import { locale } from '../../locales';
const MODULE_ID = 'preserve-link-aliases';
interface PreserveLinkAliasesSettings {
processEmbeds: boolean;
processCanvas: boolean;
}
export class PreserveLinkAliasesModule extends BaseModule {
id = MODULE_ID;
name = locale.modules['preserve-link-aliases'].name;
description = locale.modules['preserve-link-aliases'].description;
private vault: Vault;
get defaultSettings(): Record<string, unknown> {
return {
processEmbeds: false,
processCanvas: false,
};
}
onload(plugin: ZeroQoLModulesPlugin, moduleSettings: Record<string, unknown>): void {
this.vault = plugin.app.vault;
const rawSettings = moduleSettings as PreserveLinkAliasesSettings;
const renameRef = plugin.app.vault.on(
'rename',
(file: TFile, oldPath: string) => this.handleRename(file, oldPath, rawSettings),
);
this.registerCleanup(() => plugin.app.vault.offref(renameRef));
}
renderInlineSettings(
containerEl: HTMLElement,
settings: Record<string, unknown>,
saveSettings: () => Promise<void>,
): void {
const loc = locale.modules['preserve-link-aliases'];
const s = settings as unknown as PreserveLinkAliasesSettings;
new Setting(containerEl)
.setName(loc.processEmbedsLabel)
.setDesc(loc.processEmbedsDesc)
.addToggle((toggle) =>
toggle
.setValue(s.processEmbeds)
.onChange(async (value) => {
s.processEmbeds = value;
await saveSettings();
}),
);
new Setting(containerEl)
.setName(loc.processCanvasLabel)
.setDesc(loc.processCanvasDesc)
.addToggle((toggle) =>
toggle
.setValue(s.processCanvas)
.onChange(async (value) => {
s.processCanvas = value;
await saveSettings();
}),
);
}
private async handleRename(
file: TFile,
oldPath: string,
settings: PreserveLinkAliasesSettings,
): Promise<void> {
const oldName = oldPath.split('/').pop() ?? '';
const newName = file.name;
const oldBasename = oldName.replace(/\.[^/.]+$/, '');
const newBasename = newName.replace(/\.[^/.]+$/, '');
if (oldBasename === newBasename) return;
const files = this.getFilesToProcess(settings);
for (const f of files) {
const content = await this.vault.read(f);
const updated = this.processContent(content, newName, newBasename, oldBasename, settings);
if (updated !== content) {
await this.vault.modify(f, updated);
}
}
}
private getFilesToProcess(settings: PreserveLinkAliasesSettings): TFile[] {
const files: TFile[] = [];
for (const file of this.vault.getMarkdownFiles()) {
files.push(file);
}
if (settings.processCanvas) {
const abstractFiles = this.vault.getFiles();
for (const file of abstractFiles) {
if (file.extension === 'canvas') {
files.push(file as TFile);
}
}
}
return files;
}
private processContent(
content: string,
newFileName: string,
newBasename: string,
oldBasename: string,
settings: PreserveLinkAliasesSettings,
): string {
const wikiLinkRe = /(!?)\[\[([^\]]*?)]]/g;
return content.replace(wikiLinkRe, (fullMatch, prefix, inner) => {
if (prefix === '!' && !settings.processEmbeds) {
return fullMatch;
}
const pipeIndex = inner.indexOf('|');
if (pipeIndex === -1) return fullMatch;
const linkPath = inner.slice(0, pipeIndex);
const alias = inner.slice(pipeIndex + 1);
if (alias !== newBasename) return fullMatch;
const linkFilename = linkPath.split('/').pop();
if (linkFilename !== newFileName) return fullMatch;
return `${prefix}[[${linkPath}|${oldBasename}]]`;
});
}
}
+13 -4
View File
@@ -2,6 +2,7 @@ import { App, PluginSettingTab, Setting } from 'obsidian';
import type ZeroQoLModulesPlugin from './main';
import { MODULES } from './modules';
import { ZeroQSettings } from './types';
import { locale } from './locales';
const MODULE_DEFAULTS = MODULES.reduce(
(acc, mod) => {
@@ -37,21 +38,21 @@ export class ZeroQSettingTab extends PluginSettingTab {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h1', { text: 'ZeroQ QoL Modules' });
containerEl.createEl('h1', { text: locale.settings.title });
containerEl.createEl('p', {
text: 'Включайте и отключайте модули по своему усмотрению.',
text: locale.settings.description,
attr: { style: 'color: var(--text-muted); margin-bottom: 20px;' },
});
for (const module of MODULES) {
const moduleSettings = this.plugin.settings.modules[module.id];
const moduleCfg = this.plugin.settings.modules[module.id];
new Setting(containerEl)
.setName(module.name)
.setDesc(module.description)
.addToggle((toggle) =>
toggle
.setValue(moduleSettings?.enabled ?? true)
.setValue(moduleCfg?.enabled ?? true)
.onChange(async (value) => {
if (!this.plugin.settings.modules[module.id]) {
this.plugin.settings.modules[module.id] = {
@@ -69,6 +70,14 @@ export class ZeroQSettingTab extends PluginSettingTab {
}
}),
);
if (moduleCfg) {
module.renderInlineSettings(
containerEl,
moduleCfg.settings,
() => this.plugin.saveSettings(),
);
}
}
}
}
+5
View File
@@ -16,4 +16,9 @@ export interface QoLModule {
defaultSettings: Record<string, unknown>;
onload(plugin: Plugin, moduleSettings: Record<string, unknown>): void;
onunload(plugin: Plugin): void;
renderInlineSettings(
containerEl: HTMLElement,
settings: Record<string, unknown>,
saveSettings: () => Promise<void>,
): void;
}
+1 -2
View File
@@ -1,13 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"importHelpers": true,
"lib": ["DOM", "ES5", "ES6", "ES7"],
"allowSyntheticDefaultImports": true,