Документация: полная ревизия — удалены устаревшие планы и спеки, актуализированы архитектурные документы, добавлен EN README

This commit is contained in:
mirivlad 2026-07-18 18:06:44 +08:00
parent 5e7ef495b5
commit 3f71689c84
55 changed files with 512 additions and 9894 deletions

View File

@ -1,12 +1,12 @@
# Verstak Platform Docs
Этот комплект фиксирует новый архитектурный курс Верстака: не монолитное приложение с набором встроенных экранов, а local-first платформа рабочего vault, где пользовательские функции подключаются динамическими плагинами.
Этот комплект фиксирует архитектурный курс Верстака: не монолитное приложение с набором встроенных экранов, а local-first платформа рабочего vault, где пользовательские функции подключаются динамическими плагинами.
Исходная идея Верстака сохраняется:
> Верстак - локальная рабочая среда, где по каждому клиенту, проекту или делу собраны файлы, заметки, документы, ссылки, действия, журнал и история работы.
Меняется внутреннее устройство. Заметки, файловый менеджер, редакторы, предпросмотр, журнал, активность, браузерный inbox и подобные части больше не считаются внутренними разделами одного приложения. Они становятся плагинами, которые подключаются к платформе через capability registry, contribution points и permissions.
Заметки, файловый менеджер, редакторы, предпросмотр, журнал, активность, браузерный inbox и подобные части — это плагины, которые подключаются к платформе через capability registry, contribution points и permissions.
## Документы
@ -14,14 +14,17 @@
- [02_Platform_Architecture.md](02_Platform_Architecture.md) - архитектура ядра, runtime, vault, UI shell и plugin host.
- [03_Repositories.md](03_Repositories.md) - разбиение на репозитории и назначение каждого.
- [04_Plugin_System.md](04_Plugin_System.md) - динамические плагины, manifest, lifecycle, capabilities, settings, permissions.
- [05_Official_Plugins.md](05_Official_Plugins.md) - состав официальных плагинов и их зависимости через capabilities.
- [06_Migration_Strategy.md](06_Migration_Strategy.md) - как развивать Verstak v2 как отдельную платформу без временных мостов к первой версии.
- [07_Full_Implementation_Roadmap.md](07_Full_Implementation_Roadmap.md) - порядок доведения Verstak2 до полной standalone-реализации.
- [AGENTS.md](AGENTS.md) - инструкция для coding agents, чтобы они не возвращали проект к монолиту.
- [05_Official_Plugins.md](05_Official_Plugins.md) - состав официальных плагинов, их возможности и текущий статус.
- [06_Migration_Strategy.md](06_Migration_Strategy.md) - стратегия развития платформы и definition of done.
- [07_Full_Implementation_Roadmap.md](07_Full_Implementation_Roadmap.md) - полная дорожная карта реализации с текущими статусами фаз.
Детальная документация по runtime:
- [Plugin Runtime](../verstak-desktop/docs/PLUGIN_RUNTIME.md) — подробный reference по plugin lifecycle, API, contribution points.
## Главный архитектурный инвариант
Core не знает о конкретных функциях вроде "заметки", "файловый менеджер" или "markdown editor". Core знает о:
Core не знает о конкретных функциях вроде «заметки», «файловый менеджер» или «markdown editor». Core знает о:
- vault;
- plugin runtime;
@ -33,4 +36,4 @@ Core не знает о конкретных функциях вроде "зам
- storage API;
- UI shell.
Все рабочие инструменты поставляются плагинами. Даже официальные плагины должны жить по тем же правилам, что и будущие сторонние.
Все рабочие инструменты поставляются плагинами. Даже официальные плагины живут по тем же правилам, что и будущие сторонние.

View File

@ -2,11 +2,28 @@
## 1. Организация
Для GitHub/Gitea лучше использовать organization `verstak` или аналогичную общую группу. Внутри нее живут отдельные репозитории одной продуктовой идеи.
Проект состоит из шести репозиториев под [github.com/mirivlad](https://github.com/mirivlad):
Project board можно вести на уровне organization: roadmap, issues, milestones, cross-repo tasks.
- `verstak` — основное desktop-приложение (Core Platform + UI Shell)
- `verstak-official-plugins` — монорепозиторий официальных плагинов
- `verstak-sdk` — TypeScript SDK, схемы и контракты плагинов
- `verstak-browser-extension` — расширение браузера
- `verstak-sync-server` — сервер синхронизации
- `verstak-docs` — платформенная и архитектурная документация
## 2. Минимальный набор репозиториев
Локально репозитории клонируются как sibling-директории:
```text
verstak2/
├── verstak-desktop/
├── verstak-official-plugins/
├── verstak-sdk/
├── verstak-browser-extension/
├── verstak-sync-server/
└── verstak-docs/
```
## 2. Назначение репозиториев
### `verstak-desktop`
@ -24,7 +41,11 @@ Project board можно вести на уровне organization: roadmap, iss
- storage API;
- event bus;
- sync client boundary;
- dev harness для локальных плагинов.
- workspace manager;
- file watcher;
- notifications;
- browser receiver (local HTTP endpoint);
- secret store (AES-GCM).
Не содержит:
@ -36,124 +57,71 @@ Project board можно вести на уровне organization: roadmap, iss
### `verstak-official-plugins`
Монорепозиторий официальных плагинов.
Монорепозиторий официальных плагинов:
Содержит:
```text
```
plugins/
files/
notes/
markdown-editor/
file-preview/
activity/
journal/
browser-inbox/
search/
secrets/
templates/
packages/
plugin-sdk/
shared-ui/
test-harness/
platform-test/ — тестовый плагин для проверки runtime
files/ — файловый менеджер
notes/ — заметки
default-editor/ — редактор текста/markdown
file-preview/ — предпросмотр изображений
activity/ — активность
journal/ — журнал работ
browser-inbox/ — браузерный inbox
search/ — поиск
secrets/ — хранилище секретов
todo/ — задачи
trash/ — корзина
sync/ — синхронизация
```
Официальные плагины должны устанавливаться и загружаться так же, как сторонние. Нельзя делать для них скрытый privileged path, кроме явно описанных platform permissions.
Все плагины используют префикс `verstak.*` в id. Официальные плагины загружаются через тот же plugin runtime, что и сторонние.
### `verstak-sdk`
Общие контракты для разработки плагинов.
Общие контракты для разработки плагинов:
Содержит:
- manifest schema;
- TypeScript SDK;
- RPC protocol definitions;
- capability contracts;
- event schemas;
- test helpers;
- plugin packaging tools;
- examples.
На раннем этапе SDK может жить в `verstak-official-plugins/packages/plugin-sdk`, но должен быть выделен в отдельный репозиторий, когда API начнет стабилизироваться.
- JSON Schema для manifest, capabilities, contributions, permissions, sync;
- TypeScript типы для plugin API;
- тестовые утилиты.
### `verstak-sync-server`
Отдельный сервер синхронизации.
Содержит:
Отдельный сервер синхронизации:
- HTTP API;
- auth/pairing;
- device registry;
- vault operation log;
- blob upload/download;
- conflict handling;
- retention/deleted file policy;
- server migrations;
- embedded web console;
- deployment docs.
Sync server не должен импортировать desktop UI или official plugins.
### `verstak-browser-extension`
Расширение браузера.
Расширение браузера (Firefox/Chromium):
Содержит:
- Firefox/Chromium extension;
- local pairing with Verstak;
- page capture;
- selected text capture;
- link sending;
- pending queue if desktop is offline;
- domain bindings support;
- protocol docs for browser inbox plugin.
Расширение не должно напрямую знать внутреннюю структуру notes/files/activity. Оно отправляет события в local receiver, а обработка идет через плагин `official.browser-inbox`.
## 3. Репозитории позже
### `verstak-plugin-registry`
Каталог доступных плагинов:
- official plugin index;
- third-party plugin metadata;
- signatures/checksums;
- compatibility matrix;
- install URLs.
Не нужен в первый этап, если плагины ставятся вручную из локальной папки или из `verstak-official-plugins`.
- page capture, selection, link, file;
- pending queue;
- domain bindings;
- passive domain activity (opt-in).
### `verstak-docs`
Публичная документация:
Платформенная и архитектурная документация:
- user guide;
- developer guide;
- plugin authoring guide;
- sync setup;
- security model;
- migration guides.
- product vision;
- platform architecture;
- plugin system overview;
- official plugins reference;
- development strategy;
- implementation roadmap.
Может быть отдельным репозиторием позже. Сейчас допустимо держать архитектурные документы рядом с `verstak-desktop`.
## 3. Версионирование
## 4. Что не дробить слишком рано
Не стоит сразу создавать отдельный репозиторий на каждый официальный плагин. Это увеличит накладные расходы и усложнит синхронные изменения SDK/API.
Лучше:
```text
verstak-official-plugins - один repo для официальных плагинов
```
А отдельные repo оставить для сторонних плагинов или крупных независимых модулей.
## 5. Версионирование
Версии должны существовать на трех уровнях:
Версии существуют на трёх уровнях:
- app version: версия `verstak-desktop`;
- platform apiVersion: версия API плагинов;
@ -164,8 +132,9 @@ verstak-official-plugins - один repo для официальных плаг
```json
{
"version": "0.1.0",
"apiVersion": "1"
"apiVersion": "0.1.0"
}
```
Core может загрузить плагин только при совместимости `apiVersion`.
Совместимые компоненты следует собирать из одной релизной линии.

View File

@ -1,5 +1,7 @@
# Verstak Plugin System
> Подробный reference по реализации: [Plugin Runtime](../verstak-desktop/docs/PLUGIN_RUNTIME.md).
## 1. Цель
Плагины превращают Верстак из монолитного приложения в платформу. Любая пользовательская функция должна быть реализуема как плагин:
@ -19,11 +21,12 @@
## 2. Структура плагина
```text
official.notes/
verstak.notes/
plugin.json
frontend/
index.js
style.css
dist/
index.js
style.css
backend/
plugin-linux-amd64
plugin-windows-amd64.exe
@ -37,14 +40,16 @@ official.notes/
## 3. Manifest
Manifest — файл `plugin.json` в корне директории плагина.
```json
{
"schemaVersion": 1,
"id": "official.notes",
"id": "verstak.notes",
"name": "Notes",
"version": "0.1.0",
"apiVersion": "1",
"description": "Markdown notes inside Verstak cases.",
"apiVersion": "0.1.0",
"description": "Markdown notes inside Verstak workspaces.",
"source": "official",
"localization": {
"defaultLocale": "en",
@ -66,16 +71,17 @@ official.notes/
"search.provider"
],
"permissions": [
"vault.read",
"vault.write",
"files.read",
"files.write",
"storage.namespace",
"ui.register",
"events.publish",
"events.subscribe"
"events.subscribe",
"workbench.open"
],
"frontend": {
"entry": "frontend/index.js",
"style": "frontend/style.css"
"entry": "frontend/dist/index.js",
"style": "frontend/dist/style.css"
},
"backend": {
"type": "sidecar",
@ -129,7 +135,7 @@ Desktop поддерживает `system`, `en` и `ru`. В режиме `system
```json
{
"requires": ["official.markdown-editor"]
"requires": ["verstak.default-editor"]
}
```
@ -152,12 +158,11 @@ Desktop поддерживает `system`, `en` и `ru`. В режиме `system
Пример:
```text
official.notes
verstak.notes
requires:
vault.files
verstak/core/files/v1
optionalRequires:
editor.text.markdown
preview.markdown
search.provider
```
@ -240,8 +245,8 @@ Contribution должен быть декларативным, где возмо
"contributes": {
"settingsPanels": [
{
"id": "official.markdown-editor.general",
"title": "Markdown Editor",
"id": "verstak.default-editor.general",
"title": "Default Editor",
"component": "settings.general"
}
]
@ -254,7 +259,7 @@ Plugin Manager показывает кнопку настроек только
Настройки хранятся в namespace плагина:
```text
plugin_settings.official.markdown-editor
plugin_settings/<plugin_id>/
```
Плагин не должен читать/писать настройки другого плагина без разрешения.
@ -326,19 +331,19 @@ Uninstall не должен автоматически удалять польз
## 13. Packaging
Плагин распространяется как zip/tar package:
Плагин распространяется как директория внутри `plugins/` или как архив:
```text
official.notes-0.1.0.vpkg
verstak.notes-0.1.0.tar.gz
```
Пакет содержит:
- `plugin.json`;
- frontend bundle;
- frontend bundle (`frontend/dist/`);
- backend binaries if any;
- migrations;
- README;
- checksums/signature later.
На первом этапе допустима ручная установка папкой в plugin directory.
Подробнее о сборке: [PACKAGING.md](../verstak-official-plugins/docs/PACKAGING.md).

View File

@ -1,21 +1,25 @@
# Official Plugins
Официальные плагины - это базовый набор инструментов Верстака. Они не должны быть скрытыми частями core. Их задача - показать, что платформа действительно работает через capabilities и contribution points.
Официальные плагины — базовый набор инструментов Верстака. Они используют тот же
plugin runtime, что и сторонние. Их задача — показать, что платформа работает
через capabilities и contribution points.
## 1. `official.files`
Все официальные плагины используют префикс `verstak.*` в id.
## 1. `verstak.files`
Назначение:
- дерево/список файлов внутри дела;
- добавление файлов;
- перемещение/копирование в vault;
- открытие системным приложением;
- file metadata;
- file actions registry consumer.
- добавление файлов и папок;
- перемещение/переименование;
- открытие через Workbench (openProvider routing);
- trash/restore (через `verstak.trash`);
- file metadata.
Provides:
```text
```
workspace.files
vault.files
entity.file
@ -24,72 +28,64 @@ file.browser
Optional requires:
```text
```
editor.text
viewer.file
preview.file
search.provider
```
Поведение:
Текущий статус: реализован. Открывает файлы через `api.workbench.openResource()`,
не импортирует редактор напрямую. Корзина выделена в отдельный плагин `verstak.trash`.
- если есть подходящий editor capability, показывает "Edit";
- если есть viewer/preview capability, показывает "Preview";
- если подходящего provider/capability нет, показывает понятное no-provider
состояние; открытие внешним приложением остается отдельной отложенной
возможностью.
## 2. `official.notes`
## 2. `verstak.notes`
Назначение:
- markdown notes as a UI-level context over ordinary Markdown files;
- canonical `Notes/` folder inside case/project;
- `Overview.md` is allowed only as an ordinary Markdown filename, not as a
special UI entity;
- markdown notes как UI-слой над обычными `.md` файлами;
- каноническая папка `Notes/` внутри дела;
- `Overview.md` — обычное markdown-имя файла, не специальная UI-сущность;
- note metadata;
- note links.
- rename с синхронизацией title/filename;
- conflict dialog при конфликте имён.
Provides:
```text
```
workspace.notes
note.registry
```
Requires:
```text
vault.files
```
verstak/core/files/v1
```
Optional requires:
```text
```
editor.text.markdown
preview.markdown
search.provider
```
Важное правило:
Текущий статус: реализован. Title и filename синхронизированы. При конфликте
имени показывается conflict dialog.
- title и filename должны оставаться синхронизированными;
- filename - человекочитаемая проекция title;
- при конфликте имени не добавлять `_2` молча, а показывать понятный conflict dialog.
## 3. `official.markdown-editor`
## 3. `verstak.default-editor`
Назначение:
- редактирование markdown/text;
- редактирование текста и markdown;
- toolbar;
- save flow;
- dirty state;
- keyboard shortcuts.
- markdown preview (встроен, не отдельный плагин);
- keyboard shortcuts (Ctrl+S).
Provides:
```text
```
editor.text
editor.text.markdown
editor.note.markdown
@ -97,52 +93,47 @@ editor.note.markdown
Optional requires:
```text
```
link.resolver
```
Не должен:
Текущий статус: реализован как единый плагин с тремя openProviders
(`text`, `generic-markdown`, `notes-markdown`). Использует `api.files.readText` /
`api.files.writeText`. Markdown preview — часть редактора, без отдельного
плагина предпросмотра.
- сам решать, где хранятся notes;
- напрямую зависеть от `official.notes`;
- тащить file manager внутрь себя.
Markdown preview is part of the Markdown editor surface. There is no separate
official Markdown preview plugin, because a standalone view provider would
compete with the editor routing for `.md` / `.markdown` files.
## 4. `official.file-preview`
## 4. `verstak.file-preview`
Назначение:
- inline preview for image files through bounded `api.files.readBytes`;
- metadata view for previewed files;
- `Open External` action through `api.files.openExternal`.
- inline preview для изображений через `api.files.readBytes`;
- metadata view;
- `Open External` через `api.files.openExternal`.
Provides:
```text
```
viewer.file
viewer.image
preview.file
```
Текущий статус: базовый `verstak.file-preview` renders image files inline via
the public Files API and shows file metadata. Text-like files, code, and
Markdown stay with editor plugins.
Текущий статус: базовый рендеринг изображений через публичный Files API,
плюс metadata файла. Текстовые файлы, код и markdown остаются за
editor-плагинами.
## 5. `official.activity`
## 5. `verstak.activity`
Назначение:
- сбор activity events;
- отображение истории;
- отображение истории активности;
- реконструкция работы;
- подсказки для worklog.
- worklog suggestions для Journal.
Provides:
```text
```
activity.log
activity.provider
activity.reconstruction
@ -150,52 +141,33 @@ activity.reconstruction
Subscribes:
```text
```
file.opened
file.changed
note.saved
action.started
browser.capture.received
case.selected
browser.capture.page
browser.capture.selection
browser.capture.link
browser.capture.file
browser.capture.converted
```
Текущий статус: базовый `verstak.activity` implemented as both a global sidebar
view and a workspace item. Workspace tabs store and display only their own
activity stream; the global sidebar view aggregates activity from all workspace
streams plus unscoped global activity. It contributes `activityProviders`; the
desktop runtime hosts those providers and records subscribed public events into
the plugin storage even when the Activity view is not mounted. The Activity UI
is a read/clear surface, not a manual recording toggle. It now reconstructs
compact worklog suggestions from scoped activity streams and exposes them
through the command-backed `verstak.activity.suggestWorklog` runtime contract
for the Journal plugin.
Текущий статус: реализован как глобальный sidebar view и workspace item.
Workspace tabs хранят свою activity stream; глобальный view агрегирует все
workspace streams. Предоставляет `activityProviders`; desktop runtime записывает
события в plugin storage даже когда Activity view не смонтирован. Реконструирует
worklog suggestions через контракт `verstak.activity.suggestWorklog`.
## Sync Conflict UX Contract
`api.sync.now()` returns warning details through `conflicts` and `applyErrors`.
The official Sync plugin must display both as warnings after manual sync. A
conflict warning must include at least the affected entity type/path when the
server provides those fields. The plugin must not auto-resolve conflicts, rename
local files, overwrite local files, or hide conflict details behind a plain
count.
## 6. `official.journal`
## 6. `verstak.journal`
Назначение:
- ручной журнал работ;
- billable/non-billable;
- отчеты по делу/клиенту;
- принятие suggested time из activity.
- приём suggested time из activity;
- создание записи из завершённого todo.
Provides:
```text
```
worklog
journal
report.worklog
@ -203,31 +175,27 @@ report.worklog
Optional requires:
```text
```
activity.reconstruction
```
Текущий статус: базовый `verstak.journal` implemented as both a global sidebar
view and a workspace item. Workspace views store manual worklog entries in the
plugin settings namespace, import non-billable entries from
`verstak.activity.suggestWorklog`, and deduplicate repeated imports by Activity
suggestion id. The global view aggregates stored workspace worklogs. Billing
reports, invoice export, timers, and richer report filters are still future
work.
Текущий статус: реализован как глобальный sidebar view и workspace item.
Хранит записи в plugin settings namespace, импортирует не-billable записи
из `verstak.activity.suggestWorklog`, дедуплицирует по Activity suggestion id.
## 7. `official.browser-inbox`
## 7. `verstak.browser-inbox`
Назначение:
- прием ссылок, выделенного текста, страниц и snippets из browser extension;
- pending queue;
- привязка доменов к делам;
- создание inbox entries;
- превращение inbox entry в note/link/file/activity.
- приём страниц, ссылок, выделенного текста и файлов из browser extension;
- pending queue с scoping по workspace;
- domain bindings;
- конвертация inbox entry в note/link/file;
- запись конвертаций в Activity.
Provides:
```text
```
capture.browser
browser.inbox
domain.binding
@ -235,133 +203,150 @@ domain.binding
Requires:
```text
```
network.local
```
Optional requires:
```text
```
workspace.notes
activity.log
search.provider
```
Текущий статус: базовый `verstak.browser-inbox` implemented as both a global
sidebar view and a workspace item. Workspace tabs keep their own pending queue;
the global sidebar view aggregates queues from all workspaces plus unscoped
global captures. The local receiver starts in paired mode: it generates an
installation-local token and requires `X-Verstak-Receiver-Token` before
publishing browser capture events. The Browser Inbox settings panel exposes the
receiver URL and token, and rotates the token through the dangerous
`browser.receiver.manage` permission. Browser Inbox stores plugin-owned
`domainBindings` and routes unscoped captures with an
exact domain match into the bound workspace queue. Its first conversion workflow
creates ordinary Markdown notes through the public Files API and publishes a
`browser.capture.converted` event, which Activity records through its public
provider subscription. Browser Inbox also creates human-readable `.url` link
files through the public Files API. It now accepts selected files from the
browser extension and creates ordinary workspace files through `api.files.writeText`
or bounded `api.files.writeBytes`. Chunked large-file attachment capture remains
future work.
Текущий статус: реализован как глобальный sidebar view и workspace item.
Local receiver в paired mode с токеном. Поддерживает конвертацию в note
(через публичный Files API), link (`.url` файлы), text file attachments
и bounded binary attachments. Конвертации записываются в Activity.
## 9. `official.search`
## 8. `verstak.search`
Назначение:
- workspace-scoped search UI;
- baseline recursive text search through public Files API;
- search provider contribution for platform discovery;
- index notes/files/activity/worklog later;
- typo/layout tolerant search later.
- workspace-scoped search;
- поиск по мере ввода;
- поиск по именам файлов/папок и содержимому текстовых файлов;
- persistent search index;
- cross-provider runtime hosting.
Provides:
```text
```
search
search.provider
search.indexer
```
Текущий статус: базовый `verstak.search` implemented as a workspace item and
`searchProviders` contribution. It searches as the user types, matches vault
file/folder names and paths, scans text-like file contents through
`api.files.list` / `api.files.readText`, and opens file results through
Workbench. It persists a workspace-scoped JSON search index in the plugin data
namespace, refreshes it from public file events, registers its own vault-text
provider as a command-backed `searchProviders` handler, and fans out to other
registered provider commands at runtime. Full-text ranking, typo/layout
tolerant search, and sidecar indexing remain later work.
Текущий статус: реализован как workspace item и `searchProviders` contribution.
Ищет по мере ввода, индексирует vault, обновляет индекс по файловым событиям,
поддерживает других search providers через runtime.
Target UX: search should be available from the workspace header next to the
workspace title. The standalone Search workspace item may remain only as an
expanded results surface; it should not be the primary search entry point.
## 10. `official.secrets`
## 9. `verstak.secrets`
Назначение:
- защищенное хранилище клиентских доступов;
- SSH/CMS/VPS/database/API secrets;
- bridge secret, sync token, device private key, pairing token;
- permissions for secret access.
- защищённое хранилище доступов;
- workspace-scoped secrets;
- master password (AES-GCM).
Provides:
```text
```
secret-store
secrets.read-ui
secrets.write-ui
```
Важное правило:
Текущий статус: реализован. Desktop core имеет AES-GCM secret store,
разблокируемый один раз за сессию. Плагин показывает global и workspace-scoped
секреты, поддерживает редактирование, удаление, копирование markdown secret links.
Рендерит `verstak-secret://...` ссылки.
- секреты не должны храниться как обычный markdown/plain text;
- доступ к secret-store должен идти через permissions;
- плагины не получают `secrets.read` автоматически.
Текущий статус: platform contract defines `secret-store`, `secrets.read-ui`,
and `secrets.write-ui` capabilities plus dangerous `secrets.read` /
`secrets.write` permissions in the SDK manifest contract. Desktop core has a
local AES-GCM secret store unlocked once per app session by master password.
The official `verstak.secrets` plugin shows global and workspace-scoped
secrets grouped by workspace, separates first-run master-password setup from
later unlock, supports editing and deleting secrets, shows selected secret
fields as a table, copies markdown secret links, and handles `secret`
workbench resources. The default editor renders
`verstak-secret://...` markdown links only when a secrets open-provider is
enabled, and opens the Secrets plugin without inserting raw values into notes.
## 11. `official.templates`
## 10. `verstak.todo`
Назначение:
- шаблоны дел;
- client/project/server/device structures;
- initial folder/note/action layout.
- списки задач внутри дел и глобально;
- статусы: open, done, cancelled;
- приоритеты, due date, reminders;
- создание Journal записи из завершённого todo.
Provides:
```text
```
workspace.todo
```
Текущий статус: реализован. Reminders с нативными desktop-уведомлениями
(при наличии capability `verstak/core/notifications/v1`). Поддерживает
создание Journal записи из todo с копированием фактических данных.
## 11. `verstak.trash`
Назначение:
- глобальный просмотр удалённых файлов;
- восстановление из корзины;
- перманентное удаление.
Provides:
```
trash.management
```
Текущий статус: реализован как глобальный sidebar view. Работает
с internal trash storage (`.verstak/trash/files/`).
## 12. `verstak.sync`
Назначение:
- ручная синхронизация vault;
- отображение статуса и конфликтов;
- настройки подключения к sync server.
Provides:
```
sync.ui
```
Requires:
```
network.remote
```
Текущий статус: реализован как глобальный sidebar view и settings panel.
Показывает vaultId, статус, unpushed count, ошибки и конфликты.
Не делает auto-resolve конфликтов.
## 13. `verstak.templates`
Назначение:
- шаблоны дел при создании;
- предопределённые наборы workspace tools;
- one-time применение (шаблон не привязан к делу после создания).
Provides:
```
case.templates
```
Optional requires:
Текущий статус: реализован через built-in templates в desktop core
(General, Project, Writing, Admin, Minimal). Модальное окно создания
дела показывает описание шаблона и его вкладки.
```text
workspace.notes
workspace.files
```
## 14. `verstak.platform-test`
## 12. Первый минимальный набор
Назначение:
Для первого платформенного этапа достаточно:
- тестовый плагин для отладки runtime;
- диагностические панели для проверки API;
- не для конечных пользователей.
- `official.files`;
- `official.notes`;
- `official.markdown-editor`;
- `official.file-preview`;
- `official.activity`;
- `official.browser-inbox`;
Но все они должны быть настоящими динамическими плагинами, даже если поставляются вместе с приложением.
Текущий статус: используется только при разработке.

View File

@ -2,9 +2,7 @@
## 1. Цель
Verstak v2 разрабатывается как отдельное приложение и платформа. Цель -
собрать рабочий local-first vault вокруг дел без временных мостов к первой
версии:
Verstak развивается как local-first платформа с динамическими плагинами:
- local-first vault;
- дела как центр контекста;
@ -12,23 +10,19 @@ Verstak v2 разрабатывается как отдельное прилож
- заметки, файлы, журнал, активность, браузерные материалы вокруг дела;
- синхронизация и расширение как отдельные части, а не ядро смысла.
Это не миграция v1 -> v2 и не перенос старого монолита частями. Это развитие
самостоятельной платформенной модели.
## 2. Нельзя делать
- Нельзя добавлять новые функции в монолитный `App.svelte`.
- Нельзя делать official plugins скрытыми compile-time modules.
- Нельзя связывать плагины по именам, если нужна capability.
- Нельзя делать notes/files/editor обязательными частями core.
- Нельзя добавлять временные compatibility bridges к первой версии.
- Нельзя менять v2 vault layout без явного плана изменения формата.
- Нельзя менять vault layout без явного плана изменения формата.
- Нельзя хранить секреты как обычные заметки или plain text.
- Нельзя молча менять title/filename note при конфликте.
## 3. Этап 1 - Platform Skeleton
Сделать в `verstak-desktop`:
Сделано в `verstak-desktop`:
- plugin manifest schema;
- plugin discovery from plugin directories;
@ -41,17 +35,9 @@ Verstak v2 разрабатывается как отдельное прилож
- basic event bus;
- diagnostics panel.
Проверки:
- приложение запускается без плагинов;
- приложение показывает пустой Plugin Manager;
- тестовый плагин появляется в списке;
- enable/disable работает;
- failed plugin не роняет приложение.
## 4. Этап 2 - Frontend Plugin Host
Сделать:
Сделано:
- загрузку frontend bundle;
- `VerstakPluginAPI`;
@ -59,16 +45,11 @@ Verstak v2 разрабатывается как отдельное прилож
- plugin UI error boundary;
- settings panel invocation from Plugin Manager.
Проверки:
- тестовый плагин регистрирует view;
- тестовый плагин регистрирует settings panel;
- выключение плагина убирает view/settings;
- ошибка в plugin UI не роняет shell.
## 5. Этап 3 - Backend Sidecar Host
Сделать:
Отложено до отдельного milestone.
План:
- sidecar launch protocol;
- local RPC;
@ -76,57 +57,63 @@ Verstak v2 разрабатывается как отдельное прилож
- sidecar shutdown/restart;
- logs/diagnostics.
Проверки:
- sidecar отвечает health check;
- sidecar не получает API без permissions;
- падение sidecar переводит плагин в failed;
- disable останавливает sidecar.
## 6. Этап 4 - Official Plugin Development
Развивать official plugins по одному, через общий plugin runtime:
Развиваются по одному, через общий plugin runtime:
1. `official.markdown-editor`;
2. `official.files`;
3. `official.notes`;
4. `official.file-preview`;
5. `official.activity`;
6. `official.browser-inbox`.
1. `verstak.default-editor`
2. `verstak.files`
3. `verstak.notes`
4. `verstak.file-preview` ✅ (базовый)
5. `verstak.activity`
6. `verstak.journal` ✅ (базовый)
7. `verstak.browser-inbox`
8. `verstak.search` ✅ (базовый)
9. `verstak.secrets`
10. `verstak.todo`
11. `verstak.trash`
12. `verstak.sync`
13. `verstak.templates`
После каждого выноса:
После каждого изменения:
- проверить build;
- проверить запуск;
- проверить Plugin Manager;
- проверить enable/disable;
- проверить degraded mode при отключении optional plugin;
- проверить, что v2 vault остается читаемым и не получает скрытых
plugin-specific truth-слоев для пользовательских документов;
- проверить, что vault остается читаемым;
- проверить, что UI contributions исчезают при disable.
## 7. Этап 5 - Repository Split
Когда plugin runtime работает:
Выполнено:
- оставить core и shell в `verstak-desktop`;
- вынести official plugins в `verstak-official-plugins`;
- вынести sync server в `verstak-sync-server`;
- вынести browser extension в `verstak-browser-extension`;
- выделить `verstak-sdk` после стабилизации API.
- core и shell в `verstak-desktop`;
- official plugins в `verstak-official-plugins`;
- sync server в `verstak-sync-server`;
- browser extension в `verstak-browser-extension`;
- SDK в `verstak-sdk`;
- документация в `verstak-docs`.
Не начинать с физического split repo, пока runtime не умеет грузить плагины локально.
## 8. Definition Of Done (текущий статус)
## 8. Definition Of Done
Платформенный переход состоялся:
Платформенный переход можно считать состоявшимся, когда:
- [x] core запускается без official plugins;
- [x] official plugins лежат вне core modules;
- [x] notes/files/editor/preview/activity работают как плагины;
- [x] Plugin Manager умеет включать/выключать плагины;
- [x] плагин может иметь свое settings окно;
- [x] capability registry управляет видимостью actions;
- [x] отсутствие optional capability не считается ошибкой;
- [x] vault layout стабилен и читаем;
- [x] документация соответствует реализации (в процессе).
- core запускается без official plugins;
- official plugins лежат вне core modules;
- notes/files/editor/preview/activity работают как плагины;
- Plugin Manager умеет включать/выключать плагины;
- плагин может иметь свое settings окно;
- capability registry управляет видимостью actions;
- отсутствие optional capability не считается ошибкой;
- v2 vault layout остается стабильным и читаемым без compatibility bridges к v1;
- документация соответствует реализации.
Оставшиеся крупные задачи:
- [ ] Sidecar/sandbox изоляция;
- [ ] Production-grade packaging и автообновление;
- [ ] Operation-log retention (sync server);
- [ ] Синхронизация Secrets, plugin settings, Todo, Journal, Activity, Browser Inbox;
- [ ] UX-полировка (Today flow, mobile layout, search в workspace header).

View File

@ -1,26 +1,24 @@
# Verstak2 Full Implementation Roadmap
# Verstak Implementation Roadmap
## 1. Goal
Bring Verstak2 to a complete standalone local-first desktop product:
Bring Verstak to a complete standalone local-first desktop product:
- core platform and UI shell can run without official plugins;
- official plugins provide the user-facing tools;
- vault data stays human-readable and local-first;
- sync UI/settings, browser capture, activity, journal, preview, search, and
secrets are plugin/runtime extensions; sync correctness (scanner, operation
log, reconciliation, and workspace identity) stays in Desktop core;
- no compatibility bridge to the first Verstak version is introduced.
log, reconciliation, and workspace identity) stays in Desktop core.
## 2. Non-negotiable Constraints
- No v1 migration bridge or temporary compatibility layer.
- No notes/files/editor/activity/journal/browser inbox feature may become a
required core module.
- User documents remain ordinary vault files unless a feature explicitly needs
protected storage, such as secrets.
- `Overview.md` is an ordinary Markdown filename, not a special UI entity.
- New plugin-facing behavior must go through public Verstak2 API contracts and
- New plugin-facing behavior must go through public API contracts and
SDK/schema updates.
- Every significant step must be verified, committed, and pushed separately.
@ -33,47 +31,39 @@ Implemented:
- bundled frontend plugin host and `VerstakPluginAPI`;
- Command Palette UI host for `commands` contributions;
- Status Bar UI host for `statusBarItems`, vault status, and settings menu;
- workspace top-level folder model and workspace item host;
- Files Core API with safe path policy, durable snapshot scanning, and core sync
operation recording;
- public `files.openExternal` / `files.showInFolder` API and Files plugin usage;
- workspace top-level folder model, workspace item host, workspace templates;
- Files Core API with safe path policy, durable snapshot scanning, atomic
text/binary writes, and core sync operation recording;
- public `files.openExternal` / `files.showInFolder` API;
- mode-aware Workbench open/edit provider routing and default editor plugin;
- official Files plugin, Notes plugin, Markdown Editor plugin, Search plugin,
Sync plugin, and platform-test plugin;
- browser inbox local receiver and minimal official Browser Inbox plugin;
- sync server with device/user auth and operation push/pull;
- SDK manifest/types/schema coverage for current plugin APIs;
- all official plugins: Files, Notes, Default Editor, File Preview, Activity,
Journal, Browser Inbox, Search, Secrets, Todo, Trash, Sync, Templates;
- browser inbox local receiver with paired mode, domain bindings, and
create-note/link/file conversion;
- sync server with device/user auth, operation push/pull, Blob transport,
embedded web console, and real two-vault smoke scenarios;
- SDK manifest/types/schema coverage for plugin APIs;
- persisted System/English/Russian application language selection, localized
desktop shell, public `api.i18n` plugin contract, and bilingual catalogs for
all official plugins;
- automated Go, frontend, official plugin, SDK, and real-sync smoke checks.
- automated Go, frontend, official plugin, SDK, and real-sync smoke checks;
- tray icon with native menu and native desktop notifications;
- AES-GCM secret store with master password, UI plugin.
Known remaining gaps:
- `fileActions`, `noteActions`, and `contextMenuEntries` are exposed through the
desktop contribution summary and hosted by the official Files/Notes surfaces.
- Sidecar host is not implemented.
- Files/Notes are usable but not complete: chunked streaming/large-file import,
richer conflict UX, and remaining Notes polish are still incomplete.
- Activity, Journal, Browser Inbox conversion workflows, indexed Search, and
Secrets now have baseline plugin implementations and public API contracts.
Their remaining work is product UX depth: richer Today aggregation,
actionable Activity to Journal review flows, capture-from-clipboard/manual
capture, production-grade reporting, and final polish.
- Templates plugin is not implemented yet.
- File/image preview exists as a basic provider with bounded inline image
rendering through the public Files API.
- Browser extension repository has protocol, queue, and Chromium/Firefox build
scaffold; desktop has a bounded, token-paired local receiver and mounted-view
inbox plugin; receiver pairing settings, basic Browser Inbox domain binding,
create-note conversion, and
create-link conversion are implemented, text file attachment conversion is
implemented, bounded binary attachment conversion is implemented, and Activity
records conversions. Chunked large-file attachment capture remains future
work.
- Packaging/update/release workflow is not product-grade yet.
- Browser extension UI localization is not yet migrated to the shared
multilingual product policy.
- Sidecar host is not implemented (bundled plugins run in shared JS context).
- Chunked streaming/large-file import is deferred; bounded text (2 MB) and
byte (8 MB) APIs are the current limits.
- UX polish is ongoing: Today flow as work-resume surface, Activity-to-Journal
review, mobile/responsive layout, search in workspace header.
- Production-grade packaging, auto-update, and release workflow is partial:
build scripts exist, but not a polished update channel.
- Sync operation-log retention/compaction is intentionally deferred.
- Secrets, plugin settings, Todo, Journal, Activity, and Browser Inbox are not
synchronized yet.
- Browser extension chunked large-file capture is future work.
- Automatic conflict resolution is intentionally absent.
## 4. Implementation Phases
@ -93,11 +83,7 @@ Tasks:
practical;
- [x] document each public API in SDK schemas and desktop runtime docs.
Verification:
- focused e2e/smoke tests for command palette, status bar, action surfaces, and
contribution lifecycle;
- `go test ./...`, frontend build/e2e, official plugin checks, SDK tests.
Status: done.
### Phase 2 - Files And Notes Product Surface
@ -111,14 +97,11 @@ Tasks:
- [x] add Notes delete/trash through Files API with confirmation;
- [x] add Files restore metadata view;
- [x] add Files restore command;
- [x] define external open/show-in-folder as a public v2 API, replacing fallback;
- [x] define external open/show-in-folder as a public v2 API;
- [x] add watcher-based refresh for Files/Notes after external changes;
- [x] add safe binary read/streaming contract only after text workflows are stable.
- [x] add safe binary read/streaming contract.
Verification:
- plugin smoke tests and desktop e2e for create/open/rename/trash/reload;
- no `.verstak/notes`, no special `Overview.md`, no direct backend bypasses.
Status: done.
### Phase 3 - Sync Hardening
@ -131,74 +114,32 @@ Verified in the current implementation:
- [x] add server/device revocation checks for sync auth paths;
- [x] persist and display sync errors in Sync plugin;
- [x] add retry/backoff for sync client operations;
- [x] persist an atomic core snapshot under `.verstak/sync/`; scan on vault
open, before manual sync, and after watcher debounce; exclude internal paths,
trash, temporary files, and symlinks;
- [x] reconcile initial vaults safely: pull before local bootstrap, never turn
an empty initial snapshot into deletes, and stop on an incompatible
local/remote path conflict;
- [x] apply pulled operations strictly by `server_sequence`, stop at the first
failed operation, retain cursor/retry state across restart, and avoid watcher
echo after a successful remote apply;
- [x] sync workspace/Deal create, rename, trash, and restore through a
core-owned entity carrying the durable `workspaceId` rather than exposing the
workspace marker to plugins;
- [x] add real two-vault smoke scenarios for external create/update/delete,
scanner-based rename representation, no remote echo, and workspace lifecycle;
- [x] document deployment and backup procedures for `verstak-sync-server`.
- [x] persist an atomic core snapshot under `.verstak/sync/`;
- [x] reconcile initial vaults safely;
- [x] apply pulled operations strictly by `server_sequence`;
- [x] sync workspace create, rename, trash, and restore through core-owned
entity with durable `workspaceId`;
- [x] add real two-vault smoke scenarios;
- [x] document deployment and backup procedures for sync-server;
- [x] bind sync-server to loopback by default, set HTTP timeouts/graceful
shutdown, publish health/readiness/build data, and document nginx/Caddy
trusted-proxy deployment;
- [x] bound JSON/push fields and pull pages, return stable public error codes,
and make Desktop stop/persist cursor at the first failed page operation;
- [x] add streamed Blob transport with SHA-256/size verification, atomic local
apply, per-user/vault ownership references, file/quota limits, and immediate
revoked-device denial;
- [x] hash newly issued device, session, confirmation, and reset tokens;
persist sessions, require CSRF for browser mutations, and use transactions
for pairing, credential, blob, and multi-table user/device changes.
- [x] add an embedded, dependency-free sync-server web console with shared
responsive templates for public/account/admin flows, Russian/English/system
locale preference, user device revoke, and operational admin sections for
users, devices, vaults, storage, audit, SMTP settings, and diagnostics;
protect its mutations with sessions, CSRF, security headers, and
administrator re-authentication for sensitive changes.
- [x] remove the former hardcoded server HTML and split the embedded console
into shared layout/sidebar plus public, account, and per-admin-section
templates; add locale-aware timestamps, local confirmation dialog, safe
public-form CSRF, server-side filters/sorting/pagination, user email
confirmation/device lifecycle actions, one-time generated admin password
resets without URL/initial-HTML/audit/cookie/plaintext persistence, vault metadata diagnostics without file
payloads, sanitised diagnostics download, and an interactive headless-
Chromium smoke script without npm dependencies.
shutdown, publish health/readiness/build data;
- [x] bounded JSON/push fields and pull pages;
- [x] add streamed Blob transport with SHA-256/size verification;
- [x] hash tokens, persist sessions, require CSRF for browser mutations;
- [x] add embedded sync-server web console with shared responsive templates;
- [x] remove hardcoded server HTML, split into shared layout/sidebar plus
per-section templates.
Known limits in this phase:
Known limits:
- [x] The Files plugin API remains bounded to 2 MB text / 8 MB byte reads;
core sync itself sends binary/larger files through the Blob API. Files over
configured Blob/quota limits remain persistent visible warnings and are not
marked synchronized.
- [x] External rename is represented as delete + create; it is not a
cross-device rename detector for ordinary files.
- [ ] Automatic conflict resolution is intentionally absent. Operation-log
retention/compaction is also intentionally deferred: without a server
checkpoint/materialized state and proven device recovery cursors, pruning
operations could make a newly paired device unrecoverable. Safe cleanup is
limited to sessions/tokens/idempotency/audit/temp uploads/rate buckets.
- [x] Files plugin API remains bounded to 2 MB text / 8 MB byte reads.
- [x] External rename is represented as delete + create.
- [ ] Operation-log retention/compaction is intentionally deferred.
- [ ] Secrets, plugin settings, Todo, Journal, Activity, and Browser Inbox are
not synchronized in this phase.
- [ ] The embedded control plane intentionally remains server-rendered. A
separate milestone may improve its visual design or add richer client-side
interactions, but must preserve the local-first desktop model and server-side
authorization/CSRF checks.
not synchronized.
Verification:
- sync server `go test ./...`, `go vet ./...`, systemd validation and a local
release-package smoke;
- desktop unit/API tests and real two-vault smoke including a binary above the
former inline limit;
- SDK schema/type checks and official Sync plugin build/localization checks.
Status: core sync foundations complete. Retention and additional data domains
are future milestones.
### Phase 4 - Preview, Search, Activity, Journal
@ -206,30 +147,18 @@ Goal: add the next visible product layer as replaceable plugins.
Tasks:
- [x] keep Markdown preview inside `verstak.markdown-editor`, with no separate
- [x] keep Markdown preview inside `verstak.default-editor`, with no separate
provider competing for `.md` files;
- [x] implement basic image metadata preview plugin;
- [x] implement baseline `verstak.search` workspace plugin and expose
`searchProviders` in contribution summaries;
- [x] add type-as-you-search behavior and vault path/name matches to
`verstak.search`;
- [x] implement baseline `verstak.activity` event log plugin with
workspace-scoped storage, global aggregation, and public event subscriptions;
- [x] expose `verstak.activity` and `verstak.browser-inbox` as global sidebar
views while keeping their workspace items;
- [x] implement baseline `verstak.search` workspace plugin;
- [x] add type-as-you-search behavior and vault path/name matches;
- [x] implement baseline `verstak.activity` event log plugin;
- [x] expose Activity and Browser Inbox as global sidebar views;
- [x] implement persistent search index and cross-provider runtime hosting;
- [x] implement activity reconstruction and worklog suggestions;
- [x] implement journal/worklog plugin that can consume activity suggestions.
Verification:
- provider selection tests;
- plugin lifecycle tests proving optional dependencies degrade cleanly.
Status: baseline platform/product contracts are implemented. Remaining work is
tracked as UX follow-up rather than missing runtime foundation: Today should
make Activity suggestions and Journal imports actionable from the first
workspace screen, and Journal still needs reporting/timer/export depth.
Status: done. Remaining work is UX depth (Today flow, reporting, timers).
### Phase 5 - Browser Inbox
@ -239,31 +168,19 @@ receiver and an official inbox plugin.
Tasks:
- [x] define browser capture payload protocol;
- [x] implement minimal `verstak.browser-inbox` plugin with workspace-scoped
pending queues and a global aggregate view;
- [x] implement browser extension capture scaffold for URL, selected text,
page title, and link captures;
- [x] implement minimal `verstak.browser-inbox` plugin;
- [x] implement browser extension capture scaffold;
- [x] define local receiver permission/pairing model;
- [x] require an installation-local pairing token and expose its rotation through
Browser Inbox settings;
- [x] bound browser receiver payloads and file content before publishing events;
- [x] require an installation-local pairing token;
- [x] add domain-to-workspace binding;
- [x] convert inbox entries into notes through public plugin APIs;
- [x] record converted inbox entries in Activity through public plugin events;
- [x] convert inbox entries into link files through public plugin APIs;
- [x] convert captured text file attachments through public plugin APIs;
- [x] convert captured bounded binary attachments through public plugin APIs.
- [x] record converted inbox entries in Activity;
- [x] convert inbox entries into link files;
- [x] convert captured text file attachments;
- [x] convert captured bounded binary attachments.
Verification:
- extension build checks;
- local receiver API tests;
- inbox plugin smoke/e2e tests.
Status: baseline capture, pairing, routing, conversion, and Activity recording
workflows are implemented. Remaining work is UX follow-up:
capture-from-clipboard/manual capture, domain binding state, and conversion
outcomes in the visible app flow.
Status: done. Remaining work: capture-from-clipboard/manual capture,
chunked large-file capture.
### Phase 6 - Secrets
@ -275,13 +192,9 @@ Tasks:
- [x] define secret-store capability and permissions;
- [x] implement encrypted local secret storage;
- [x] add UI-only official secrets plugin;
- [x] integrate secret references with workspaces without exposing raw values to
unrelated plugins.
- [x] integrate secret references with workspaces.
Verification:
- permission denial tests;
- storage encryption and no-plaintext regression checks.
Status: done.
### Phase 7 - Sidecar/Sandbox Boundary
@ -289,16 +202,13 @@ Goal: move from trusted bundled plugin JavaScript toward safer plugin execution.
Tasks:
- implement sidecar launch protocol and local RPC;
- permission-scope sidecar APIs;
- stop sidecars on plugin disable;
- surface sidecar logs and health in Plugin Manager;
- document supported sidecar languages and packaging rules.
- [ ] implement sidecar launch protocol and local RPC;
- [ ] permission-scope sidecar APIs;
- [ ] stop sidecars on plugin disable;
- [ ] surface sidecar logs and health in Plugin Manager;
- [ ] document supported sidecar languages and packaging rules.
Verification:
- sidecar health and permission tests;
- disable/failure lifecycle tests.
Status: not started.
### Phase 8 - Packaging, Update, Release
@ -306,40 +216,31 @@ Goal: produce installable, recoverable releases.
Tasks:
- define release artifact matrix for desktop, official plugins, SDK, sync server,
and browser extension;
- add plugin package signing/verification or equivalent integrity checks;
- add backup/restore docs for vault and sync server;
- add crash/log collection for local diagnostics;
- build release smoke checklist.
- [x] define release artifact matrix (desktop, plugins, SDK, sync server,
browser extension);
- [x] add build scripts for .deb, AppImage, Windows portable, and plugin
packages;
- [ ] add plugin package signing/verification or equivalent integrity checks;
- [x] add backup/restore docs for vault and sync server;
- [ ] add crash/log collection for local diagnostics;
- [ ] build release smoke checklist.
Verification:
- clean build from clone;
- packaged desktop starts with bundled official plugins;
- documented recovery path works on a sample vault.
Status: packaging scripts exist and produce release artifacts. Signing,
auto-update, and release smoke checklist are future work.
## 5. Immediate Execution Order
1. [x] Command Palette UI host in `verstak-desktop`.
2. [x] Status bar item host in `verstak-desktop`.
3. [x] External open public v2 API to replace Files fallback.
4. [x] Notes trash/delete UX in `verstak-official-plugins`.
5. [x] Sync hardening pass: snapshots/reconciliation, workspace identity,
bounded pagination, Blob transport/ownership/quota, secure token/session
policy, and expanded real two-vault smoke. Checkpoint-based operation
retention and additional data domains remain future work.
6. [x] Browser inbox protocol design, extension scaffold, local receiver,
minimal inbox plugin, and note/link/text-file/binary-file conversions are
implemented.
7. [ ] Product UX follow-up in `verstak-desktop`: make the shell-level Today
flow the command center for captures, recent activity, Activity worklog
suggestions, and Journal import/review. This should reuse existing official
plugin contracts instead of moving Activity, Browser Inbox, or Journal into
desktop core.
This order finishes generic platform surfaces before building product features
that depend on them.
1. [x] Command Palette UI host.
2. [x] Status bar item host.
3. [x] External open public API.
4. [x] Notes trash/delete UX.
5. [x] Sync hardening pass.
6. [x] Browser inbox protocol, extension scaffold, local receiver, inbox plugin,
and conversions.
7. [ ] Product UX follow-up: make the shell-level Today flow the command center
for captures, recent activity, Activity worklog suggestions, and Journal
import/review. This should reuse existing official plugin contracts instead
of moving Activity, Browser Inbox, or Journal into desktop core.
## 6. Stop Conditions

View File

@ -26,11 +26,11 @@
Перед работой прочитать:
- `verstak-platform-docs/01_Product_Vision.md`
- `verstak-platform-docs/02_Platform_Architecture.md`
- `verstak-platform-docs/04_Plugin_System.md`
- `verstak-platform-docs/05_Official_Plugins.md`
- `verstak-platform-docs/06_Migration_Strategy.md`
- `verstak-docs/01_Product_Vision.md`
- `verstak-docs/02_Platform_Architecture.md`
- `verstak-docs/04_Plugin_System.md`
- `verstak-docs/05_Official_Plugins.md`
- `verstak-docs/06_Migration_Strategy.md`
Если код противоречит документам, не молча подгонять документы под код. Сначала понять, это старый монолитный долг или осознанное новое решение.

View File

@ -1,25 +1,35 @@
<div align="center">
# Verstak Platform Docs
### Architecture and platform documentation for Verstak.
**English** · [Русский](README.ru.md)
</div>
# Platform Architecture Reference
Сводные архитектурные артефакты платформы Верстак.
Architecture artifacts for the Verstak platform.
> Первый публичный выпуск — alpha. Инструкции для сборки и упаковки каждого
> исполняемого компонента находятся в его README: Desktop, official plugins,
> browser extension и SDK должны собираться из одной release-линейки.
> Each executable component's build instructions live in its own README:
> Desktop, official plugins, browser extension and SDK must be built from
> the same release line.
## Содержание
## Contents
- [Product Vision](01_Product_Vision.md) — что остаётся неизменным
- [Platform Architecture](02_Platform_Architecture.md) — устройство ядра
- [Repositories](03_Repositories.md) — разбиение на репозитории
- [Plugin System](04_Plugin_System.md) — динамические плагины
- [Official Plugins](05_Official_Plugins.md) — состав официальных плагинов
- [Platform Development Strategy](06_Migration_Strategy.md) — план развития v2
- [Full Implementation Roadmap](07_Full_Implementation_Roadmap.md) — путь до полной реализации Verstak2
- [Product Vision](01_Product_Vision.md) — what stays unchanged
- [Platform Architecture](02_Platform_Architecture.md) — core design
- [Repositories](03_Repositories.md) — repository layout
- [Plugin System](04_Plugin_System.md) — dynamic plugins
- [Official Plugins](05_Official_Plugins.md) — plugin set and current status
- [Development Strategy](06_Migration_Strategy.md) — platform evolution
- [Implementation Roadmap](07_Full_Implementation_Roadmap.md) — full roadmap with phase status
## Схемы SDK
## SDK Schemas
Схемы данных и контракты SDK находятся в репозитории
[`verstak-sdk`](https://github.com/mirivlad/verstak-sdk).
Data schemas and SDK contracts live in the
[`verstak-sdk`](https://github.com/mirivlad/verstak-sdk) repository.
- [Manifest Schema](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/manifest.json)
- [Capabilities](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/capabilities.json)
@ -28,18 +38,18 @@
- [Event Schemas](https://github.com/mirivlad/verstak-sdk/tree/main/schemas/events)
- [Sync Operations](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/sync.json)
## Репозитории
## Repositories
| Репозиторий | Назначение |
| Repository | Purpose |
|---|---|
| [`verstak`](https://github.com/mirivlad/verstak) | Core Platform + UI Shell |
| [`verstak-official-plugins`](https://github.com/mirivlad/verstak-official-plugins) | Официальные плагины |
| [`verstak-sdk`](https://github.com/mirivlad/verstak-sdk) | Plugin SDK и схемы |
| [`verstak-sync-server`](https://github.com/mirivlad/verstak-sync-server) | Сервер синхронизации |
| [`verstak-browser-extension`](https://github.com/mirivlad/verstak-browser-extension) | Расширение браузера |
| [`verstak-docs`](https://github.com/mirivlad/verstak-docs) | Документация |
| [`verstak-official-plugins`](https://github.com/mirivlad/verstak-official-plugins) | Official plugins |
| [`verstak-sdk`](https://github.com/mirivlad/verstak-sdk) | Plugin SDK and schemas |
| [`verstak-sync-server`](https://github.com/mirivlad/verstak-sync-server) | Sync server |
| [`verstak-browser-extension`](https://github.com/mirivlad/verstak-browser-extension) | Browser extension |
| [`verstak-docs`](https://github.com/mirivlad/verstak-docs) | Documentation |
## Лицензия
## License
Copyright © 2026 Verstak contributors. Документация распространяется на
условиях [GNU AGPLv3 или новее](LICENSE).
Copyright © 2026 Verstak contributors. Documentation is licensed under
[GNU AGPLv3 or later](LICENSE).

55
README.ru.md Normal file
View File

@ -0,0 +1,55 @@
<div align="center">
# Verstak Platform Docs
### Architecture and platform documentation for Verstak.
[English](README.md) · **Русский**
</div>
# Platform Architecture Reference
Сводные архитектурные артефакты платформы Верстак.
> Сборка каждого исполняемого компонента описана в его README: Desktop,
> official plugins, browser extension и SDK должны собираться из одной
> release-линейки.
## Содержание
- [Product Vision](01_Product_Vision.md) — что остаётся неизменным
- [Platform Architecture](02_Platform_Architecture.md) — устройство ядра
- [Repositories](03_Repositories.md) — разбиение на репозитории
- [Plugin System](04_Plugin_System.md) — динамические плагины
- [Official Plugins](05_Official_Plugins.md) — состав официальных плагинов
- [Platform Development Strategy](06_Migration_Strategy.md) — стратегия развития
- [Full Implementation Roadmap](07_Full_Implementation_Roadmap.md) — дорожная карта
## Схемы SDK
Схемы данных и контракты SDK находятся в репозитории
[`verstak-sdk`](https://github.com/mirivlad/verstak-sdk).
- [Manifest Schema](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/manifest.json)
- [Capabilities](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/capabilities.json)
- [Contributions](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/contributions.json)
- [Permissions](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/permissions.json)
- [Event Schemas](https://github.com/mirivlad/verstak-sdk/tree/main/schemas/events)
- [Sync Operations](https://github.com/mirivlad/verstak-sdk/blob/main/schemas/sync.json)
## Репозитории
| Репозиторий | Назначение |
|---|---|
| [`verstak`](https://github.com/mirivlad/verstak) | Core Platform + UI Shell |
| [`verstak-official-plugins`](https://github.com/mirivlad/verstak-official-plugins) | Официальные плагины |
| [`verstak-sdk`](https://github.com/mirivlad/verstak-sdk) | Plugin SDK и схемы |
| [`verstak-sync-server`](https://github.com/mirivlad/verstak-sync-server) | Сервер синхронизации |
| [`verstak-browser-extension`](https://github.com/mirivlad/verstak-browser-extension) | Расширение браузера |
| [`verstak-docs`](https://github.com/mirivlad/verstak-docs) | Документация |
## Лицензия
Copyright © 2026 Verstak contributors. Документация распространяется на
условиях [GNU AGPLv3 или новее](LICENSE).

View File

@ -1,100 +0,0 @@
# Milestone: Platform Runtime 1
Фиксирует текущее состояние plugin runtime в Milestone 1.
## Что уже работает
- **Plugin Discovery** — сканирование директорий, чтение plugin.json, валидация манифеста.
- **Core Capabilities** — 5 capabilities регистрируются в `main.go` ДО plugin discovery.
- **Plugin Lifecycle** — полный цикл: discovered → loaded / degraded / missing-required-capability / failed.
- **Dev Install Flow**`./scripts/build.sh` + `./scripts/install-dev-plugins.sh`.
- **Smoke Platform**`./scripts/smoke-platform.sh` проверяет discovery, capabilities, lifecycle без GUI.
- **Wails v2 + Svelte 4** — core запускается, Plugin Manager UI работает.
- **platform-test plugin** — собирается, устанавливается, проходит lifecycle, регистрирует contributions.
## Репозитории
| Репозиторий | За что отвечает |
|---|---|
| `verstak-desktop` | Core platform: plugin runtime, capability/contribution/permission registries, event bus, Wails shell, Plugin Manager UI |
| `verstak-official-plugins` | Монорепо официальных плагинов (platform-test и будущие) |
| `verstak-docs` | Документация платформы (этот репозиторий) |
| `verstak-sdk` | Manifest schema + TypeScript SDK |
| `verstak-sync-server` | Сервер синхронизации |
| `verstak-browser-extension` | Расширение Firefox |
## Core Capabilities (зарегистрированы)
Следующие capabilities регистрируются в `main.go` до plugin discovery, чтобы плагины могли разрешить `requires` при загрузке:
| Capability | Описание |
|---|---|
| `verstak/core/plugin-manager/v1` | Управление плагинами: discovery, enable/disable, reload |
| `verstak/core/capability-registry/v1` | Реестр возможностей: регистрация, запрос, проверка зависимостей |
| `verstak/core/contribution-registry/v1` | Реестр контрибуций: views, commands, sidebar items, actions и т.д. |
| `verstak/core/permissions/v1` | Реестр разрешений: проверка dangerous, запрос пользователю |
| `verstak/core/events/v1` | In-process event bus: publish/subscribe |
### Capabilities НЕ зарегистрированы (намеренно)
| Capability | Почему отсутствует |
|---|---|
| `verstak/core/vault/v1` | Реализация vault api (`internal/core/vault/api.go`) ещё не создана |
| `verstak/core/sync/v1` | Реализация sync boundary (`internal/core/sync/boundary.go`) ещё не создана |
Плагины, объявляющие эти capabilities в `optionalRequires`, переходят в статус `degraded`, но продолжают работать. Плагины с `requires` получат `missing-required-capability`.
## DEGRADED: что означает
Плагин получает статус `degraded`, когда:
1. Все `requires` capabilities разрешены (есть в registry).
2. Хотя бы одна из `optionalRequires` capabilities отсутствует.
В этом состоянии:
- Плагин загружается и регистрирует свои `provides` capabilities.
- Contributions (views, commands, sidebar items) попадают в registry.
- Plugin Manager UI показывает статус `degraded` с указанием отсутствующих optional capabilities.
- UI-функции, зависящие от отсутствующих capabilities, должны скрываться или gracefully degrade.
## Команды для полной проверки
```bash
# 1. Собрать официальные плагины (соберёт frontend, backend, упакует dist/)
cd ~/git/verstak2/verstak-official-plugins
./scripts/build.sh
# 2. Установить platform-test как dev plugin в verstak-desktop
cd ~/git/verstak2/verstak-desktop
./scripts/install-dev-plugins.sh
# 3. Запустить smoke-проверку (headless, без GUI)
./scripts/smoke-platform.sh
# 4. Запустить приложение (откроет Wails GUI)
go run -mod=mod .
```
## Проверка через Plugin Manager UI
1. Запустить приложение: `go run -mod=mod .`
2. Открыть Plugin Manager.
3. Убедиться, что `verstak.platform-test` отображается со статусом `degraded`.
4. Убедиться, что указаны отсутствующие optional capabilities (`verstak/core/vault/v1`, `verstak/core/sync/v1`).
5. Contributions (view, commands, sidebar item) должны быть видны.
## Файлы реализации
| Файл | Назначение |
|---|---|
| `verstak-desktop/main.go` | Инициализация core, регистрация capabilities, plugin lifecycle, Wails run |
| `verstak-desktop/internal/api/app.go` | Wails-bound API, ReloadPlugins, GetPlugins, GetCapabilities |
| `verstak-desktop/internal/core/plugin/plugin.go` | Manifest struct, ValidateManifest, DiscoverPlugins, Status constants |
| `verstak-desktop/internal/core/capability/registry.go` | CapabilityRegistry: Register, CheckRequired, List |
| `verstak-desktop/internal/core/contribution/registry.go` | ContributionRegistry: views, commands, sidebar items и т.д. |
| `verstak-desktop/internal/core/permissions/registry.go` | PermissionsRegistry: defaults, IsDangerous |
| `verstak-desktop/internal/core/events/bus.go` | EventBus: Subscribe, Publish |
| `verstak-desktop/cmd/smoke-platform/main.go` | Headless smoke test |
| `verstak-official-plugins/scripts/build.sh` | Build + package всех плагинов |
| `verstak-desktop/scripts/install-dev-plugins.sh` | Копирование dist/ в ./plugins/ |
| `verstak-desktop/scripts/smoke-platform.sh` | Полная smoke-проверка |

View File

@ -1,128 +0,0 @@
# Milestone: Platform Runtime 2 — Vault Core Capability
Фиксирует состояние plugin runtime после добавления vault layer как core capability.
## Что реализовано
- **Vault Layer** — полноценный vault service (`internal/core/vault/vault.go`) с созданием, открытием, закрытием и валидацией.
- **Vault Capability**`verstak/core/vault/v1` регистрируется в `main.go` и доступен плагинам через capability registry.
- **Vault Events**`vault.created`, `vault.opened`, `vault.closed` публикуются в event bus.
- **Vault Tests**`internal/core/vault/vault_test.go` покрывает layout creation, open/close cycle, corrupt JSON, path traversal, plugin namespace paths, status transitions, event publishing.
- **platform-test plugin** — статус обновлён: был `degraded` из-за отсутствия vault, теперь `degraded` только из-за отсутствия `verstak/core/sync/v1`.
## Vault Layout
```
<base>/
VerstakVault/ ← vault root (создаётся CreateVault)
.verstak/
vault.json ← VaultMeta: schemaVersion=1, vaultId (UUID), createdAt, updatedAt, app="verstak"
plugin-data/ ← per-plugin data namespaces
<plugin-id>/
plugin-settings/ ← per-plugin settings namespaces
<plugin-id>/
plugin-cache/ ← per-plugin cache namespaces
<plugin-id>/
trash/ ← soft-deleted items
logs/ ← vault-scoped logs
```
`EnsureVaultLayout()` создаёт `.verstak/` и все стандартные поддиректории. Идемпотентен — безопасно вызывать повторно.
## Vault API
| Метод | Описание |
|---|---|
| `NewVault(bus)` | Создаёт Vault instance. Начальный статус: `not-created`. |
| `CreateVault(path)` | Создаёт `VerstakVault/` на указанном пути. Генерирует `vault.json` с UUID. Публикует `vault.created`. Статус → `open`. |
| `OpenVault(path)` | Открывает существующий vault. Валидирует `vault.json` (schemaVersion, vaultId). Публикует `vault.opened`. Статус → `open`. |
| `CloseVault()` | Закрывает vault. Сбрасывает path и meta. Публикует `vault.closed`. Статус → `closed`. |
| `GetVaultStatus()` | Текущий статус: `not-created`, `closed`, `open`, `error`. |
| `GetVaultPath()` | Путь к vault root. |
| `GetVaultMeta()` | Указатель на `VaultMeta` (vaultId, timestamps, app). |
| `ResolveSafePath(rel)` | Резолвит относительный путь внутри vault. Блокирует `../` traversal. Требует `open` vault. |
| `GetPluginDataPath(id)` | Возвращает (и создаёт) `.verstak/plugin-data/<id>/`. |
| `GetPluginSettingsPath(id)` | Возвращает (и создаёт) `.verstak/plugin-settings/<id>/`. |
| `GetPluginCachePath(id)` | Возвращает (и создаёт) `.verstak/plugin-cache/<id>/`. |
## Vault Events
| Event | Когда | Payload |
|---|---|---|
| `vault.created` | Успешный `CreateVault` | `path`, `vaultId` |
| `vault.opened` | Успешный `OpenVault` | `path`, `vaultId` |
| `vault.closed` | `CloseVault` | `vaultId` |
| `vault.error` | Ошибки операций | `error` |
## Vault Status Flow
```
not-created ──CreateVault──▶ open ──CloseVault──▶ closed
│ │
└──OpenVault─────────┘
```
## Core Capabilities (зарегистрированы)
| Capability | Описание |
|---|---|
| `verstak/core/plugin-manager/v1` | Управление плагинами: discovery, enable/disable, reload |
| `verstak/core/capability-registry/v1` | Реестр возможностей: регистрация, запрос, проверка зависимостей |
| `verstak/core/contribution-registry/v1` | Реестр контрибуций: views, commands, sidebar items, actions |
| `verstak/core/permissions/v1` | Реестр разрешений: проверка dangerous, запрос пользователю |
| `verstak/core/events/v1` | In-process event bus: publish/subscribe |
| `verstak/core/vault/v1` | Vault service: создание/открытие/закрытие vault, plugin namespace paths, safe path resolution |
Все 6 capabilities регистрируются в `main.go` до plugin discovery.
## platform-test: текущий статус
| Аспект | Статус |
|---|---|
| Discovery | ✅ |
| Capability resolution (required) | ✅ |
| Capability resolution (optional: vault) | ✅ |
| Capability resolution (optional: sync) | ❌ `verstak/core/sync/v1` отсутствует |
| **Итоговый статус** | **degraded** (только из-за missing sync optional) |
## Команды для проверки
```bash
# 1. Собрать официальные плагины
cd ~/git/verstak2/verstak-official-plugins
./scripts/build.sh
# 2. Установить platform-test как dev plugin
cd ~/git/verstak2/verstak-desktop
./scripts/install-dev-plugins.sh
# 3. Smoke-проверка (headless)
./scripts/smoke-platform.sh
# 4. Запуск приложения
cd ~/git/verstak2/verstak-desktop
go run -mod=mod .
# 5. Vault unit tests
go test ./internal/core/vault/ -v
```
## Что НЕ сделано (намеренно)
- **Sync** (`verstak/core/sync/v1`) — sync boundary существует как заглушка, полная реализация позже.
- **Notes** — плагин для заметок не входит в core.
- **Files** — плагин для файлового менеджера не входит в core.
- **Editor** — markdown editor не входит в core.
Vault — фундамент для всех этих будущих плагинов, но сам по себе не зависит от них.
## Файлы реализации
| Файл | Назначение |
|---|---|
| `verstak-desktop/main.go` | Инициализация core, регистрация 6 capabilities (включая vault), plugin lifecycle |
| `verstak-desktop/internal/core/vault/vault.go` | Vault service: CreateVault, OpenVault, CloseVault, ResolveSafePath, plugin namespace paths |
| `verstak-desktop/internal/core/vault/vault_test.go` | Vault tests: layout, lifecycle, path traversal, events |
| `verstak-desktop/internal/core/capability/registry.go` | CapabilityRegistry |
| `verstak-desktop/internal/core/events/bus.go` | EventBus |
| `verstak-desktop/internal/api/app.go` | Wails API, ReloadPlugins |

View File

@ -1,107 +0,0 @@
# Milestone 4 — App Settings + Vault Plugin State + First Run UI
**Дата:** 2026-06-17
**Статус:** ✅ Завершён
## Цель
Сделать нормальную модель настроек приложения, выбор vault при первом запуске, и enable/disable плагинов через vault plugin state.
## Что сделано
### 1. App Settings Core (`internal/core/appsettings/`)
- `manager.go` — Load/Save/Update, recent vaults, defaults, corrupt config recovery
- `manager_test.go` — 6 тестов
- Хранение: `~/.config/verstak/config.json`
- Поля: currentVaultPath, recentVaults, theme, devMode, userPluginsDir, windowState, lastOpenedAt
- Правила: defaults при отсутствии, backup+recovery при битом config, без secrets
### 2. Vault Plugin State (`internal/core/pluginstate/`)
- `manager.go` — enable/disable, desired plugins, missing-installed tracking
- `manager_test.go` — 7 тестов
- Хранение: `<vault>/.verstak/plugins.json`
- Поля: enabledPlugins, disabledPlugins, desiredPlugins, updatedAt
- Installed ≠ Enabled ≠ Desired
### 3. Wails API (app.go)
- `GetAppSettings()` / `UpdateAppSettings(patch)` / `SetCurrentVault(path)`
- `GetVaultPluginState()` / `EnablePlugin(id)` / `DisablePlugin(id)`
- `SetCurrentVault` вызывает `OpenVault` + сохраняет в app settings + загружает plugin state
### 4. First Run / Vault Selection UI (VaultSelection.svelte)
- Показывается когда currentVaultPath пустой или vault не открывается
- Create New Vault → CreateVault → OpenVault → SetCurrentVault
- Open Existing Vault → OpenVault → SetCurrentVault
- Recent Vaults → OpenVault → SetCurrentVault
- Понятные ошибки при неудаче
### 5. Sidebar Navigation (Sidebar.svelte)
- Ширина 220px, фиксированная
- Навигация: Plugin Manager
- Plugin sidebar items (из contributions)
- Vault status indicator
- Отступы и hover-эффекты
### 6. Plugin Manager Integration
- Enable/Disable toggle в PluginCard
- Disabled plugin не регистрирует capabilities/contributions
- Missing installed plugins — отдельная секция
- Vault state загружается при открытии
### 7. Layout Fixes
- App.svelte: global reset (margin, padding, box-sizing)
- PluginManager: отступы header, border-bottom separator
- Content area: padding 1.5rem
## Тесты
- `go test ./...` — 52 PASS (6 appsettings + 7 pluginstate + 39 previous)
- `./scripts/check.sh` — ✅
- `./scripts/smoke-platform.sh` — ✅ (enable/disable/plugins.json verification)
- `./scripts/build.sh` — ✅
## Структура файлов
```
~/.config/verstak/config.json ← app settings (local)
<vault>/.verstak/plugins.json ← vault plugin state
<vault>/.verstak/plugin-settings/<id>/ ← per-plugin settings
<vault>/.verstak/plugin-data/<id>/ ← per-plugin data
<vault>/.verstak/plugin-cache/<id>/ ← per-plugin cache
```
## Пример plugins.json
```json
{
"schemaVersion": 1,
"enabledPlugins": ["verstak.platform-test"],
"disabledPlugins": [],
"desiredPlugins": [
{
"id": "verstak.platform-test",
"version": "0.1.0",
"source": "official"
}
],
"updatedAt": "2026-06-17T..."
}
```
## Исправление отчёта Milestone 3
В отчёте Milestone 3 была арифметическая ошибка: написано "24/24 PASS", реально — **39 PASS** (16 plugin + 8 storage + 7 vault + 8 other). Исправлено в документации.
## Что НЕ сделано (будет в следующих milestone)
- Notes/files/editor/sync plugins
- Plugin marketplace/distribution
- Auto-install plugins
- Advanced window state management

View File

@ -1,122 +0,0 @@
# Milestone 4b — App Settings + Vault Plugin State (UI Completion)
## Цель
Довести Milestone 4 до пользовательски завершённого состояния:
- При первом запуске можно выбрать/создать vault
- После выбора путь сохраняется в app settings
- При следующем запуске vault открывается автоматически
- Plugin Manager умеет enable/disable plugins через vault plugin state
## Что сделано
### 1. First Run / Vault Selection UI
Новый компонент `VaultSelection.svelte`:
- Экран показывается если `currentVaultPath` пустой или vault не открывается
- Create new vault → `CreateVault(path)``SetCurrentVault(path)`
- Open existing vault → `SetCurrentVault(path)`
- Recent vaults из app settings
- Понятная ошибка если vault не открылся
- После успеха → событие `verstak:vault-opened` → переход в основной UI
### 2. App Startup Flow
Обновлён `App.svelte`:
- При монтировании: `GetAppSettings()` + `GetVaultStatus()`
- Если `currentVaultPath` пустой или vault не open → `needsVaultSelection = true`
- Показывает `VaultSelection` или основной UI
- Слушает `verstak:vault-opened` для перехода
- Добавлены отступы: `padding: 1.5rem` на `.content`
### 3. Plugin Manager Enable/Disable
Обновлён `PluginCard.svelte`:
- Кнопка "▶ Enable" если plugin disabled
- Кнопка "⏸ Disable" если plugin enabled
- Скрыты если vault не открыт (показывается hint)
- Скрыты если plugin failed/incompatible
Обновлён `PluginManager.svelte`:
- `enablePlugin(id)``EnablePlugin(id)``reload()`
- `disablePlugin(id)``DisablePlugin(id)``reload()`
- Загрузка `vaultPluginState` при vault open
- Вычисление `missingInstalled` из desired plugins
### 4. Missing Installed Plugins UI
Новый блок в `PluginManager.svelte`:
- Показывает desired plugins которых нет локально
- Карточка с красной рамкой и статусом "missing"
- Показывает source если известен
- Auto-install НЕ делается
### 5. Backend Changes
`internal/api/app.go`:
- `SetCurrentVault(path)` — открывает vault, сохраняет в app settings, загружает plugin state, регистрирует vault capability
- `RecordDesiredPlugin(id, version, source)` — записывает desired plugin
- `ReloadPlugins()` — записывает desired plugins при discovery (только если vault open)
`main.go`:
- Запись desired plugins при первичной загрузке (только если vault open)
`internal/core/appsettings/manager.go`:
- Исправлен `addRecent()` — убран дублирующий sort
### 6. Smoke Test
Обновлён `scripts/smoke-platform.sh`:
- Добавлен `-test-enable-disable` флаг
- Создаёт temp vault, открывает, загружает plugin state
- Disable → проверяет IsDisabled/IsEnabled
- Enable → проверяет IsEnabled/IsDisabled
- Проверяет `plugins.json` на диске
## Верификация
| Команда | Результат |
|---------|-----------|
| `go test ./...` | ✅ все тесты проходят |
| `./scripts/check.sh` | ✅ go vet + gofmt + go mod tidy |
| `./scripts/smoke-platform.sh` | ✅ discovery + enable/disable |
| `./scripts/build.sh` | ✅ wails build OK |
## Хранение данных
| Что | Где | Назначение |
|-----|-----|-----------|
| App settings | `~/.config/verstak/config.json` | Локальные настройки установки |
| Vault plugin state | `<vault>/.verstak/plugins.json` | Enabled/disabled/desired plugins |
| Plugin settings | `<vault>/.verstak/plugin-settings/<id>/settings.json` | Настройки конкретного плагина |
## Пример plugins.json
```json
{
"schemaVersion": 1,
"enabledPlugins": ["verstak.platform-test"],
"disabledPlugins": [],
"desiredPlugins": [
{
"id": "verstak.platform-test",
"version": "0.1.0",
"source": "official"
}
],
"updatedAt": "2026-06-17T04:00:00Z"
}
```
## Статус platform-test
- После enable: DEGRADED (optional sync missing)
- После disable: status = disabled, capabilities/contributions исчезают
- После enable обратно: DEGRADED снова
## Что НЕ сделано
- Notes/files/editor/sync
- Auto-install plugins
- Plugin marketplace/distribution
- Sync capability

View File

@ -1,165 +0,0 @@
# Milestone 5a — Frontend Plugin Host: Declarative UI Contributions + Plugin Settings
## Цель
Создать первый UI-host слой, чтобы shell отображал UI contributions плагинов и корректно убирал их при disable/reload. Ошибка plugin UI не должна ронять shell.
## Contribution Points
### Реализованы в этом milestone
| Contribution Point | Backend Registry | Frontend Host | Статус |
|---|---|---|---|
| `sidebarItems` | ✅ Registry.Register/Unregister/ListByPoint | ✅ Sidebar.svelte | Работает |
| `views` | ✅ Registry | ✅ ViewContainer.svelte (placeholder) | Работает (declarative placeholder) |
| `settingsPanels` | ✅ Registry | ✅ PluginManager.svelte | Работает |
| `commands` | ✅ Registry | ContributionRegistry (UI command palette not implemented) | Registry готов, UI planned |
### Планируемые (не реализованы)
- `fileActions`, `noteActions`, `contextMenuEntries`, `searchProviders`, `activityProviders`, `statusBarItems`
## Что сделано
### 1. Contribution Registry Lifecycle
**`internal/core/contribution/registry.go`:**
- Добавлен `ListByPoint(pointType)` — запрос contributions по типу
- `Register()` теперь idempotent: удаляет старые записи plugin перед добавлением
- Добавлены `ContributionPointType` константы для всех 10 типов
**`internal/api/app.go` — ReloadPlugins:**
- Перед регистрацией contributions вызывается `Unregister(pluginID)` → предотвращает дубли при повторном reload
- Disabled/failed plugins не регистрируют contributions
- Flattened ContributionSummary для фронтенда (FlatSidebarItem, FlatView, FlatSettingsPanel, FlatCommand)
### 2. Manifest Contributions Schema
`plugin.json` может объявлять (без изменений — схема существовала):
```json
{
"contributes": {
"sidebarItems": [{ "id": "...", "title": "...", "icon": "🧪", "view": "...", "position": 100 }],
"views": [{ "id": "...", "title": "...", "component": "..." }],
"settingsPanels": [{ "id": "...", "title": "...", "component": "..." }],
"commands": [{ "id": "...", "title": "...", "icon": "⚡", "handler": "..." }]
}
}
```
### 3. UI Shell Rendering
**Sidebar.svelte:**
- Строит plugin sidebar items из ContributionRegistry (поле `sidebarItems`)
- Сортировка по `position` (default 100)
- Фильтрация: скрыты items от disabled/failed/incompatible плагинов
- Клик → `verstak:open-view` событие
- Error boundary: перехват ошибок API, показ "⚠️ Plugin UI error"
**ViewContainer.svelte:**
- Declarative placeholder host для plugin views
- Показывает plugin name, view id, component id, статус "frontend bundle host not implemented yet"
- Error boundary: `{#key}` + catch rendering errors → "⚠️ Plugin UI failed" fallback
- Empty state: "Select a plugin view from the sidebar"
### 4. Plugin Settings
**PluginManager.svelte:**
- Загружает `settingsPanels` из ContributionRegistry
- PluginCard принимает `settingsPanels` prop
- "⚙️ Settings" кнопка показывается только если у plugin есть settingsPanel
- Клик → `verstak:open-settings` → открывает settings panel в modal
- Disable plugin → кнопка Settings исчезает
- Error boundary: `{#key}` + error state вокруг settings panel
### 5. Error Boundary
- ViewContainer: `{#key activeView}` + try/catch в reactive declarations → "⚠️ Plugin UI failed"
- PluginManager: `{#key}` вокруг settings modal + `settingsError` state
- Ошибки логируются в `console.error`
### 6. Platform-test Plugin
`verstak-official-plugins/plugins/platform-test/plugin.json` уже содержит все contribution points. Без изменений.
### 7. Enable/Disable Verification
| Сценарий | Результат |
|---|---|
| Plugin enabled → sidebar item visible | ✅ Sidebar items из ContributionRegistry |
| Plugin enabled → view opens | ✅ ViewContainer placeholder |
| Plugin enabled → settings button visible | ✅ Если есть settingsPanel |
| Plugin disabled → sidebar item disappears | ✅ `Unregister` → contributions удалены |
| Plugin disabled → view/settings unavailable | ✅ Не показываются |
| Plugin re-enabled → contributions return | ✅ При Reload — Register |
| ReloadPlugins no duplicates | ✅ Unregister перед Register + Register idempotent |
| Failed plugin → shell stable | ✅ Error boundary в ViewContainer + PluginManager |
### 8. Build Script
`scripts/build.sh` — добавлена функция `global_update()`:
- `git pull --ff-only` для всех 6 репозиториев
- Сборка official plugins (npm install + build для каждого plugin с frontend, go build для backend)
- Копирование собранных плагинов в `verstak-desktop/plugins/`
- Ошибки не фатальны — собираются и показываются в конце
## Изменённые файлы
### verstak-desktop
| Файл | Изменение |
|---|---|
| `internal/core/contribution/registry.go` | Добавлен `ListByPoint`, `ContributionPointType`, `Register` idempotent |
| `internal/core/contribution/registry_test.go` | **НОВЫЙ**: 5 тестов (register, unregister, ListByPoint, duplicate prevention, no-side-effects) |
| `internal/api/app.go` | Flat типы для фронтенда, `buildContributionSummary`, `ReloadPlugins` — Unregister перед Register |
| `frontend/src/lib/shell/Sidebar.svelte` | Sidebar items из ContributionRegistry, фильтрация, сортировка, error boundary |
| `frontend/src/lib/shell/ViewContainer.svelte` | **НОВЫЙ**: declarative placeholder host + error boundary |
| `frontend/src/lib/plugin-manager/PluginCard.svelte` | Settings button по settingsPanels prop, disabled state |
| `frontend/src/lib/plugin-manager/PluginManager.svelte` | Загрузка settingsPanels, settings modal, error boundary |
| `frontend/src/App.svelte` | Обработка `verstak:open-view`, `verstak:open-settings`, `verstak:close-settings` |
| `cmd/smoke-platform/main.go` | Добавлен `-test-contributions` флаг с тестом lifecycle |
| `scripts/smoke-platform.sh` | Добавлен вызов `-test-contributions` |
| `scripts/build.sh` | Добавлена `global_update()` — pull всех репозиториев, сборка official plugins |
| `docs/PLUGIN_RUNTIME.md` | Обновлён раздел Contribution Points + Reload |
### verstak-docs
| Файл | Изменение |
|---|---|
| `docs/MILESTONE_PLATFORM_RUNTIME_5a.md` | **НОВЫЙ**: этот документ |
## Результаты проверок
```
go test ./internal/... -count=1
→ all packages PASS (включая 5 новых contribution tests)
go vet ./...
→ clean
cd frontend && npm run build
→ ✓ built in 1.43s
bash scripts/smoke-platform.sh
→ smoke-platform passed (plugin + workspace + contributions lifecycle)
→ enable/disable test passed
→ workspace test passed
→ contributions lifecycle test passed
git status --short
→ clean (все 6 репозиториев)
git log HEAD --not --remotes
→ empty (все запушено)
```
## Non-goals (не реализовано)
- Frontend bundle loader (plugin JS bundle host). ViewContainer — declarative placeholder
- Official plugin extraction (notes/files/editor/activity)
- Backend sidecar runtime
- Secrets
- Remote plugin registry
- Sync
- Command palette UI
- Пользовательские функции в core

View File

@ -1,187 +0,0 @@
# Milestone 5b — Frontend Bundle Host / Plugin API Stub
## Цель
Первый настоящий frontend plugin host слой: shell умеет загружать frontend bundle плагина, давать ему ограниченный VerstakPluginAPI stub и рендерить view/settings panel через зарегистрированный component id.
## Что реализовано
### 1. Bundle Contract
Плагин регистрирует компоненты через глобальную функцию:
```javascript
window.VerstakPluginRegister('verstak.platform-test', {
components: {
'DiagnosticsPanel': {
mount: function(containerEl, props, api) {
// Рендерит UI в containerEl
// containerEl — div, созданный PluginBundleHost
// api — ограниченный VerstakPluginAPI
},
unmount: function(containerEl) {
// Очистка при смене view/unmount
containerEl.innerHTML = '';
}
},
'PlatformTestSettings': {
mount: function(containerEl, props, api) { /*...*/ },
unmount: function(containerEl) { /*...*/ }
}
}
});
```
**VerstakPluginAPI** — ограниченный API, передаваемый в mount():
| Метод | Статус | Описание |
|---|---|---|
| `api.pluginId` | ✅ Работает | ID плагина |
| `api.capabilities.has(id)` | ✅ Stub | Возвращает false (planned: реальный запрос к registry) |
| `api.events.publish(type, payload)` | ✅ Stub | Логирует в console (planned: event bus bridge) |
| `api.events.subscribe(type, handler)` | ✅ Stub | Логирует в console (planned: event bus bridge) |
| `api.settings.read(key)` | ✅ Stub | Возвращает null (planned: backend storage namespace) |
| `api.settings.write(key, value)` | ✅ Stub | Логирует в console (planned: backend storage) |
| `api.commands.execute(id, args)` | ✅ Stub | Логирует в console (planned: command execution) |
### 2. Безопасная резолюция asset path
**Backend методы:**
| Метод | Описание |
|---|---|
| `GetPluginFrontendInfo(pluginID)` | Возвращает frontend metadata (entry, style, rootPath, name, icon, version) |
| `GetPluginAssetContent(pluginID, assetPath)` | Читает файл из директории плагина с валидацией безопасности |
**Проверки безопасности:**
- Абсолютные пути (начинающиеся с `/` или `\`) — отклоняются
- Path traversal (`..`) — отклоняется
- Выход за пределы plugin root — отклоняется через `filepath.Abs` + `strings.HasPrefix`
### 3. FrontendPluginHost
**PluginBundleHost.svelte** — загружает и рендерит плагин бандлы:
1. Получает plugin frontend info через `GetPluginFrontendInfo()`
2. Если у плагина есть frontend entry — загружает JS контент через `GetPluginAssetContent()`
3. Выполняет bundle через `new Function(content)` (безопасно: нет доступа к внешней области видимости)
4. Ждёт вызов `VerstakPluginRegister` и находит компонент по componentId
5. Создаёт `VerstakPluginAPI` и вызывает `component.mount(container, props, api)`
6. При смене view — вызывает `component.unmount(container)` и очищает
**Error boundary:**
- Если bundle не загружается — fallback с pluginID, componentId, error text
- Если компонент не найден — показывает доступные components
- Если mount выбрасывает исключение — fallback без падения shell
- Все состояния: idle, loading, error, loaded
### 4. ViewContainer.svelte
- Проверяет наличие frontend bundle у плагина
- Если есть — рендерит PluginBundleHost
- Если нет — показывает "frontend bundle not available" placeholder
- Badge в заголовке: "frontend bundle" (зелёный) или "no frontend bundle" (красный)
### 5. PluginManager — Settings Panel
- Убран hardcoded platform-test settings form
- Settings panel рендерится через PluginBundleHost, если у плагина есть frontend entry
- Если нет — показывает "Settings panel frontend bundle not available"
### 6. platform-test plugin — Real Frontend Bundle
**`frontend/dist/index.js`** (14.6 KB):
- Регистрирует компоненты через `VerstakPluginRegister`
- Диагностическая панель:
- Plugin name, version, ID
- "✅ Frontend Bundle Loaded" badge
- Test results summary
- Capabilities status section
- API methods info
- Settings panel:
- Plugin name + ID
- Interactive counter (increment/decrement/reset)
- Demo settings list
- Темная тема (совпадает с shell)
**`frontend/style.css`** (4.9 KB):
- Shared dark-theme styles
- Используется обоими компонентами
### 7. Тесты
**Backend (11 новых тестов в `internal/api/app_test.go`):**
| Тест | Проверяет |
|---|---|
| GetPluginFrontendInfo (known) | Полные данные для плагина с frontend |
| GetPluginFrontendInfo (no frontend) | Статус "no-frontend" |
| GetPluginFrontendInfo (unknown) | Статус "not-found" |
| GetPluginAssetContent (existing) | Чтение существующего файла |
| GetPluginAssetContent (style) | Чтение style.css |
| GetPluginAssetContent (absolute path) | Отклонение `/` и `\` |
| GetPluginAssetContent (path traversal) | Отклонение `..` |
| GetPluginAssetContent (path escape) | Отклонение выхода за root |
| GetPluginAssetContent (not found) | Ошибка для неизвестного pluginID |
| GetPluginAssetContent (no frontend) | Ошибка если нет frontend |
| GetPluginAssetContent (missing file) | Ошибка если файл не существует |
**Smoke test (frontend bundle checks):**
- Manifest объявляет `frontend.entry = "frontend/dist/index.js"`
- Файл бандла существует на диске
- Бандл содержит `"VerstakPluginRegister"`
- Компоненты `DiagnosticsPanel` и `PlatformTestSettings` зарегистрированы
### 8. Security Constraints
| Сценарий | Результат |
|---|---|
| frontend entry `../etc/passwd` | Отклоняется (path traversal) |
| frontend entry `/etc/passwd` | Отклоняется (absolute path) |
| Плагин без frontend | Не ломает UI, показывает placeholder |
| Плагин с missing entry | Error fallback с понятным сообщением |
| Bundle execution error | Error fallback, shell не падает |
| Компонент не найден в bundle | Error fallback со списком доступных components |
## Изменённые файлы
### verstak-desktop
| Файл | Изменение |
|---|---|
| `internal/api/app.go` | + `GetPluginFrontendInfo()`, `GetPluginAssetContent()` с path validation |
| `internal/api/app_test.go` | **NEW**: 11 тестов |
| `frontend/src/lib/plugin-host/VerstakPluginAPI.js` | **NEW**: Bundle contract + API stub |
| `frontend/src/lib/plugin-host/PluginBundleHost.svelte` | **NEW**: Загрузка/рендер бандлов, error boundary |
| `frontend/src/lib/shell/ViewContainer.svelte` | Обновлён: PluginBundleHost вместо placeholder |
| `frontend/src/lib/plugin-manager/PluginManager.svelte` | Обновлён: Settings через PluginBundleHost |
| `cmd/smoke-platform/main.go` | Обновлён: frontend bundle checks |
| `docs/PLUGIN_RUNTIME.md` | Обновлён: bundle contract, security |
### verstak-official-plugins
| Файл | Изменение |
|---|---|
| `plugins/platform-test/frontend/src/index.js` | **NEW**: DiagnosticsPanel + SettingsPanel |
| `plugins/platform-test/frontend/style.css` | **NEW**: Dark theme styles |
| `plugins/platform-test/frontend/dist/index.js` | **REPLACED**: VerstakPluginRegister contract |
### verstak-docs
| `docs/MILESTONE_PLATFORM_RUNTIME_5b.md` | **NEW**: этот документ |
## Проверки
```
go test ./internal/... -count=1 → ✅ 56 PASS (all packages)
go vet ./... → ✅ clean
cd frontend && npm run build → ✅ built (72.98 KB gzip:21.64 KB)
bash scripts/smoke-platform.sh → ✅ 4 теста (plugin + enable/disable + workspace + contributions + frontend bundle)
bash scripts/build.sh → ✅ wails build
```
## Non-goals (не реализовано)
- Official notes/files/editor extraction
- Backend sidecar runtime
- Secrets
- Полноценный command palette
- Remote plugin registry
- Прямой доступ плагина к Wails backend methods (кроме VerstakPluginAPI)

View File

@ -1,496 +0,0 @@
# Verstak2 UI/UX Audit - 2026-06-30
## Scope
This audit compares current Verstak2 against the old Verstak repository at
`/home/mirivlad/git/verstak`. The old repository is used only as UI/UX,
feature, and workflow reference. Code and architecture are not copied.
No previous complete UI/UX comparison report was found in the Verstak2
repositories. Existing documents cover implementation plans and roadmap items,
but not an end-to-end scenario audit.
## Environment
- Date: 2026-06-30
- Verstak2 desktop repo: `/home/mirivlad/git/verstak2/verstak-desktop`
- Old Verstak repo: `/home/mirivlad/git/verstak`
- Tooling:
- Node.js v24.18.0 installed locally under `/tmp/verstak2-tools`
- Playwright Chromium used through local CLI wrapper
- Wails CLI v2.12.0 installed locally
- Debian 13 WebKitGTK package present as `libwebkit2gtk-4.1-dev`
- Wails note:
- `wails doctor` still reports missing `libwebkit2gtk-4.0-dev`.
- Wails v2.12.0 supports WebKitGTK 4.1 with Go build tag `webkit2_41`.
- `go test -tags webkit2_41 ./...` passes for current `verstak-desktop`.
## Artifacts
Screenshots are stored in:
`docs/ui-ux-audit-assets/2026-06-30/`
- `v1-start.png`
- `v1-today.png`
- `v1-inbox.png`
- `v1-activity.png`
- `v2-plugin-manager.png`
- `v2-workspace-files.png`
- `v2-command-palette.png`
- `v2-workspace-activity.png`
- `v2-workspace-browser-inbox.png`
## Verification Summary
### Repository Update
All Verstak2 subrepositories were updated. `verstak-desktop` initially could
not fast-forward because of local tracked changes in generated Wails typings
and `scripts/build.sh`. Those changes were saved in:
`stash@{0}: codex-pre-ui-audit-desktop-update`
After that, `verstak-desktop` fast-forwarded to `origin/main`.
### Automated Checks
Current `verstak-desktop`:
- `go test -tags webkit2_41 ./...`: pass
- `npm --prefix frontend run build`: pass
- `frontend npm run test:e2e`: 42 passed, 11 failed
- `scripts/check.sh` with `GOFLAGS=-tags=webkit2_41`: fails on gofmt only:
- `plugins/platform-test/backend/main.go`
- `build/bin/plugins/platform-test/backend/main.go`
### E2E Failures Worth Tracking
The failed E2E tests cluster around:
- Plugin Manager navigation selectors expecting `.sidebar .nav-item` while the
current sidebar uses different structure/classes.
- File/workbench editor height regressions:
- text editor height was about 266px where test expects over 300px;
- markdown preview height was about 217px where test expects over 300px.
- Platform Test status bar component failure:
- `openDiagnostics` component not found in bundle.
- Sync status bar failure:
- `SyncStatusBar` bundle content is empty.
Some selector failures may be stale test assumptions. The editor height and
status bar errors are visible workflow/UI issues.
### Post-Pull Status - 2026-06-30 Later Sync
After pulling `verstak-desktop` to `46f754c` (`feat: workspace routing,
GlobalSearch, and shell refinements`), several roadmap items from this audit
are now implemented and verified.
Current verification:
- `GOFLAGS=-tags=webkit2_41 go test ./...`: pass
- `npm --prefix frontend run test:e2e`: 71 passed
- Focused UX coverage now includes:
- app starts in the first workspace instead of Plugin Manager;
- workspace opens with a shell-level Today tab before plugin tools;
- workspace selection and main content stay in sync after Plugin Manager
round trips;
- workspace tool selection survives Workbench open/back/close flows;
- status bar plugin failures do not render large error panels;
- Plugin Manager remains reachable from the status/settings menu;
- Files uses readable dates and understandable labeled controls;
- Vault Selection is localized and has clear primary actions;
- global search stays visible after opening tool views;
- global search searches workspaces and file contents with RU/EN layout
fallback;
- plugin settings modal has enough space for complex panels;
- Browser Inbox desktop workflow renders an empty capture flow, stored
captures, detail metadata, and create/remove actions;
- Activity desktop workflow renders an empty event flow, stored events, and
worklog suggestions;
- Command Palette promotes shell workflow commands before diagnostics and can
open Today, Files, Activity, Browser Inbox, Plugin Manager, and start
markdown/text file creation;
- Command Palette can run Sync Now and open Sync settings.
The historical screenshots in `docs/ui-ux-audit-assets/2026-06-30/` remain
useful as "before" evidence, but the active roadmap below supersedes the
original P0 ordering.
## Scenario Findings
### 1. First Meaningful Screen
Old Verstak opens into a working shell:
- left navigation has clear system sections: Today, Inbox, Activity;
- workspace tree sits in the same navigation context;
- global search is immediately visible in the header;
- sync status and settings are discoverable but secondary.
Verstak2 opens into Plugin Manager:
- the first screen is technical/admin-oriented rather than user-task-oriented;
- plugin cards expose implementation details such as capabilities, roots,
permissions, API versions, and contribution counts;
- status bar plugin errors are visible immediately;
- workspace entry exists in the sidebar, but it is not the primary landing
experience.
Verdict: Verstak2 is feature-visible, but not user-workflow-first.
### 2. Navigation Model
Old Verstak has one coherent navigation model:
- System section for cross-workspace workflows;
- Workspace section for user cases/spaces;
- settings and sync anchored at the bottom;
- current page title and search are consistent.
Verstak2 separates:
- Tools;
- Workspaces;
- workspace tabs;
- Plugin Manager;
- status bar settings.
This is architecturally accurate for a platform, but it leaks platform concepts
too strongly into daily use. The user needs "work", "capture", "search",
"activity", and "settings"; they should not need to reason about plugin
contributions on the main path.
### 3. Workspace Files
Verstak2 workspace Files is one of the stronger current screens:
- workspace header exists;
- search slot exists;
- Files, Activity, Browser Inbox tabs are correctly scoped to workspace;
- file rows, folder rows, filtering, sorting, and action buttons are present.
Problems:
- Files toolbar buttons render visually as near-identical small squares in the
screenshot. Accessibility names exist, but visible affordance is poor.
- Editor/preview area appears too short in E2E, causing regressions for file
opening workflows.
- Status bar error area consumes a large part of the lower viewport and reduces
usable file space.
### 4. Activity
Old Verstak gives Activity a clear system-level place and empty state.
Today also exposes related workflow tabs: feed, suggestions, in-progress,
captures, browser.
Original Verstak2 desktop mock rendered only:
- heading `Activity`;
- text `Global activity feed`.
Current desktop shell/mock now mirrors the official Activity plugin's core
visible workflow: scoped empty state, event count, stored event rows, clear
action, and worklog suggestions. Remaining work is to expose real activity
ingestion and Journal import from the visible app flow, not just prove the
surface contract in mock E2E.
### 5. Browser Inbox / Capture Flow
Old Verstak "Неразобранное" has:
- a clear name for unprocessed captures;
- explanatory text;
- an immediate visible action: paste from clipboard.
Original Verstak2 desktop mock rendered only:
- heading `Browser Inbox`;
- text `Global browser inbox`.
Current desktop shell/mock now mirrors the official Browser Inbox plugin's core
visible workflow: scoped empty state, capture count, stored capture rows,
detail metadata, and visible conversion/remove actions. Remaining work is to
make the real capture ingestion path and extension pairing visible from the
app.
### 6. Command Palette
Verstak2 command palette opens and is keyboard-accessible. Current visible
commands are only Platform Test commands. It is useful infrastructure, but not
yet a meaningful user launcher.
Expected near-term commands:
- open/create workspace;
- search workspace;
- create note;
- create file;
- open Browser Inbox;
- open Activity;
- sync now / sync settings;
- plugin/settings only after user tasks.
### 7. Status Bar
Status bar is currently the largest immediate UX blocker.
Observed:
- permanent lower panel shows plugin errors;
- error cards overlap/consume workspace area;
- messages expose internal plugin/component/bundle details;
- normal user status is visually dominated by red plugin failure content.
The old version keeps sync/settings status quiet and secondary. Verstak2 should
follow that principle: status should be compact, actionable, and non-blocking.
Developer diagnostics belong behind an explicit diagnostics surface.
## Priority Plan
### DONE - Remove User-Facing Internal Errors From Main Workspace
Goal: no normal user screen should be dominated by plugin bundle errors.
Status: implemented in `verstak-desktop` and verified by
`frontend/e2e/ux-p0.spec.js` plus `frontend/e2e/status-bar.spec.js`.
Actions:
- Fix Platform Test status contribution:
- manifest/component mismatch around `openDiagnostics` vs available bundle
components.
- Fix Sync status contribution:
- empty frontend bundle content for `SyncStatusBar`.
- Change status bar error rendering:
- compact "Plugin issue" badge;
- details only on click;
- cap height so it cannot cover workspace content.
Verification:
- `frontend/e2e/status-bar.spec.js` passes.
- First workspace screenshot has no large red error cards.
### DONE - Make Workspace The Default User Landing
Goal: after vault opens, user lands in a productive workspace or Today-style
overview, not Plugin Manager.
Status: implemented in `verstak-desktop`; current mock flow starts in the first
workspace and keeps Plugin Manager reachable through settings.
Actions:
- If a vault has workspaces, open the last active workspace.
- If no workspace exists, show a first-run workspace creation screen.
- Move Plugin Manager out of the primary default route:
- keep accessible through Settings;
- optionally expose via command palette.
Verification:
- Initial app screen shows workspace or useful overview.
- Plugin Manager remains reachable but is not the first user-facing surface.
### PARTIAL - Restore Old "Today / Inbox / Activity" Workflow Shape
Goal: preserve the old product's user workflow while keeping Verstak2 plugin
architecture.
Status: first shell-level Today surface is implemented in `verstak-desktop`.
Workspace now opens with `Today` before plugin tools, showing captures, recent
activity, worklog suggestions, and quick actions. The Today "Open Inbox" action
switches into the workspace Browser Inbox tool, and Files remains stable after
Workbench open/back/close flows.
Remaining gap: this is still a thin aggregation layer. Activity, Browser Inbox,
and Journal exist as plugin-backed surfaces, but the old product's richer
"what should I do now?" flow needs real capture/activity/journal data density
and stronger empty-state actions. Current architecture has the ingredients:
- `verstak.activity` stores scoped activity and exposes worklog suggestion
command `verstak.activity.suggestWorklog`;
- `verstak.browser-inbox` stores scoped captures and can convert captures into
notes, links, and files;
- `verstak.journal` stores worklog entries and can import Activity suggestions;
- `GlobalSearch` indexes workspaces, tools, files, Browser Inbox, Activity, and
Journal plugin settings.
Actions:
- Expand the user-level `Today` surface with richer real data.
- Surface and prioritize:
- recent activity;
- unprocessed captures;
- in-progress work;
- worklog suggestions;
- quick create actions.
- Keep implementation plugin-driven, but present as one coherent workflow.
- Add first-class commands for opening Today, Inbox, Activity, and creating
work items.
Verification:
- User can answer "what should I do now?" from the first screen.
- Browser captures and activity are visible without opening Plugin Manager.
- `frontend/e2e/ux-today.spec.js` covers Today-first startup and the Today to
Browser Inbox quick-action path.
### PARTIAL - Make Activity A Real Visible Workflow
Goal: Activity should look useful even with no events and should connect events
to worklog reconstruction.
Status: partially implemented. The real Activity plugin has scoped storage,
event subscriptions, worklog suggestion generation, and a command contribution
for `verstak.activity.suggestWorklog`. The desktop E2E/mock surface now mirrors
the core visible workflow: scoped empty state, event count, stored event rows,
clear action, and worklog suggestions.
Remaining UX work is mostly around real activity ingestion from app actions,
Journal import/review, and making suggestions actionable from Today/Activity
rather than merely visible.
Actions:
- Keep empty-state copy explaining that file changes, browser captures, and
conversions will appear here.
- Add visible actions for:
- import worklog suggestion to Journal;
- filter by source/type;
- open related file/capture from an activity row.
- Connect Today worklog suggestions to Activity/Journal actions.
Verification:
- Empty Activity explains what will appear.
- Stored events render with source/type/summary.
- Worklog suggestions are visible from activity events.
- `frontend/e2e/activity.spec.js` covers the desktop shell/mock contract.
### PARTIAL - Make Browser Inbox A Real Visible Workflow
Goal: Browser Inbox should look usable even when empty.
Status: partially implemented. The real Browser Inbox plugin has storage,
event subscriptions, scoped capture lists, domain binding, and Create Note /
Create Link / Create File actions for selected captures. The desktop E2E/mock
surface now mirrors the core visible workflow: scoped empty state, capture
count, stored capture list, detail metadata, and Create Note / Create Link /
Create File / Remove actions. Today also opens this workspace tool directly.
Remaining UX work is mostly around actual primary capture actions, extension
pairing/settings visibility, and making the real capture ingestion path easy to
exercise from the app.
Actions:
- Keep empty-state copy explaining capture flow.
- Add primary actions:
- paste from clipboard;
- open browser extension pairing/settings;
- create note/link/file from selected capture when data exists.
- Show domain/workspace binding state if configured.
Verification:
- Empty Browser Inbox explains the capture flow.
- Stored captures render with visible metadata and conversion actions.
- Captures can be converted from the visible UI in the real plugin.
- `frontend/e2e/browser-inbox.spec.js` covers the desktop shell/mock contract.
### DONE - Fix Files Toolbar Visual Affordance
Goal: toolbar buttons must be visually recognizable, not identical dark boxes.
Status: implemented and verified by `frontend/e2e/ux-p0.spec.js` and the full
Files E2E suite. Files now uses readable dates and understandable action
controls; editor/preview height regressions are covered by passing E2E.
Actions:
- Ensure Lucide icons render with sufficient contrast and size.
- Add tooltips for icon-only buttons.
- Disable unavailable actions visibly instead of showing identical active
squares.
- Keep layout dense, but make grouping clear: navigation, create, selected item
actions, clipboard, filter/sort.
Verification:
- Screenshot shows recognizable icons.
- Buttons have accessible names and hover tooltips.
- File open editor/preview height returns above E2E threshold.
### PARTIAL - Make Command Palette User-Oriented
Goal: command palette should be a power-user entry point for real tasks.
Status: partial. Command Palette works, executes active plugin commands, and
now promotes shell workflow commands above diagnostics: Open Today, Open Files,
Open Activity, Open Browser Inbox, Open Plugin Manager, Create Markdown File,
Create Text File, Sync Now, and Open Sync Settings. Remaining command coverage
gaps are capture actions, richer note templates, Journal import/review, and
hiding test diagnostics from production-like mode.
Actions:
- Register remaining user commands from Browser Inbox, Activity, Search, and
Journal.
- Keep user commands ranked above diagnostics/test commands.
- Hide test/plugin diagnostics from production-like mode.
Verification:
- `Ctrl+K` shows open Today/Files/Activity/Inbox commands before diagnostics.
- `Ctrl+K` can start markdown/text file creation through the visible Files
workflow.
- `Ctrl+K` can run Sync Now and open Sync settings.
- `frontend/e2e/command-palette.spec.js` covers shell command ranking and the
Open Activity, create-file, and sync command paths.
### DONE - Reduce Plugin Manager Density
Goal: Plugin Manager should remain useful but stop looking like the product's
main screen.
Status: implemented enough for the current roadmap. Plugin cards now show a
short default surface and hide API version, root, capabilities, optional
requires, and permissions under `Technical details`.
Actions:
- Split default view into simple plugin list:
- name;
- enabled/disabled;
- short description;
- settings;
- disable/enable.
- Put capabilities, permissions, roots, and contribution details behind
expandable "Developer details".
Verification:
- Plugin Manager fits more plugins per viewport.
- Non-developer user can safely enable/disable/configure without reading
internal contracts.
## Next Review Steps
1. Design and implement the next Today follow-up: make captures, Activity
worklog suggestions, and Journal import/review actionable from the first
workspace screen.
2. Re-run:
- `GOFLAGS=-tags=webkit2_41 ./scripts/check.sh`
- `npm --prefix frontend run test:e2e`
3. Re-capture the same screenshots as "after" evidence.
4. Run real Wails GUI smoke with:
- `wails dev -tags webkit2_41`
5. Do a second audit focused on:
- first-run/open vault;
- real filesystem vault;
- Browser extension capture;
- native window/WebKitGTK rendering differences.

File diff suppressed because it is too large Load Diff

View File

@ -1,228 +0,0 @@
# Sync Server & Plugin Design
## [S1] Problem
Verstak2 needs synchronization between devices. The old Verstak (`~/git/verstak`) has a working sync server and client, but Verstak2 is a platform architecture with plugins. Sync must be implemented as:
1. A separate sync server repository
2. A sync plugin that provides settings UI via the plugin manager
## [S2] Sync Server
### Location
Separate repository: `verstak-sync-server`
### Source
Based on `~/git/verstak/cmd/verstak-server/` with minimal changes.
### Structure
```
verstak-sync-server/
cmd/server/main.go
internal/
server/
server.go
routes.go
handlers_api.go
handlers_auth.go
handlers_admin.go
middleware.go
config.go
schema.go
tokens.go
smtp.go
go.mod
go.sum
README.md
```
### API Endpoints
- `POST /api/v1/sync/push` — push operations
- `POST /api/v1/sync/pull` — pull operations
- `POST /api/v1/blobs/` — upload blob (multipart)
- `GET /api/v1/blobs/:sha256` — download blob
- `POST /api/client/pair` — device pairing with login/password
- `POST /api/auth/test` — test credentials
- `GET /api/client/me` — device info
- `POST /api/client/revoke-current` — revoke current device
- `POST /api/client/revoke-device` — revoke specific device (web)
- `GET /api/v1/health` — health check
- Admin endpoints: `/admin/dashboard`, `/admin/users`, `/admin/api/stats`
### Config
YAML file at `data/config.yml`:
```yaml
port: 47732
admin:
- username: admin
password_hash: "$2a$10$..."
```
### Database
SQLite at `data/server.db` with tables:
- `server_users` — user accounts
- `server_devices` — paired devices
- `server_ops` — sync operations
- `server_tombstones` — deleted entities
- `server_idempotency_keys` — idempotent push
- `server_user_devices` — user-device mapping
- `server_audit_log` — audit trail
## [S3] Sync Plugin
### Location
`verstak-official-plugins/plugins/sync/`
### Manifest
```json
{
"schemaVersion": 1,
"id": "verstak.sync",
"name": "Sync",
"version": "0.1.0",
"apiVersion": "0.1.0",
"description": "Vault synchronization across devices via Verstak Sync Server.",
"source": "official",
"icon": "sync",
"provides": ["verstak/sync/v1", "verstak/sync.status/v1"],
"requires": ["verstak/core/files/v1"],
"permissions": [
"files.read",
"files.write",
"network.remote",
"settings.read",
"settings.write",
"ui.register"
],
"frontend": {
"entry": "frontend/dist/index.js"
},
"contributes": {
"settingsPanels": [{
"id": "verstak.sync.settings",
"title": "Sync",
"component": "SyncSettings"
}],
"statusBarItems": [{
"id": "verstak.sync.status",
"label": "Sync",
"position": "right"
}]
}
}
```
### Capabilities Provided
- `verstak/sync/v1` — sync provider capability
- `verstak/sync.status/v1` — sync status provider
### Settings (Extended)
The plugin stores settings via `settings.read/write` API:
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `serverUrl` | string | "" | Sync server URL |
| `username` | string | "" | User login |
| `password` | string | "" | User password (stored temporarily, not persisted) |
| `syncInterval` | number | 0 | Auto-sync interval in minutes (0 = disabled) |
| `autoSync` | boolean | false | Enable auto-sync |
| `deviceName` | string | hostname | Device identifier |
| `deviceId` | string | "" | Assigned device ID |
| `deviceToken` | string | "" | Device auth token |
| `lastStatus` | string | "disabled" | Connection status |
| `lastSyncAt` | string | "" | Last successful sync timestamp |
| `lastError` | string | "" | Last error message |
### Settings UI Component
`SyncSettings.svelte` — registered as settings panel via contributes.
**Two states:**
1. **Not configured** — setup form:
- Server URL input
- Username input
- Password input
- "Test Connection" button
- "Connect" button
2. **Configured** — status view:
- Status indicator (connected/disconnected/error/revoked)
- Server URL display
- Device Name display
- Device ID display
- Last Sync Time display
- Last Error display (if any)
- "Sync Now" button
- "Disconnect" button
- "Reset Key" button
- Sync Interval input + Save button
- Auto-sync toggle
### Backend Integration
The plugin uses VerstakPluginAPI to:
- Read/write settings via `settings.read/write`
- Call backend methods for sync operations
Backend methods needed in `verstak-desktop`:
- `SyncStatus()` — get current sync status
- `SyncConfigure(serverURL, username, password)` — pair device
- `SyncDisconnect()` — disconnect and revoke
- `SyncTestConnection(serverURL, username, password)` — test credentials
- `SyncSetInterval(minutes)` — set auto-sync interval
- `SyncNow()` — trigger immediate sync
- `ResetSyncKey()` — clear device token
### Status Bar Item
Shows sync status in the bottom-right status bar:
- Icon: sync icon
- Label: status text (Connected/Disconnected/Error)
- Click: opens plugin settings
## [S4] Data Flow
```
┌─────────────────────────────────────────────────────────┐
│ Verstak Desktop │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Plugin Manager│───▶│ Sync Plugin │───▶│ Settings │ │
│ │ (Settings) │ │ (UI Panel) │ │ API │ │
│ └──────────────┘ └──────┬───────┘ └───────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────┐│
│ │ VerstakPluginAPI ││
│ │ settings.read/write → files.read/write ││
│ └──────────────────────────┬──────────────────────────┘│
│ │ │
│ ┌──────────────────────────▼──────────────────────────┐│
│ │ Core Platform (Go Backend) ││
│ │ SyncService: RecordOp, GetUnpushedOps, MarkPushed ││
│ │ SyncClient: Push, Pull, UploadBlob, DownloadBlob ││
│ └──────────────────────────┬──────────────────────────┘│
└─────────────────────────────┼───────────────────────────┘
│ HTTP
┌─────────────────────────────────────────────────────────┐
│ Verstak Sync Server │
│ SQLite: users, devices, ops, tombstones │
│ Blobs: SHA-256 content-addressed storage │
│ API: push/pull, pair, auth, admin │
└─────────────────────────────────────────────────────────┘
```
## [S5] Implementation Order
1. **Sync Server** — copy and adapt from `~/git/verstak/cmd/verstak-server/`
2. **Backend API** — add sync methods to `verstak-desktop/internal/api/app.go`
3. **Sync Plugin** — create plugin structure with manifest and frontend
4. **Settings UI** — implement `SyncSettings.svelte` component
5. **Status Bar** — add sync status indicator
6. **Testing** — E2E tests for settings UI, integration tests for sync
## [S6] Testing Strategy
- **Unit tests**: Sync client (push/pull), settings read/write
- **E2E tests**: Settings UI (configure, disconnect, sync now)
- **Integration tests**: Full sync cycle (two devices, push/pull)
- **Manual verification**: Real sync server with two vault instances

View File

@ -1,196 +0,0 @@
# Activity Worklog Suggestions Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a minimal Activity reconstruction layer that renders and exposes worklog suggestions from recorded activity events.
**Architecture:** Keep reconstruction inside `verstak.activity`. Generate suggestions from normalized stored events, render them in the Activity view, and expose them through a command-backed runtime contract for the future Journal plugin.
**Tech Stack:** Official plugin browserless JavaScript bundle, Node smoke tests, JSON plugin manifest, Markdown docs.
## Global Constraints
- Do not move activity or journal logic into desktop core.
- Do not build the Journal plugin in this slice.
- Suggestions are derived from existing Activity events and are informational.
- Use TDD: update the activity smoke test first, run it red, then implement.
- Commit and push each affected repository after meaningful changes.
---
### Task 1: Document The Slice
**Files:**
- Create: `/home/mirivlad/git/verstak2/verstak-docs/docs/superpowers/specs/2026-06-29-activity-worklog-suggestions-design.md`
- Create: `/home/mirivlad/git/verstak2/verstak-docs/docs/superpowers/plans/2026-06-29-activity-worklog-suggestions.md`
**Interfaces:**
- Produces: written contract for `verstak.activity.suggestWorklog`.
- [ ] **Step 1: Write spec and plan**
Write the design and this implementation plan.
- [ ] **Step 2: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
```
Expected: exits 0.
- [ ] **Step 3: Commit and push docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git add docs/superpowers/specs/2026-06-29-activity-worklog-suggestions-design.md docs/superpowers/plans/2026-06-29-activity-worklog-suggestions.md
git commit -m "docs: plan activity worklog suggestions"
git push
```
Expected: docs `main` is clean and pushed.
### Task 2: Activity Suggestions Runtime
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/plugin.json`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-activity-plugin.js`
**Interfaces:**
- Produces command: `verstak.activity.suggestWorklog(args?: { workspaceRootPath?: string }): Promise<{ suggestions: WorklogSuggestion[] }>`
- Produces DOM: `data-activity-section="worklog-suggestions"` and `data-worklog-suggestion="<suggestionId>"`
- [ ] **Step 1: Write the failing smoke assertions**
Extend `scripts/smoke-activity-plugin.js` to read the Activity manifest, mock
`api.commands.register`, and assert that:
- `commands.register` permission exists;
- command contribution `verstak.activity.suggestWorklog` exists;
- the command is registered after mount;
- Project events render a suggestion;
- executing the command returns a suggestion with `minutes`, `summary`, and
source `eventIds`;
- global Activity renders separate Project and ClientA suggestions;
- clear removes the rendered suggestion.
- [ ] **Step 2: Run RED**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-activity-plugin.js
```
Expected: fails because `verstak.activity.suggestWorklog` is not registered.
- [ ] **Step 3: Update manifest**
Add `commands.register` to permissions and add:
```json
{
"id": "verstak.activity.suggestWorklog",
"title": "Suggest Worklog From Activity",
"handler": "verstak.activity.suggestWorklog"
}
```
under `contributes.commands`.
- [ ] **Step 4: Implement suggestions**
In `plugins/activity/frontend/src/index.js`:
- define `WORKLOG_COMMAND_ID = 'verstak.activity.suggestWorklog'`;
- add `suggestions` state;
- derive suggestions from normalized events grouped by workspace/day;
- render a suggestions band above the activity list;
- register the command with `api.commands.register`;
- recompute suggestions after load, event refresh, and clear.
- [ ] **Step 5: Run GREEN**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-activity-plugin.js
./scripts/check.sh
```
Expected: both commands exit 0.
- [ ] **Step 6: Commit and push official plugin**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
git add plugins/activity/plugin.json plugins/activity/frontend/src/index.js scripts/smoke-activity-plugin.js
git commit -m "feat: suggest worklogs from activity"
git push
```
Expected: official plugins `main` is clean and pushed.
### Task 3: Mark Roadmap Progress
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified official plugin commit.
- Produces docs matching implemented behavior.
- [ ] **Step 1: Update Activity status**
Replace the sentence saying reconstruction/worklog suggestions are future work
with a sentence describing the implemented suggestion band and command.
- [ ] **Step 2: Update roadmap**
Change:
```md
- implement activity reconstruction and worklog suggestions;
```
to:
```md
- [x] implement activity reconstruction and worklog suggestions;
```
- [ ] **Step 3: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "Reconstruction and worklog suggestions are still future work|implement activity reconstruction and worklog suggestions" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
```
Expected: `git diff --check` exits 0. `rg` shows only the checked roadmap line.
- [ ] **Step 4: Commit and push docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git add 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
git commit -m "docs: mark activity worklog suggestions complete"
git push
```
Expected: docs `main` is clean and pushed.

View File

@ -1,217 +0,0 @@
# Binary File Write And Browser Attachments Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add bounded binary file writes to the public Files API and use them for Browser Inbox binary attachment conversion.
**Architecture:** Desktop owns the safe byte-write primitive and sync payload compatibility. SDK and frontend bridge expose the method as public plugin API. Browser extension sends base64 for selected files, receiver republishes it, and Browser Inbox converts through `api.files.writeBytes`.
**Tech Stack:** Go Files service and Wails bridge, plain JavaScript plugin host and WebExtension code, TypeScript SDK types/tests, official plugin smoke harness, Markdown docs.
## Global Constraints
- Do not add chunked streaming in this slice.
- Do not allow plugins to write outside vault-relative path policy.
- Keep the write limit at 8 MB to match existing `readBytes`.
- Keep sync backward compatible with existing text `content` payloads.
- Use TDD for each behavior change.
---
### Task 1: Desktop Files API `writeBytes`
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/core/files/service.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/core/files/service_test.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/api/app.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/api/app_test.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/frontend/src/lib/plugin-host/VerstakPluginAPI.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/frontend/tests/plugin-api-files-test.mjs`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/frontend/wailsjs/go/api/App.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/frontend/wailsjs/go/api/App.d.ts`
**Interfaces:**
- Produces `api.files.writeBytes(relativePath, dataBase64, options)`.
- Produces backend `WriteVaultFileBytes(pluginID, relativePath, dataBase64, options)`.
- [ ] **Step 1: Write RED service/API/bridge tests**
Add tests for successful byte write, invalid base64 rejection, oversized payload
rejection, permission enforcement, sync `dataBase64` payload, remote binary
apply, and frontend bridge exposure.
- [ ] **Step 2: Run RED**
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop-current
go test ./internal/core/files -run TestWriteVaultFileBytesAtomicAndConflictBehavior -count=1
go test ./internal/api -run 'TestFilesBridgeReadWriteListMoveTrash|TestFilesBridgePermissions|TestApplyRemoteFileOps|TestFileBridgeRecordsSyncOps' -count=1
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node frontend/tests/plugin-api-files-test.mjs
```
Expected: failures because `writeBytes` does not exist.
- [ ] **Step 3: Implement byte write**
Add atomic bounded base64 decode/write in the Files service, Wails bridge method,
plugin host method, generated Wails stubs, sync payload `DataBase64`, and remote
apply support.
- [ ] **Step 4: Run GREEN**
```bash
go test ./internal/core/files
go test ./internal/api
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node frontend/tests/plugin-api-files-test.mjs
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/test.sh
```
Expected: all commands exit 0.
### Task 2: SDK `writeBytes` Contract
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.ts`
- Modify: `/home/mirivlad/git/verstak2/verstak-sdk/src/test-utils.ts`
- Modify: `/home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.test.ts`
**Interfaces:**
- Produces SDK `files.writeBytes(relativePath, dataBase64, options?)`.
- [ ] **Step 1: Write RED SDK test**
Assert `createMockPluginAPI().files.writeBytes` writes base64 content that can
be read back through `readBytes`.
- [ ] **Step 2: Run RED**
```bash
cd /home/mirivlad/git/verstak2/verstak-sdk
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test
```
Expected: failure because `writeBytes` is missing.
- [ ] **Step 3: Implement SDK types and mock**
Add the method to `VerstakPluginAPI` and mock implementation.
- [ ] **Step 4: Run GREEN**
```bash
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test
```
Expected: SDK tests pass.
### Task 3: Browser Extension Binary Capture
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/protocol.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/background.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-protocol.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/README.md`
**Interfaces:**
- Produces file captures with `file.dataBase64` for selected files up to 8 MB.
- [ ] **Step 1: Write RED protocol test**
Add a binary file capture assertion and validate that missing both `file.text`
and `file.dataBase64` is rejected.
- [ ] **Step 2: Run RED**
```bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test
```
Expected: failure because `dataBase64` is not preserved.
- [ ] **Step 3: Implement base64 file capture**
Read selected files as `ArrayBuffer`, base64 encode them, keep text only when
`file.text()` succeeds for text-like files, and enforce the 8 MB limit.
- [ ] **Step 4: Run GREEN**
```bash
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm run build
```
Expected: extension tests and build pass.
### Task 4: Receiver And Browser Inbox Binary Conversion
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/core/browserreceiver/receiver.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/core/browserreceiver/receiver_test.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js`
**Interfaces:**
- Consumes `file.dataBase64`.
- Produces Browser Inbox conversion through `api.files.writeBytes`.
- [ ] **Step 1: Write RED tests**
Receiver test asserts `fileDataBase64`; Browser Inbox smoke asserts `Create File`
uses `writeBytes` for binary captures.
- [ ] **Step 2: Run RED**
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop-current
go test ./internal/core/browserreceiver -run TestReceiverAcceptsFileCaptureAndPublishesEvent -count=1
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
```
Expected: failures for missing `fileDataBase64` and missing `writeBytes` path.
- [ ] **Step 3: Implement receiver and plugin conversion**
Add `DataBase64` to receiver file payload, preserve it in Browser Inbox storage,
and call `api.files.writeBytes` when present.
- [ ] **Step 4: Run GREEN**
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop-current
go test ./internal/core/browserreceiver
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
PATH=/tmp/verstak2-tools/venv/bin:/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/check.sh
```
Expected: receiver and official plugin checks pass.
### Task 5: Documentation And Final Verification
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/docs/PLUGIN_RUNTIME.md`
**Interfaces:**
- Documents public `files.writeBytes` and binary Browser Inbox completion.
- [ ] **Step 1: Update docs**
Document `writeBytes`, base64/8 MB limits, sync compatibility, and mark binary
attachment conversion complete.
- [ ] **Step 2: Run full verification**
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop-current && PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/test.sh
cd /home/mirivlad/git/verstak2/verstak-sdk && PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test
cd /home/mirivlad/git/verstak2/verstak-browser-extension && PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test && PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm run build
cd /home/mirivlad/git/verstak2/verstak-official-plugins && PATH=/tmp/verstak2-tools/venv/bin:/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/check.sh
cd /home/mirivlad/git/verstak2/verstak-docs && git diff --check
```
Expected: all commands exit 0.

View File

@ -1,92 +0,0 @@
# Browser Inbox Conversion Activity Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Record Browser Inbox note conversions in Activity through the public `browser.capture.converted` event.
**Architecture:** Keep plugins decoupled. Browser Inbox already publishes `browser.capture.converted`; Activity subscribes to that event through its normal frontend event list and manifest `activityProviders` contribution.
**Tech Stack:** Plain JavaScript official plugins, Node smoke test harness, plugin manifest, Markdown docs.
## Global Constraints
- Do not add a direct Browser Inbox to Activity API.
- Do not move conversion recording into desktop core.
- Use only public event subscription behavior.
- Use TDD: write the failing Activity smoke test first, run it red, then implement.
---
### Task 1: Activity Conversion Event Recording
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-activity-plugin.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/plugin.json`
**Interfaces:**
- Consumes: `browser.capture.converted` event payload from Browser Inbox.
- Produces: stored Activity entry using the existing activity event model.
- [ ] **Step 1: Write the failing smoke test**
In `scripts/smoke-activity-plugin.js`, add `browser.capture.converted` to the
required subscription checks and dispatch a workspace-scoped conversion event.
Assert it is stored, rendered, and included in worklog suggestion event ids.
- [ ] **Step 2: Run RED**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-activity-plugin.js
```
Expected: fails because Activity does not subscribe to
`browser.capture.converted`.
- [ ] **Step 3: Implement event subscription**
Add `browser.capture.converted` to `ACTIVITY_EVENTS` in
`plugins/activity/frontend/src/index.js` and to
`contributes.activityProviders[0].events` in `plugins/activity/plugin.json`.
- [ ] **Step 4: Run GREEN**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-activity-plugin.js
PATH=/tmp/verstak2-tools/venv/bin:/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/check.sh
```
Expected: both commands exit 0.
### Task 2: Roadmap Documentation
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified Activity recording behavior.
- Produces docs that mark Activity integration for capture conversion complete.
- [ ] **Step 1: Update docs**
Describe that `browser.capture.converted` is recorded by Activity and that
link/file-specific conversions remain.
- [ ] **Step 2: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "browser.capture.converted|link/file" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
```
Expected: `git diff --check` exits 0 and `rg` shows updated status lines.

View File

@ -1,100 +0,0 @@
# Browser Inbox Create Link Conversion Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a Browser Inbox conversion workflow that creates an ordinary `.url` link file from a captured URL.
**Architecture:** Keep conversion behavior in `verstak.browser-inbox`. The plugin writes a human-readable `.url` file through `api.files.writeText`, publishes `browser.capture.converted`, and leaves Activity/other consumers decoupled through the event bus.
**Tech Stack:** Plain JavaScript official plugin bundle, Node smoke test harness, Markdown docs.
## Global Constraints
- Do not add a core link entity model in this slice.
- Do not call private APIs from Browser Inbox.
- Use only public plugin APIs: `api.files.writeText` and `api.events.publish`.
- Do not silently rename on link path conflict; rely on `overwrite: false`.
- Use TDD: write the failing smoke test first, run it red, then implement.
---
### Task 1: Browser Inbox Create Link Conversion
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js`
**Interfaces:**
- Consumes:
- `api.files.writeText(relativePath, content, { createIfMissing: true, overwrite: false })`
- `api.events.publish('browser.capture.converted', payload)`
- Produces:
- `Create Link` action for workspace-scoped URL captures.
- `browser.capture.converted` event with `conversionType: "link"`.
- [ ] **Step 1: Write the failing smoke test**
Add a scenario to `scripts/smoke-browser-inbox-plugin.js` that clicks
`data-browser-inbox-action="create-link"` and asserts the written path,
`.url` content, write options, queue removal, and published event.
- [ ] **Step 2: Run RED**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
```
Expected: fails because `Create Link` is not rendered.
- [ ] **Step 3: Implement conversion**
Add helpers in `plugins/browser-inbox/frontend/src/index.js`:
- `safeLinkFilename(title)`;
- `captureToUrlShortcut(capture)`;
- `createLinkFromCapture(capture)`.
Render a `Create Link` button for captures with `workspaceRootPath` and `url`.
On success, remove the capture and publish `browser.capture.converted`.
- [ ] **Step 4: Run GREEN**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
PATH=/tmp/verstak2-tools/venv/bin:/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/check.sh
```
Expected: both commands exit 0.
### Task 2: Roadmap Documentation
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified create-link conversion behavior.
- Produces docs that mark link file conversion done while leaving file attachment capture for later.
- [ ] **Step 1: Update docs**
Describe Browser Inbox create-link conversion through the public Files API and
mark link conversion as complete.
- [ ] **Step 2: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "Create Link|link conversion|file attachment|browser.capture.converted" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
```
Expected: `git diff --check` exits 0 and `rg` shows updated status lines.

View File

@ -1,108 +0,0 @@
# Browser Inbox Create Note Conversion Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a first Browser Inbox conversion workflow that creates an ordinary Markdown note from a capture.
**Architecture:** Keep conversion behavior in `verstak.browser-inbox`. The plugin uses only public `api.files.writeText` and `api.events.publish`, removes the capture after a successful conversion, and leaves conversion consumers decoupled through the event bus.
**Tech Stack:** Plain JavaScript official plugin bundle, Node smoke test harness, plugin manifest permissions, Markdown docs.
## Global Constraints
- Do not move Browser Inbox conversion workflows into desktop core.
- Do not call Notes, Activity, Journal, or Search private APIs.
- Use only public plugin APIs: `api.files.writeText` and `api.events.publish`.
- Do not silently rename on note path conflict; rely on `overwrite: false`.
- Use TDD: write the failing smoke test first, run it red, then implement.
---
### Task 1: Browser Inbox Create Note Conversion
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/plugin.json`
**Interfaces:**
- Consumes:
- `api.files.writeText(relativePath, content, { createIfMissing: true, overwrite: false })`
- `api.events.publish('browser.capture.converted', payload)`
- Produces:
- `Create Note` action for workspace-scoped captures.
- `browser.capture.converted` event after successful conversion.
- [ ] **Step 1: Write the failing smoke test**
Add fake API support for `files.writeText` and `events.publish` in
`scripts/smoke-browser-inbox-plugin.js`, then add a scenario that clicks
`data-browser-inbox-action="create-note"` and asserts the written path,
Markdown content, write options, queue removal, and published event.
- [ ] **Step 2: Run RED**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
```
Expected: fails because `Create Note` is not rendered.
- [ ] **Step 3: Implement conversion**
Add helpers in `plugins/browser-inbox/frontend/src/index.js`:
- `noteTitle(capture)`;
- `safeNoteFilename(title)`;
- `captureToMarkdown(capture)`;
- `createNoteFromCapture(capture)`.
Render a `Create Note` button for captures with a workspace root. On success,
remove the capture and publish `browser.capture.converted`.
- [ ] **Step 4: Update manifest permissions**
Add `files.write` and `events.publish` to
`plugins/browser-inbox/plugin.json`.
- [ ] **Step 5: Run GREEN**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
PATH=/tmp/verstak2-tools/venv/bin:/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/check.sh
```
Expected: both commands exit 0.
### Task 2: Roadmap Documentation
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified create-note conversion behavior.
- Produces docs that mark note conversion done while leaving link/file/activity conversion for later.
- [ ] **Step 1: Update docs**
Describe Browser Inbox create-note conversion through the public Files API and
mark only note conversion as complete.
- [ ] **Step 2: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "Create Note|note conversion|conversion workflow|notes/links/files/activity" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
```
Expected: `git diff --check` exits 0 and `rg` shows updated status lines.

View File

@ -1,119 +0,0 @@
# Browser Inbox Domain Binding Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Route unscoped Browser Inbox captures into workspace queues through plugin-owned domain bindings.
**Architecture:** Keep domain binding in `verstak.browser-inbox`. The plugin reads `domainBindings` from its own settings namespace, annotates unscoped incoming captures before storage, and preserves desktop core as a capture-event publisher only.
**Tech Stack:** Plain JavaScript official plugin bundle, Node smoke test harness, Markdown docs.
## Global Constraints
- Do not move Browser Inbox queues or conversion workflows into desktop core.
- Do not import Notes, Files, Activity, or Journal from Browser Inbox.
- Route only captures that do not already include `workspaceRootPath`.
- Domain matching is exact and case-insensitive for this slice.
- Use TDD: write the failing smoke test first, run it red, then implement.
---
### Task 1: Browser Inbox Domain Routing
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js`
**Interfaces:**
- Consumes: Browser Inbox plugin settings key `domainBindings`.
- Produces: unscoped captures annotated with `workspaceRootPath` and `workspaceName` when an exact domain binding exists.
- [ ] **Step 1: Write the failing smoke test**
Add a scenario to `scripts/smoke-browser-inbox-plugin.js`:
```js
const bindingApi = makeApi({
domainBindings: {
'client.example.com': 'ClientA',
'project.example.com': 'Project'
}
});
const bindingGlobal = await mountWithApi(bindingApi, {});
await bindingApi.handlers['browser.capture.page']({
name: 'browser.capture.page',
timestamp: '2026-06-29T00:00:00Z',
payload: {
captureId: 'bound-client-capture',
capturedAt: '2026-06-29T00:00:00.000Z',
kind: 'page',
url: 'https://client.example.com/page',
title: 'Bound Client Page',
domain: 'client.example.com'
}
});
await flush();
if (bindingApi.getStoredCaptures('captures:workspace:ClientA').length !== 1) {
throw new Error('domain-bound capture was not stored under ClientA workspace key');
}
```
Also prove explicit `workspaceRootPath: "Project"` wins over a binding for
`client.example.com`.
- [ ] **Step 2: Run RED**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
```
Expected: fails because domain-bound captures are still stored in the receiving
view scope.
- [ ] **Step 3: Implement minimal routing**
In `plugins/browser-inbox/frontend/src/index.js`, add helper functions to
normalize binding keys, derive a domain from capture fields, and annotate
captures without `workspaceRootPath` before storage.
- [ ] **Step 4: Run GREEN**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
PATH=/tmp/verstak2-tools/venv/bin:/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/check.sh
```
Expected: both commands exit 0.
### Task 2: Roadmap Documentation
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified plugin routing behavior.
- Produces docs that distinguish implemented domain binding from future conversion workflows.
- [ ] **Step 1: Update docs**
Mark domain binding as implemented in the Browser Inbox status text and roadmap
while leaving conversion workflow as future work.
- [ ] **Step 2: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "domain-to-workspace binding|Domain binding|conversion" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
```
Expected: `git diff --check` exits 0 and `rg` shows the updated status lines.

View File

@ -1,171 +0,0 @@
# Browser Inbox Text File Attachment Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add end-to-end text file attachment capture and conversion through Browser Inbox.
**Architecture:** The browser extension remains a protocol producer and does not know workspace internals. Desktop receiver validates and republishes `browser.capture.file`; Browser Inbox stores the capture and writes the final text file through the public Files API.
**Tech Stack:** Plain JavaScript WebExtension code, Go receiver tests, official plugin JavaScript smoke harness, Markdown docs.
## Global Constraints
- Do not add private `.verstak/inbox` staging in this slice.
- Do not add binary file writes in this slice.
- Use only existing public plugin APIs for conversion: `api.files.writeText` and `api.events.publish`.
- Keep the extension offline queue behavior unchanged.
- Use TDD for each behavior change.
---
### Task 1: Extension Text File Capture Protocol
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/protocol.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/background.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.html`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.css`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-protocol.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/README.md`
**Interfaces:**
- Produces `kind: "file"` capture payloads with `file.name`, `file.mime`,
`file.size`, and `file.text`.
- [ ] **Step 1: Write RED protocol assertions**
Add a test that builds a file capture and asserts `validateCapture` accepts it,
then add a validation rejection for missing `file.text`.
- [ ] **Step 2: Run RED**
```bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test
```
Expected: failure mentioning unsupported `kind` or missing file validation.
- [ ] **Step 3: Implement protocol and popup capture**
Add `file` payload support in `buildCapture` / `validateCapture`. Add a popup
file input and `Send File` button that reads one selected file as text and sends
`{ type: "verstak.capture", kind: "file", fileName, fileMime, fileSize, fileText }`.
- [ ] **Step 4: Run GREEN**
```bash
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm test
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH npm run build
```
Expected: protocol tests pass and extension dist builds.
### Task 2: Desktop Receiver File Event
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/core/browserreceiver/receiver.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop-current/internal/core/browserreceiver/receiver_test.go`
**Interfaces:**
- Consumes extension `kind: "file"` payloads.
- Produces `browser.capture.file` events with flattened `fileName`,
`fileMime`, `fileSize`, and `fileText`.
- [ ] **Step 1: Write RED receiver test**
Add a test that subscribes to `browser.capture.file`, posts a file capture, and
asserts the flattened event payload.
- [ ] **Step 2: Run RED**
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop-current
go test ./internal/core/browserreceiver -run TestReceiverAcceptsFileCaptureAndPublishesEvent -count=1
```
Expected: failure with unsupported kind.
- [ ] **Step 3: Implement receiver support**
Add a `CaptureFile` struct, `File *CaptureFile` field, file validation, and
file payload flattening in `EventPayload`.
- [ ] **Step 4: Run GREEN**
```bash
go test ./internal/core/browserreceiver
./scripts/test.sh
```
Expected: receiver package and desktop script pass.
### Task 3: Browser Inbox Create File Conversion
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js`
**Interfaces:**
- Consumes `browser.capture.file` events.
- Produces `Create File` action and `browser.capture.converted` with
`conversionType: "file"`.
- [ ] **Step 1: Write RED smoke test**
Add success and failure scenarios for a workspace file capture. Assert the write
path `Project/Files/notes.txt`, file content, write options, queue removal, and
converted event.
- [ ] **Step 2: Run RED**
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
```
Expected: failure because `browser.capture.file` is not subscribed or
`Create File` is not rendered.
- [ ] **Step 3: Implement conversion**
Preserve `fileName`, `fileMime`, `fileSize`, and `fileText` in storage. Render a
`Create File` button for workspace file captures with text content. Write the
safe file path through `api.files.writeText`, publish the converted event, and
remove the capture on success.
- [ ] **Step 4: Run GREEN**
```bash
PATH=/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH node scripts/smoke-browser-inbox-plugin.js
PATH=/tmp/verstak2-tools/venv/bin:/tmp/verstak2-tools:/home/mirivlad/.lmstudio/.internal/utils:$PATH ./scripts/check.sh
```
Expected: browser inbox smoke and official plugin check pass.
### Task 4: Roadmap Documentation
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified file capture/conversion behavior.
- Produces roadmap language that marks text file attachment conversion complete
while leaving binary attachments for a future public API slice.
- [ ] **Step 1: Update docs**
Describe Browser Inbox text file capture and conversion through the public Files
API.
- [ ] **Step 2: Verify docs**
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "text file attachment|binary attachment|browser.capture.file|Create File" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md docs/superpowers/specs/2026-06-29-browser-inbox-text-file-attachment-design.md docs/superpowers/plans/2026-06-29-browser-inbox-text-file-attachment.md
```
Expected: whitespace check exits 0 and `rg` shows updated status lines.

View File

@ -1,179 +0,0 @@
# Browser Receiver Pairing Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a token-based pairing gate to the local browser capture receiver.
**Architecture:** Keep Browser Inbox as a plugin. The desktop runtime generates
and requires an installation-local receiver token, while the plugin and browser
extension expose the settings transfer and rotation flow.
**Tech Stack:** Go desktop core package tests, browser extension protocol docs, Markdown docs.
## Completion Status (2026-07-10)
Completed. The implementation also added bounded ingress validation, token
persistence and rotation, the `browser.receiver.manage` SDK permission, a
Browser Inbox settings panel, and extension popup token persistence.
## Global Constraints
- Do not move Browser Inbox queues or conversion workflows into desktop core.
- Production startup must fail closed when a token cannot be persisted.
- Paired mode must not publish capture events for missing or wrong tokens.
- Use TDD: write the failing Go receiver test first, run it red, then implement.
- Commit and push each affected repository after meaningful changes.
---
### Task 1: Document The Pairing Contract
**Files:**
- Create: `/home/mirivlad/git/verstak2/verstak-docs/docs/superpowers/specs/2026-06-29-browser-receiver-pairing-design.md`
- Create: `/home/mirivlad/git/verstak2/verstak-docs/docs/superpowers/plans/2026-06-29-browser-receiver-pairing.md`
**Interfaces:**
- Produces documented `X-Verstak-Receiver-Token` pairing contract.
- [x] **Step 1: Write spec and plan**
Write the design and this implementation plan.
- [x] **Step 2: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
```
Expected: exits 0.
- [x] **Step 3: Commit and push docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git add docs/superpowers/specs/2026-06-29-browser-receiver-pairing-design.md docs/superpowers/plans/2026-06-29-browser-receiver-pairing.md
git commit -m "docs: plan browser receiver pairing"
git push
```
Expected: docs `main` is clean and pushed.
### Task 2: Desktop Receiver Token Gate
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver_test.go`
- Modify: `/home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver.go`
**Interfaces:**
- Produces:
- `type Options struct { RequireToken bool; ReceiverToken string }`
- `func NewWithOptions(bus *events.Bus, options Options, providers ...WorkspaceProvider) *Receiver`
- [x] **Step 1: Write the failing tests**
Add tests proving missing/wrong token rejection and correct token acceptance.
- [x] **Step 2: Run RED**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop
go test ./internal/core/browserreceiver
```
Expected: fails because `Options` / `NewWithOptions` do not exist.
- [x] **Step 3: Implement token gate**
Add `Options`, `NewWithOptions`, header validation, and constant-time token
comparison. Keep `New` behavior unchanged.
- [x] **Step 4: Run GREEN**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop
go test ./internal/core/browserreceiver
go test ./internal/core/...
```
Expected: both commands exit 0.
- [x] **Step 5: Commit and push desktop**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-desktop
git add internal/core/browserreceiver/receiver.go internal/core/browserreceiver/receiver_test.go
git commit -m "feat: require token for paired browser receiver"
git push
```
Expected: commit is pushed. Existing unrelated `frontend/wailsjs/go/models.ts`
remains unstaged.
### Task 3: Extension And Roadmap Docs
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-browser-extension/README.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified receiver token gate.
- Produces docs matching implemented pairing behavior.
- [x] **Step 1: Update extension README**
Change the receiver token header description from optional future work to:
```md
- `X-Verstak-Receiver-Token: <token>` required when the desktop receiver is in paired mode
```
- [x] **Step 2: Verify and commit extension docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension
git diff --check
git add README.md
git commit -m "docs: describe receiver token pairing"
git push
```
Expected: extension `main` is clean and pushed.
- [x] **Step 3: Update platform docs**
In `05_Official_Plugins.md`, describe that Browser Inbox receives captures
through the local receiver token pairing model. In
`07_Full_Implementation_Roadmap.md`, mark:
```md
- [x] define local receiver permission/pairing model;
```
- [x] **Step 4: Verify and commit docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "define local receiver permission/pairing model|X-Verstak-Receiver-Token" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
git add 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
git commit -m "docs: mark browser receiver pairing complete"
git push
```
Expected: docs `main` is clean and pushed.

View File

@ -1,178 +0,0 @@
# Journal Worklog Plugin Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Create a baseline `verstak.journal` plugin that stores worklog entries and imports Activity worklog suggestions.
**Architecture:** Implement Journal as a plain official frontend plugin. Store entries in plugin settings by workspace key, render a workspace/global Journal view, and consume Activity suggestions through `api.commands.executeFor`.
**Tech Stack:** Official plugin JSON manifest, browserless JavaScript bundle, Node smoke test, Markdown docs.
## Global Constraints
- Journal/worklog remains plugin functionality, not desktop core.
- Do not build billing reports or invoice export in this slice.
- Import from Activity must degrade cleanly when Activity is unavailable.
- Use TDD: write and run a failing smoke test before creating the plugin.
- Commit and push each affected repository after meaningful changes.
---
### Task 1: Document The Slice
**Files:**
- Create: `/home/mirivlad/git/verstak2/verstak-docs/docs/superpowers/specs/2026-06-29-journal-worklog-plugin-design.md`
- Create: `/home/mirivlad/git/verstak2/verstak-docs/docs/superpowers/plans/2026-06-29-journal-worklog-plugin.md`
**Interfaces:**
- Produces a written contract for `verstak.journal`.
- [ ] **Step 1: Write spec and plan**
Write the design and this implementation plan.
- [ ] **Step 2: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
```
Expected: exits 0.
- [ ] **Step 3: Commit and push docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git add docs/superpowers/specs/2026-06-29-journal-worklog-plugin-design.md docs/superpowers/plans/2026-06-29-journal-worklog-plugin.md
git commit -m "docs: plan journal worklog plugin"
git push
```
Expected: docs `main` is clean and pushed.
### Task 2: Journal Plugin
**Files:**
- Create: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/plugin.json`
- Create: `/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/frontend/src/index.js`
- Create: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-journal-plugin.js`
- Modify: `/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/check.sh`
**Interfaces:**
- Produces plugin id `verstak.journal`.
- Produces component `JournalView`.
- Consumes `api.commands.executeFor('verstak.activity', 'verstak.activity.suggestWorklog', args)`.
- [ ] **Step 1: Write the failing smoke test**
Create `scripts/smoke-journal-plugin.js` and assert manifest identity,
mounting, manual entry storage, Activity import, duplicate prevention, and
global aggregation.
- [ ] **Step 2: Run RED**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-journal-plugin.js
```
Expected: fails because the Journal manifest and frontend entry do not exist.
- [ ] **Step 3: Add manifest and frontend**
Create `plugins/journal/plugin.json` with `provides` of `worklog`, `journal`,
and `report.worklog`, optional dependency `activity.reconstruction`, and
workspace/sidebar UI contributions. Create `JournalView` that stores entries
under `worklog:workspace:<encoded workspace root>`.
- [ ] **Step 4: Wire check script**
Add `node "$ROOT/scripts/smoke-journal-plugin.js"` to the frontend smoke
section in `scripts/check.sh`.
- [ ] **Step 5: Run GREEN**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-journal-plugin.js
./scripts/check.sh
```
Expected: both commands exit 0.
- [ ] **Step 6: Commit and push official plugin**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
git add plugins/journal scripts/smoke-journal-plugin.js scripts/check.sh
git commit -m "feat: add journal worklog plugin"
git push
```
Expected: official plugins `main` is clean and pushed.
### Task 3: Mark Roadmap Progress
**Files:**
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md`
- Modify: `/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md`
**Interfaces:**
- Consumes verified Journal plugin commit.
- Produces docs matching implemented behavior.
- [ ] **Step 1: Update Journal status**
Add a current status sentence describing the baseline Journal plugin, manual
worklog entries, and Activity suggestion import.
- [ ] **Step 2: Update roadmap**
Change:
```md
- implement journal/worklog plugin that can consume activity suggestions.
```
to:
```md
- [x] implement journal/worklog plugin that can consume activity suggestions.
```
- [ ] **Step 3: Verify docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "implement journal/worklog plugin that can consume activity suggestions|baseline `verstak.journal`" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
```
Expected: `git diff --check` exits 0 and `rg` shows the checked roadmap line
and Journal status.
- [ ] **Step 4: Commit and push docs**
Run:
```bash
cd /home/mirivlad/git/verstak2/verstak-docs
git add 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
git commit -m "docs: mark journal worklog plugin complete"
git push
```
Expected: docs `main` is clean and pushed.

View File

@ -1,281 +0,0 @@
# Platform Localization Implementation Plan
> **Execution note:** implement task-by-task in the current session. Do not use
> subagents. Use TDD for every production boundary and keep commits scoped to a
> single repository.
**Goal:** Add persisted System/Russian/English language selection to Verstak
Desktop, localize the shell and all official plugins, and establish the public
SDK contract used by future multilingual plugins.
**Architecture:** Desktop owns the application preference, locale resolution,
shell catalogs, safe plugin-catalog loading, and a generic runtime bridge. Each
plugin owns its manifest and UI catalogs and accesses them through
`api.i18n`. Manifest literals remain English fallbacks. Locale changes update
mounted UI without remounting plugin components.
**Tech stack:** Go/Wails backend tests, Svelte 4 and plain JavaScript frontend,
TypeScript SDK with Vitest, JSON Schema, browserless Node plugin smoke tests,
Playwright mocked-Wails E2E.
## Global Constraints
- `~/git/verstak` is UI reference only; do not copy its implementation.
- Core must not contain official plugin messages or depend on official plugin
IDs.
- Official and third-party plugins use the same manifest and runtime API.
- Store only `system`, `ru`, or `en`; default to `system`.
- Resolve `ru-*` system locales to `ru`, everything else to `en`.
- Do not remount plugins on locale changes or discard form/editor state.
- Preserve existing unrelated generated Wails binding changes in the desktop
worktree.
- Add tests before production changes and observe each focused test fail before
implementing its boundary.
---
## Task 1: SDK localization contract
**Files:**
- Modify `verstak-sdk/schemas/manifest.json`
- Modify `verstak-sdk/src/types.ts`
- Modify `verstak-sdk/src/plugin-api.ts`
- Modify `verstak-sdk/src/test-utils.ts`
- Modify `verstak-sdk/src/plugin-api.test.ts`
- Generated by build: `verstak-sdk/dist/*`
**Contract produced:**
```ts
interface PluginLocalizationConfig {
defaultLocale: string;
locales: Record<string, string>;
}
interface PluginI18nAPI {
getLocale(): 'ru' | 'en';
t(key: string, params?: Record<string, string | number>, fallback?: string): string;
onDidChangeLocale(listener: (locale: 'ru' | 'en') => void): Unsubscribe;
}
```
- [ ] Add failing schema/type/mock tests for a valid manifest localization
block, required default catalog, safe relative paths, API shape, fallback,
interpolation, and locale-change notification.
- [ ] Run `npm test -- --run src/plugin-api.test.ts` and confirm RED.
- [ ] Add `localization` to the JSON Schema and TypeScript manifest types.
- [ ] Add `i18n` to `VerstakPluginAPI` and configurable locale/messages to
`createMockPluginAPI`.
- [ ] Run focused tests GREEN.
- [ ] Run `npm test`, `npm run lint`, and `npm run build`.
- [ ] Commit only SDK source/schema/generated dist changes.
---
## Task 2: Desktop application language setting
**Files:**
- Modify `verstak-desktop/internal/core/appsettings/manager.go`
- Modify `verstak-desktop/internal/core/appsettings/manager_test.go`
- Modify `verstak-desktop/internal/api/app.go`
- Modify `verstak-desktop/internal/api/app_test.go`
**Behavior:** missing language becomes `system`; valid updates persist; invalid
updates return an error; unrelated settings remain unchanged.
- [ ] Add failing manager tests for default, reload, and preservation.
- [ ] Add failing API tests for `GetAppSettings` and accepted/rejected patches.
- [ ] Run focused Go tests and confirm RED.
- [ ] Add `Language` to `appsettings.Config`, default/load normalization, and
update handling without changing unrelated values.
- [ ] Expose and validate `language` in the Wails API.
- [ ] Run focused tests GREEN.
- [ ] Run `gofmt` on changed Go files and `go test ./internal/core/appsettings
./internal/api`.
---
## Task 3: Plugin localization metadata and safe catalog loading
**Files:**
- Modify `verstak-desktop/internal/core/plugin/plugin.go`
- Modify `verstak-desktop/internal/core/plugin/plugin_test.go`
- Modify `verstak-desktop/internal/api/app.go`
- Modify `verstak-desktop/internal/api/app_test.go`
- Generated Wails bindings are intentionally not regenerated over existing
user changes; frontend imports call the binding only after generation is
explicitly reconciled.
**Backend API:**
```go
GetPluginLocalization(pluginID, locale string) (map[string]string, string)
```
- [ ] Add failing manifest parsing tests for localization metadata.
- [ ] Add failing API tests for declared catalog reads, locale fallback input,
malformed JSON, non-string values, missing declarations, absolute paths,
backslashes, traversal, and containment.
- [ ] Run focused Go tests and confirm RED.
- [ ] Add localization manifest structs and the read-only safe catalog API.
- [ ] Run focused tests GREEN and `go test ./internal/core/plugin ./internal/api`.
---
## Task 4: Frontend localization service
**Files:**
- Add `verstak-desktop/frontend/src/lib/i18n/catalogs/en.js`
- Add `verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js`
- Add `verstak-desktop/frontend/src/lib/i18n/index.js`
- Add `verstak-desktop/frontend/tests/i18n-test.mjs`
- Modify `verstak-desktop/frontend/src/main.js`
- Modify `verstak-desktop/frontend/src/lib/test/wails-mock.js`
- [ ] Add a failing standalone Node test for preference validation, system
locale resolution, interpolation, shell fallback, plugin catalog fallback,
subscription, and contribution-copy localization.
- [ ] Run `node frontend/tests/i18n-test.mjs` and confirm RED.
- [ ] Implement the framework-independent locale store and catalog cache.
- [ ] Initialize it from `GetAppSettings` before mounting Svelte.
- [ ] Extend the Wails mock with language state and plugin catalogs.
- [ ] Run the focused test GREEN and `npm run build`.
---
## Task 5: Runtime `api.i18n` bridge
**Files:**
- Modify `verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js`
- Modify `verstak-desktop/frontend/src/lib/plugin-host/PluginBundleHost.svelte`
- Modify `verstak-desktop/frontend/tests/plugin-api-contributions-test.mjs`
- Modify `verstak-desktop/frontend/tests/bundle-host-test.cjs`
- [ ] Extend bridge tests first to require `getLocale`, `t`, and disposable
`onDidChangeLocale` behavior.
- [ ] Confirm focused RED.
- [ ] Preload the plugin catalog before mount and expose the i18n service
through the public API.
- [ ] Keep locale subscriptions under existing API disposal cleanup.
- [ ] Translate host loading/error states through shell catalogs.
- [ ] Confirm bridge/bundle tests GREEN.
---
## Task 6: Language selector and shell migration
**Files:**
- Modify `verstak-desktop/frontend/src/App.svelte`
- Modify every Svelte file under `verstak-desktop/frontend/src/lib/shell/`
- Modify every Svelte file under
`verstak-desktop/frontend/src/lib/plugin-manager/`
- Modify `verstak-desktop/frontend/src/lib/plugin-host/PluginBundleHost.svelte`
- Modify `verstak-desktop/frontend/e2e/status-bar.spec.js`
- Add `verstak-desktop/frontend/e2e/localization.spec.js`
- Modify `verstak-desktop/frontend/tests/shell-source-contract-test.mjs` where
assertions intentionally depend on translated literals.
- [ ] Add failing E2E assertions for the language submenu/radio state,
persistence, Russian system locale, English fallback, and live shell update.
- [ ] Confirm focused Playwright RED.
- [ ] Add the language submenu to StatusBar and persist changes through
`UpdateAppSettings`.
- [ ] Replace shell user-facing literals with shell catalog keys, including
aria labels, tooltips, empty/loading/error/confirmation text.
- [ ] Localize copies of plugin manifests and contributions before display.
- [ ] Confirm focused E2E GREEN, standalone shell tests GREEN, and frontend
production build GREEN.
---
## Task 7: Official plugin localization declarations and catalog checks
**Files:**
- Modify every `verstak-official-plugins/plugins/*/plugin.json`
- Add `locales/en.json` and `locales/ru.json` under every official plugin
- Add `verstak-official-plugins/scripts/check-locales.mjs`
- Modify `verstak-official-plugins/scripts/check.sh`
- Modify relevant plugin smoke harness API mocks under
`verstak-official-plugins/scripts/`
- [ ] Add the locale checker first and confirm it fails for missing metadata
and catalogs.
- [ ] Add localization declarations to all 13 manifests.
- [ ] Add English and Russian manifest/contribution keys to all catalogs.
- [ ] Verify catalog parity, string-only values, safe paths, and required
derived manifest keys.
- [ ] Run `./scripts/check.sh` GREEN before migrating runtime UI strings.
---
## Task 8: Plain-JavaScript official plugin UI migration
**Files:**
- Modify `frontend/src/index.js` for activity, browser-inbox, default-editor,
file-preview, files, journal, notes, platform-test, search, secrets, todo, and
trash.
- Expand each plugin's `locales/en.json` and `locales/ru.json`.
- Modify focused `scripts/smoke-*-plugin.js` tests.
For each plugin, repeat independently:
- [ ] Add a failing bilingual rendering assertion and a locale-change state
preservation assertion to its existing smoke test.
- [ ] Add a local `t` helper backed only by `api.i18n`.
- [ ] Replace user-visible literals, attributes, empty/loading/error states,
confirmation prompts, and known validation messages with stable keys.
- [ ] Subscribe once at mount, update only rendered text/state, and unsubscribe
at unmount.
- [ ] Run the focused plugin smoke test GREEN before moving to the next plugin.
Do not translate IDs, paths, user data, logs, API labels in diagnostics, or
provider-returned values.
---
## Task 9: Svelte Sync plugin migration
**Files:**
- Modify `verstak-official-plugins/plugins/sync/frontend/src/SyncSettings.svelte`
- Modify `verstak-official-plugins/plugins/sync/frontend/src/SyncStatusBar.svelte`
- Modify `verstak-official-plugins/plugins/sync/frontend/src/index.js`
- Expand `verstak-official-plugins/plugins/sync/locales/en.json`
- Expand `verstak-official-plugins/plugins/sync/locales/ru.json`
- Modify Sync smoke/E2E tests as appropriate.
- [ ] Add failing English/Russian and live-change assertions.
- [ ] Bridge `api.i18n` into reactive Svelte state without remounting.
- [ ] Translate UI labels and known status/error chrome.
- [ ] Run Sync plugin build and focused tests GREEN.
---
## Task 10: Full verification and documentation
**Files:**
- Modify `verstak-docs/04_Plugin_System.md`
- Modify `verstak-docs/07_Full_Implementation_Roadmap.md`
- Modify other contract docs only where the implemented behavior requires it.
- [ ] Run SDK: `npm test`, `npm run lint`, `npm run build`.
- [ ] Run desktop backend: `go test -count=1 ./...` and `go vet ./...`.
- [ ] Run desktop standalone frontend tests, production build, and focused then
full `npm run test:e2e`.
- [ ] Run official plugins: `./scripts/check.sh` and all plugin builds.
- [ ] Run `git diff --check` in every changed repository.
- [ ] Inspect changed-file lists and ensure pre-existing Wails generated changes
are not staged or overwritten.
- [ ] Update docs to match only verified behavior.
- [ ] Commit scoped changes in each repository.
- [ ] Report exact verification labels. If native Wails/WebKit was not exercised,
state: `GUI behavior was not verified in the real desktop shell.`

View File

@ -1,240 +0,0 @@
# Alpha Activity And Journal Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Make Activity a durable, background-maintained session log that safely proposes Journal entries without creating a Дело automatically.
**Architecture:** Desktop adds a plugin background-service lifecycle, unsubscribe-capable event bridge, and append-only Activity log. The Activity plugin owns session reconstruction and watermarks; Journal receives an explicit reviewed candidate and requires a destination for unassigned sessions.
**Tech Stack:** Go, Wails, Svelte, plain JavaScript plugin bundles, Go tests, Node smoke tests, Playwright.
## Global Constraints
- Activity raw events are retained for 60 days, 10,000 events, or 8 MiB, whichever is reached first.
- Raw activity uses append/compaction, not settings.json rewrites.
- Session scope is workspaceId plus path cache or unassigned.
- Point-event duration is capped at 10 minutes per adjacent pair; gap over 20 minutes starts another session.
- Accepted/dismissed watermarks consume only the reviewed slice.
- Journal save is always an explicit user action.
---
### Task 1: Add background services, event unsubscription, and an append-only Activity log
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/events/bus.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api_test.go
- Create: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/BackgroundPluginHost.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/types.ts
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/contributions.json
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/manifest.json
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.test.ts
**Interfaces:**
- Adds contributes.backgroundServices with id and component fields.
- Bus Subscribe returns subscription ID; Unsubscribe removes only that handler.
- Adds ActivityLogAppend, ActivityLogRead, ActivityLogDeleteWorkspace, and ActivityLogCompact backend methods.
- Background bundle registration exposes start(api) returning a cleanup function.
- [ ] **Step 1: Write failing manifest, bus, and storage tests**
Add a manifest validation test and isolated bus-unsubscribe test:
~~~go
subA := bus.Subscribe("browser.activity.domain", handlerA)
subB := bus.Subscribe("browser.activity.domain", handlerB)
bus.Unsubscribe("browser.activity.domain", subA)
bus.Publish(events.Event{Name: "browser.activity.domain"})
if gotA != 0 || gotB != 1 { t.Fatalf("wrong selective unsubscribe: %d %d", gotA, gotB) }
~~~
Add an append test asserting two appends create two NDJSON records and a
compaction test keeping only the newest record under a small test limit.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -run 'Test.*(Background|Unsubscribe|ActivityLog)' -count=1
~~~
Expected: FAIL because subscriptions have no IDs and the log API does not exist.
- [ ] **Step 3: Implement the platform boundary**
Define:
~~~go
type ContributionBackgroundService struct {
ID string `json:"id"`
Component string `json:"component"`
}
type ActivityLogRecord struct {
ActivityID string `json:"activityId"`
Payload json.RawMessage `json:"payload"`
}
~~~
Append NDJSON under plugin-data/verstak.activity/events.ndjson using a locked
append. Compact to the declared retention limits by rewriting a temporary file.
Keep only compact candidate state in plugin data. BackgroundPluginHost loads
enabled plugins with a background contribution once at app startup and calls the
registered start function; it calls cleanup on disable/reload/destroy.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -count=1
git add internal/core/plugin internal/core/events internal/core/storage internal/api frontend/src/App.svelte frontend/src/lib/plugin-host
git commit -m "feat: add background activity runtime"
git -C /home/mirivlad/git/verstak2/verstak-sdk add src/types.ts schemas/contributions.json schemas/manifest.json src/plugin-api.test.ts
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define plugin background services"
~~~
Expected: focused tests prove one subscription, selective cleanup, append, and compaction.
### Task 2: Rebuild Activity sessions and persistent candidate watermarks
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/plugin.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-activity-plugin.js
**Interfaces:**
- Activity background service registers ActivityService.start(api).
- Event record includes activityId, scope, occurredAt, durationSeconds, and sessionId.
- Candidate contains sessionId, handledThrough, estimatedMinutes, dateSlices, and source activities.
- [ ] **Step 1: Write failing smoke scenarios**
Add three explicit scenarios:
~~~js
const nineMinutes = eventsAt('2026-07-12T10:00:00Z', '2026-07-12T10:09:00Z');
assert.equal(buildSessions(nineMinutes)[0].estimatedMinutes, 9);
const late = appendLateEvent(existingSession, eventAt('2026-07-12T09:58:00Z'));
assert.equal(late.sessionId, existingSession.sessionId);
assert.equal(candidateAfterDismiss.sourceActivityIds.includes('a'), false);
~~~
Also assert a 23:50 to 00:30 session has one session ID and date slices for both
local dates.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-activity-plugin.js
~~~
Expected: FAIL because Activity derives candidate IDs from first and last events.
- [ ] **Step 3: Implement background session state**
Replace view-mounted event recording with ActivityService.start(api). Persist
immutable generated session IDs and anchors in activity-state.json. Build scope
as either workspaceId plus root path or unassigned. Sum explicit browser
duration and only zero-duration adjacent point intervals, cap a point interval
at 10 minutes, split at 20 minutes, and cap a session at 120 minutes.
For accept or dismiss, store the ordered handledThrough watermark. New events
after the watermark require another 10 minutes before a new candidate appears;
late events at or before the watermark remain diagnostic only.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
node scripts/smoke-activity-plugin.js
./scripts/check.sh
git add plugins/activity scripts/smoke-activity-plugin.js
git commit -m "feat: persist activity sessions and watermarks"
~~~
Expected: session, late-event, dismissal, browser-duration, and midnight smoke tests pass.
### Task 3: Review candidates safely in Journal and expose sessions in UI
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-journal-plugin.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/activity.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/todo.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
**Interfaces:**
- Journal candidate request accepts destinationWorkspaceId only for unassigned scope.
- Saved Journal entry stores sourceSessionId and handledThrough in addition to activity IDs.
- Activity UI has Sessions default and Events diagnostic view.
- [ ] **Step 1: Write failing E2E and smoke assertions**
Require an unassigned candidate to show a destination selector and reject Save
until an active workspace ID is selected:
~~~js
await page.getByRole('button', { name: /review/i }).click();
await expect(page.getByText(/choose.*дело/i)).toBeVisible();
await expect(page.getByRole('button', { name: /save/i })).toBeDisabled();
~~~
Require the mock WritePluginSetting path to retain sourceSessionId after reload.
Require Clear Activity and Journal Delete to show cancellation-safe confirmation
before changing stored rows.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
npm run test:e2e -- activity.spec.js todo.spec.js
~~~
Expected: FAIL because the current mock discards settings and Journal only accepts path-scoped candidates.
- [ ] **Step 3: Implement review UI and local dates**
Render domain rows as hostname plus duration without URLs. Keep raw event
details in the Events tab. For unassigned scope, list active workspace IDs and
require a selection before handing the candidate to Journal. Preselect the
largest date slice, show both date slices, use local date conversion, and save
the session watermark with the Journal entry. Show a visible action if Journal
is disabled instead of silently dropping the request. Put a confirm/cancel
dialog in front of Clear Activity and Journal Delete; cancelling leaves the
append log, candidate state, and entry list untouched.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-activity-plugin.js && node scripts/smoke-journal-plugin.js
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- activity.spec.js todo.spec.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/activity plugins/journal scripts/smoke-activity-plugin.js scripts/smoke-journal-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: review activity sessions in journal"
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/activity.spec.js frontend/e2e/todo.spec.js frontend/src/lib/test/wails-mock.js
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover activity journal review flow"
~~~
Expected: candidate-to-Journal, local date, and persisted mock workflows pass.

View File

@ -1,216 +0,0 @@
# Alpha Browser Domain Activity Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Add explicitly consented, privacy-minimal active-tab domain timing to the browser extension and deliver immutable activity batches to Desktop.
**Architecture:** Manual captures remain unchanged. A new tracker stores mutable accumulators, immutable pending batches, and acknowledged IDs locally; Desktop accepts a separate authenticated activity endpoint and emits browser.activity.domain.
**Tech Stack:** Plain WebExtension JavaScript, Node tests, Go HTTP handler tests, shared JSON vectors.
## Global Constraints
- Passive tracking defaults to disabled and requires explicit informed consent.
- Track only normalized hostname plus duration for an active tab in a focused window.
- Never send URL, title, text, page content, keystrokes, or history.
- A pending batch is byte-for-byte immutable; acknowledge only its own ID.
- Discard ambiguous or negative clock deltas and gaps over 10 minutes.
- Use hostname-normalization-v1 for extension, Desktop receiver, exclusions, and bindings.
---
### Task 1: Define and test canonical hostname normalization
**Files:**
- Create: /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname.js
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-hostname.js
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize.go
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize_test.go
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
**Interfaces:**
- Produces JavaScript normalizeHostnameV1(value, mode).
- Produces Go hostname.NormalizeV1(value string, mode Mode) (string, error).
- Mode is URLSource for HTTP(S) URLs and BareHost for binding/exclusion input.
- [ ] **Step 1: Write the shared vector corpus and failing tests**
Include vectors for Unicode/punycode, trailing dot, port stripping/rejection,
IPv4, bracketed IPv6, localhost, one-label internal names, malformed labels,
and an overlong name:
~~~json
[
{"mode":"bare","input":"пример.рф.","want":"xn--e1afmkfd.xn--p1ai"},
{"mode":"url","input":"https://Example.COM:8443/a","want":"example.com"},
{"mode":"bare","input":"example.com:8443","error":"port"},
{"mode":"bare","input":"[::1]","want":"::1"}
]
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension
node scripts/test-hostname.js
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
~~~
Expected: both fail because the normalizers do not exist.
- [ ] **Step 3: Implement both normalizers**
Use the browser URL parser plus ASCII IDNA conversion in JavaScript. Use
net/url, net/netip, and golang.org/x/net/idna.Lookup.ToASCII in Go. Store
ASCII A-labels, lowercase names, no ports or IPv6 brackets, and reject malformed
or over-limit values.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension && npm test
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
git -C /home/mirivlad/git/verstak2/verstak-sdk add schemas/hostname-normalization-v1.json
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define hostname normalization vectors"
git -C /home/mirivlad/git/verstak2/verstak-browser-extension add shared/hostname.js shared/hostname-normalization-v1.json scripts/test-hostname.js
git -C /home/mirivlad/git/verstak2/verstak-browser-extension commit -m "feat: normalize activity hostnames"
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/hostname
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: normalize browser hostnames"
~~~
Expected: every copied corpus passes in both implementations.
### Task 2: Add a separate authenticated Desktop activity receiver
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
**Interfaces:**
- Accepts POST /activities with BrowserDomainActivity.
- Emits browser.activity.domain with normalized hostname, duration, ID, and optional workspaceId.
- Returns status accepted and activityId for first delivery and duplicate delivery.
- [ ] **Step 1: Write failing handler tests**
Create a valid immutable request and then retry the same ID:
~~~go
payload := "{\"schemaVersion\":1,\"activityId\":\"a-1\",\"hostname\":\"пример.рф\",\"durationSeconds\":300,\"startedAt\":\"2026-07-12T10:00:00Z\",\"endedAt\":\"2026-07-12T10:05:00Z\"}"
req := httptest.NewRequest(http.MethodPost, "/activities", strings.NewReader(payload))
req.Header.Set("X-Verstak-Receiver-Token", "pair-token")
~~~
Assert invalid tokens, URL-like hostnames, zero or oversize duration, and a
duplicate ID do not publish a second event.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver -run Activity -count=1
~~~
Expected: FAIL because /activities is not routed.
- [ ] **Step 3: Implement the bounded idempotent route**
Add BrowserDomainActivity, a 30-day bounded receiver ID cache, and
handleActivity which invokes hostname.NormalizeV1. Do not reuse capture types
or persistence. In App, resolve an active exact hostname binding to workspaceId
and a path cache before Activity receives the event.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver ./internal/api -count=1
git add internal/core/browserreceiver/receiver.go internal/core/browserreceiver/receiver_test.go internal/api/app.go internal/api/app_test.go
git commit -m "feat: receive browser domain activity"
~~~
Expected: authentication, normalization, idempotency, and binding tests pass.
### Task 3: Build the opt-in extension tracker and immutable delivery queue
**Files:**
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/activity-tracker.js
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-activity-tracker.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/api.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/background.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.html
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.css
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/chromium/manifest.json
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/firefox/manifest.json
**Interfaces:**
- Persists activeAccumulator, pendingBatches, and acknowledgedIds under verstak.activityTracker.
- Produces sendActivity(receiverUrl, token, immutablePayload).
- Settings include passiveActivityEnabled false and excludedDomains empty.
- [ ] **Step 1: Write failing state-machine tests**
Cover consent, active-tab-only timing, long-gap discard, and the A/B batch case:
~~~js
tracker.addElapsed('example.com', 600, start, end);
const batchA = tracker.freeze('example.com');
tracker.addElapsed('example.com', 300, laterStart, laterEnd);
assert.equal(tracker.pendingBatches[0].payload.durationSeconds, 600);
tracker.acknowledge(batchA.activityId);
assert.equal(tracker.activeAccumulator['example.com'].durationSeconds, 300);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension
node scripts/test-activity-tracker.js
~~~
Expected: FAIL because the state machine is absent.
- [ ] **Step 3: Implement tracker, disclosure, and listeners**
Implement the tracker as pure functions. In background.js, register
tabs.onActivated, tabs.onUpdated, windows.onFocusChanged, idle.onStateChanged,
and a five-minute alarm only while opted in. Set idle detection to 600 seconds.
On a gap over 600 seconds, clock rollback, lock, or browser startup, reset the
active checkpoint without adding time. Retry frozen batches oldest-first and
remove only matching IDs.
Show an unchecked Russian/English consent explanation in settings. Add windows,
alarms, and idle permissions to both manifests. Keep manual capture queue and
popup actions unchanged.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
npm test
node scripts/test-activity-tracker.js
git add shared chromium/manifest.json firefox/manifest.json scripts/test-activity-tracker.js
git commit -m "feat: add opt-in browser domain tracker"
~~~
Expected: tracker unit tests and existing extension tests pass.

View File

@ -1,229 +0,0 @@
# Alpha Browser Inbox Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Preserve a manually captured link when it leaves the global Inbox, give Browser Inbox a usable archive, and make links durable across case lifecycle changes.
**Architecture:** A capture remains one canonical record. Its global archive state is independent of its workspaceId relation; domain bindings use the same stable identity. Browser Inbox owns capture UI and migration, while Desktop exposes a narrow URL-open capability for user-initiated links.
**Tech Stack:** Plain JavaScript plugin bundle, Go Wails API, Node smoke tests, Playwright.
## Global Constraints
- Only manual extension actions create Browser Inbox captures.
- Captures have at most one optional case assignment in this alpha.
- Remove from global Inbox archives; only Delete everywhere is permanent.
- Assignment and binding use workspaceId, never path as identity.
- Create .url files through files.write and never overwrite a name silently.
- URL opening is direct HTTP(S) opening, not a Linux .url association.
---
### Task 1: Migrate Browser Inbox records and implement archive state
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
**Interfaces:**
- Canonical capture fields: globalState, workspaceId, workspaceState, workspaceRootPath, workspaceTrashId.
- Domain binding value: workspaceId plus workspaceRootPath cache plus state.
- Mutations: archive, restore, assign, unassign, deleteEverywhere, migrate.
- [ ] **Step 1: Write failing migration and archive smoke tests**
Use a legacy path assignment and assert it resolves to a stable ID. Test the
original data-loss regression:
~~~js
await inbox.assignWorkspace('capture-1', activeWorkspace);
await inbox.archiveCapture('capture-1');
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), false);
assert.equal(workspaceRows(activeWorkspace.workspaceId).some(row => row.captureId === 'capture-1'), true);
await inbox.restoreCapture('capture-1');
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), true);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-browser-inbox-plugin.js
~~~
Expected: FAIL because archive and assignment are currently one mutable path field.
- [ ] **Step 3: Implement canonical state and idempotent migration**
Normalize every capture on read. Set legacy captures globalState active. Resolve
legacy paths through Desktop identities: active marker gives workspaceId,
otherwise set workspaceState unavailable. Store global captures once; workspace
views filter the canonical collection by workspaceId rather than duplicate
storage keys. Reject permanent deletion without an explicit confirm token from
the UI.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
node scripts/smoke-browser-inbox-plugin.js
./scripts/check.sh
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: preserve archived browser inbox captures"
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/api/app.go internal/api/app_test.go
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: migrate browser inbox relations"
~~~
Expected: migration, archive, restore, and assignment tests pass.
### Task 2: Handle rename, Trash, restore, purge, and archive UI
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/browser-inbox.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
**Interfaces:**
- Receives workspace.renamed, workspace.trashed, workspace.restored, and workspace.purged.
- Status filter values are active, archive, all.
- Archive bulk action is restore; bulk permanent delete is not offered.
- [ ] **Step 1: Write failing lifecycle and UI tests**
Assert a rename changes only path cache, trash disables routing, restore restores
the same workspace ID, purge makes captures unassigned and bindings orphaned:
~~~js
await emitWorkspaceEvent('workspace.trashed', { workspaceId: 'w-1', trashId: 't-1' });
assert.equal(capture.workspaceState, 'trashed');
assert.equal(domainBinding('client.example').state, 'trashed');
await emitWorkspaceEvent('workspace.purged', { workspaceId: 'w-1', trashId: 't-1' });
assert.equal(capture.workspaceState, 'unassigned');
assert.equal(domainBinding('client.example').state, 'orphaned');
~~~
In Playwright, select Archive, restore one visible item, and bulk-restore only
the filtered archive rows. Also click Delete everywhere for an assigned capture,
assert the dialog names its assigned Дело, cancel, and assert that the global
and assigned rows still exist.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
npm run test:e2e -- browser-inbox.spec.js
~~~
Expected: FAIL because the current UI has only processed filters and destructive Clear.
- [ ] **Step 3: Implement lifecycle and labels**
Subscribe once from the Browser Inbox background service. Update path caches on
rename only when workspaceId matches. On external unavailable, do not route to
the same path again. Add Active, Archive, and All filters; make search apply
inside the selected filter; show Archive badge inside a case; add Restore to
Inbox and visible filtered bulk restore. Rename Clear to an archive action with
count confirmation. Keep Delete everywhere separate and require a dialog that
names the assigned Дело; cancellation must perform no mutation.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- browser-inbox.spec.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: add browser inbox archive lifecycle"
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/browser-inbox.spec.js frontend/src/lib/test/wails-mock.js
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover browser inbox archive lifecycle"
~~~
Expected: archive filters, restore, rename, Trash, restore, and purge tests pass.
### Task 3: Save and open durable links without file-association reliance
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/permissions/registry.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/plugin.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
**Interfaces:**
- Permission and API: urls.openExternal(url string) for user-initiated HTTP(S) URLs only.
- SaveLinkInCase(captureId, workspaceId, filename) creates Links/name.url with InternetShortcut URL field.
- Collision response offers a proposed unique filename and never overwrites.
- [ ] **Step 1: Write failing API and plugin tests**
Add tests for invalid URL, readonly failure, unique filename, collision, and
direct opener argument:
~~~go
if errStr := app.OpenExternalURL("browser.plugin", "https://example.test/a"); errStr != "" {
t.Fatal(errStr)
}
if got := opener.Arguments[0]; got != "https://example.test/a" {
t.Fatalf("opened %q, want URL not .url path", got)
}
~~~
Add plugin smoke assertions for a title-derived name, Link fallback, and
proposal Link (2).url after a collision.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'Test.*OpenExternalURL' -count=1
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-browser-inbox-plugin.js
~~~
Expected: FAIL because only file-path external opening exists.
- [ ] **Step 3: Implement safe save and open**
Register urls.openExternal as dangerous. Validate HTTP(S), invoke the existing
OS opener with the URL string, and expose api.urls.openExternal. Browser Inbox
requests that permission, creates Links through files.createFolder and
files.writeText using:
~~~text
[InternetShortcut]
URL=https://example.test/
~~~
Sanitize a 96-character filename stem. On collision show an editable dialog
with proposed name (2).url and Cancel; leave capture state unchanged on errors.
When opening a saved link, parse and validate URL= before invoking the URL API.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/permissions internal/api frontend/src/lib/plugin-host
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: open external URLs safely"
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: save browser inbox links safely"
~~~
Expected: URL API tests and link save/collision smoke tests pass.

View File

@ -1,207 +0,0 @@
# Alpha Shell UX Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Make the Desktop overview scoped and actionable, hide diagnostic plugin IDs by default, and remove alpha UI strings that expose implementation noise.
**Architecture:** The shell consumes stable workspace IDs and plugin capabilities rather than hard-coded namespaces. Overview builds deterministic case-scoped projections; debug display is a presentation preference plus a session-only command-line override.
**Tech Stack:** Svelte, Wails app settings, Go tests, Playwright E2E, existing i18n catalogs.
## Global Constraints
- Default UI labels are Russian and use Дела, Входящие, Активности, Журнал.
- A normal tab never shows a plugin ID.
- Settings Debug Show plugin IDs defaults false; Desktop --debug enables it only for that run.
- Overview never leaks an unscoped event into every case.
- Todo entries are included only if todo.workspace capability is loaded.
- No action opens or creates a Дело implicitly.
---
### Task 1: Add effective debug display setting and human plugin tab labels
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/main.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-manager/PluginManager.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/plugin-api-bridge.spec.js
**Interfaces:**
- Persisted app setting showPluginIds bool defaults false.
- GetAppSettings returns effectiveShowPluginIds and UpdateAppSettings accepts showPluginIds.
- Runtime --debug sets effectiveShowPluginIds true without persisting it.
- [ ] **Step 1: Write failing app-setting and E2E tests**
Add a Go test proving the setting preserves theme and language, plus a browser
test proving normal mode has no raw ID and debug mode has one:
~~~js
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.' })).toHaveCount(0);
await mockAppSettings({ showPluginIds: true });
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.default-editor' })).toHaveCount(1);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -run 'Test.*PluginIds' -count=1
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
~~~
Expected: FAIL because no display preference exists.
- [ ] **Step 3: Implement presentation-only debug state**
Parse --debug before Wails startup and keep it in App runtime state. Merge it
with the persisted bool when returning effective settings. Render the localized
manifest title normally; append the plugin ID only when effectiveShowPluginIds
is true. Add the Debug settings checkbox and localized explanatory copy.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -count=1
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
git add main.go internal/core/appsettings internal/api frontend/src frontend/e2e/plugin-api-bridge.spec.js
git commit -m "feat: hide plugin IDs outside debug mode"
~~~
Expected: normal and debug labels are both verified.
### Task 2: Implement deterministic case-scoped Overview
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/TodaySurface.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-today.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-p0.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
**Interfaces:**
- Every overview row has workspaceId and lastMeaningfulAt.
- Needs attention maximum is five; Continue work maximum is four; Recent changes maximum is eight.
- Hidden recommendation state is keyed by entity ID and invalidated by later meaningful change.
- [ ] **Step 1: Write failing scope and ranking tests**
Add fixture events for two workspace IDs plus an unscoped event:
~~~js
await seedOverview({
selectedWorkspaceId: 'client-a',
events: [
{ activityId: 'a', workspaceId: 'client-a', type: 'note.saved', occurredAt: now },
{ activityId: 'b', workspaceId: 'client-b', type: 'file.changed', occurredAt: now },
{ activityId: 'global', type: 'workspace.selected', occurredAt: now }
]
});
await expect(page.getByText(/client-b/i)).toHaveCount(0);
await expect(page.locator('[data-overview-section="continue"] [data-overview-item]')).toHaveCount(4);
~~~
Also assert no selection shows only the select/create prompt and up to five
unassigned active Inbox captures.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
~~~
Expected: FAIL because rowsFor currently accepts untagged events for every workspace.
- [ ] **Step 3: Implement exact overview projections**
Replace path-only filtering with workspaceId equality. Build:
1. Needs attention: ready Journal candidates, active unprocessed captures, then
urgent Todo rows only if todo.workspace capability exists; take five.
2. Continue work: distinct entities updated in 14 days; sort by
lastMeaningfulAt descending and the documented type tie-break; take four.
3. Recent changes: note save/create, file create/rename or last change per
file, saved link, and Journal create in seven days; take eight.
Persist Hide recommendation without changing underlying data. Use localized
empty states and remove English technical strings.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
git add src/lib/shell/TodaySurface.svelte src/lib/shell/WorkspaceHost.svelte src/lib/i18n frontend/e2e/ux-today.spec.js frontend/e2e/ux-p0.spec.js src/lib/test/wails-mock.js
git commit -m "feat: make overview case-scoped and actionable"
~~~
Expected: limits, scope, empty state, and recommendation hide tests pass.
### Task 3: Verify alpha UX as a user sees it
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-followup.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/scripts/check-gui.sh
**Interfaces:**
- E2E fixture uses effectiveShowPluginIds false by default.
- GUI checker accepts a --debug-labels case.
- [ ] **Step 1: Write the final failing alpha scenarios**
Add a scenario that creates a capture, assigns it, archives it globally, opens
the assigned case, and verifies the capture is still present. Add a session
candidate path to Journal and assert normal tab labels are human-readable.
~~~js
await archiveGlobalCapture('capture-1');
await openWorkspace('client-a');
await expect(page.getByTestId('browser-capture-capture-1')).toBeVisible();
await expect(page.getByRole('tab', { name: /verstak\./i })).toHaveCount(0);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
npm run test:e2e -- ux-followup.spec.js
~~~
Expected: FAIL until the prior plans are integrated.
- [ ] **Step 3: Add deterministic GUI smoke commands**
Extend check-gui.sh to start an isolated test vault, run normal and --debug
screens, and save screenshots under frontend/e2e-results. The script must not
read or mutate the user's normal vault.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
npm run test:e2e -- ux-followup.spec.js
GOCACHE=/tmp/verstak-go-cache ./scripts/check-gui.sh
git add frontend/e2e/ux-followup.spec.js scripts/check-gui.sh
git commit -m "test: cover first alpha UX smoke flow"
~~~
Expected: E2E and isolated GUI smoke pass.

View File

@ -1,216 +0,0 @@
# Alpha Workspace Identity Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Give every managed Дело a durable UUID identity and publish lifecycle events that let alpha features follow a case across rename, Trash, restore, and path reuse.
**Architecture:** The filesystem remains the source of truth for whether a case folder exists. A UUID marker inside each case is the relation identity; Desktop metadata indexes that marker and paths remain presentation caches.
**Tech Stack:** Go, Wails bindings, JSON metadata, Go tests.
## Global Constraints
- A new Дело is created only by an explicit user action.
- workspaceId is UUID v4; a path is never relation identity.
- The marker is .verstak/workspace.json inside the case.
- Existing writable cases migrate without data loss; non-writable cases are view-only.
- Use TDD and make one commit per task.
---
### Task 1: Store and resolve durable workspace identities
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
**Interfaces:**
- Produces Workspace.ID string and Metadata.WorkspaceID string.
- Produces func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error).
- Produces func (m *Manager) ListWorkspaceIdentities() ([]WorkspaceIdentity, error).
- [ ] **Step 1: Write the failing identity tests**
Add tests proving creation writes a UUID marker, rename retains it, and a newly
created folder after external removal gets another UUID:
~~~go
ws, err := m.CreateWorkspace("Clients", "default")
if err != nil { t.Fatal(err) }
first, err := m.EnsureWorkspaceIdentity(ws.Name)
if err != nil || first.WorkspaceID == "" { t.Fatalf("identity = %+v, %v", first, err) }
if err := m.RenameWorkspace("Clients", "Clients-2026"); err != nil { t.Fatal(err) }
renamed, _ := m.EnsureWorkspaceIdentity("Clients-2026")
if renamed.WorkspaceID != first.WorkspaceID { t.Fatal("rename changed workspace ID") }
~~~
- [ ] **Step 2: Run the focused test to verify it fails**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -run 'Test.*Workspace.*Identity' -count=1
~~~
Expected: FAIL because WorkspaceIdentity and EnsureWorkspaceIdentity do not exist.
- [ ] **Step 3: Write the minimal marker implementation**
Add these types and use them in CreateWorkspace, GetWorkspaceMetadata, and
workspace listing:
~~~go
type WorkspaceIdentity struct {
WorkspaceID string `json:"workspaceId"`
RootPath string `json:"rootPath"`
State string `json:"state"`
}
const workspaceIdentityRelativePath = ".verstak/workspace.json"
func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error) {
// Validate the folder, read the in-folder marker, and atomically create
// a UUID-v4 marker only when absent and writable.
}
~~~
Write the same ID to central metadata only as an index. Never recover an ID from
stale path-keyed metadata when the in-folder marker is absent.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -count=1
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go
git commit -m "feat: add durable workspace identities"
~~~
Expected: tests pass and the commit contains only workspace identity changes.
### Task 2: Expose identity and lifecycle events through Desktop
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/api/App.d.ts
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/models.ts
**Interfaces:**
- Produces ListWorkspaceIdentities() ([]workspace.WorkspaceIdentity, string).
- Lifecycle payloads include workspaceId and workspaceRootPath; Trash also includes trashId.
- Produces workspace.restored and workspace.purged events.
- [ ] **Step 1: Add failing API tests**
Extend TestWorkspaceAPIPublishesLifecycleEvents:
~~~go
if got := received["workspace.created"]["workspaceId"]; got == "" {
t.Fatal("workspace.created must include workspaceId")
}
if got := received["workspace.renamed"]["workspaceId"]; got != createdID {
t.Fatalf("rename ID = %v, want %s", got, createdID)
}
~~~
Add restore and purge tests around RestoreVaultTrash and DeleteVaultTrash that
assert the original UUID and trash ID are emitted.
- [ ] **Step 2: Run red**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'TestWorkspaceAPI.*Lifecycle' -count=1
~~~
Expected: FAIL because lifecycle payloads only identify a path.
- [ ] **Step 3: Publish complete identity lifecycle**
Resolve the marker before publishing create, rename, selected, and trashed
events. Detect workspace Trash metadata in RestoreVaultTrash and
DeleteVaultTrash, then publish:
~~~go
map[string]interface{}{
"operation": "restore",
"workspaceId": identity.WorkspaceID,
"workspaceRootPath": restoredRoot,
"trashId": trashID,
}
~~~
Change App.svelte workspace-node conversion to preserve workspace.id while using
rootPath only for selection and display.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/api ./internal/core/workspace -count=1
git add internal/api/app.go internal/api/app_test.go frontend/src/App.svelte frontend/wailsjs/go/api/App.d.ts frontend/wailsjs/go/models.ts
git commit -m "feat: publish workspace identity lifecycle"
~~~
Expected: focused API and workspace tests pass.
### Task 3: Migrate legacy references and repair duplicate IDs
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
**Interfaces:**
- Produces RepairWorkspaceIdentity(keepRootPath, regenerateRootPath string) string.
- Identity states are active, unavailable, or duplicate.
- Later Inbox and Activity plans consume workspaceId through ListWorkspaceIdentities.
- [ ] **Step 1: Write failing duplicate and legacy tests**
Create two folders with copied marker data and assert neither is an active
relation target. Create a legacy path-keyed capture fixture and assert a
resolved path gets an ID while a missing path becomes unavailable:
~~~go
if errStr := app.RepairWorkspaceIdentity("Original", "Copied"); errStr != "" {
t.Fatal(errStr)
}
if originalID == copiedID { t.Fatal("repair must issue a new ID") }
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -run 'Test.*(Duplicate|Legacy).*Workspace' -count=1
~~~
Expected: FAIL because copied markers are accepted as ordinary folders.
- [ ] **Step 3: Implement deterministic repair**
Group active roots by marker UUID during identity listing. Mark a group with
more than one root duplicate and exclude it from assignment targets.
RepairWorkspaceIdentity verifies the duplicate, keeps the first marker, creates
a new marker for the second root, and moves no relation data.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -count=1
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go internal/api/app.go internal/api/app_test.go
git commit -m "feat: repair duplicate workspace identities"
~~~
Expected: legacy, path-reuse, and duplicate-ID tests pass.

View File

@ -1,99 +0,0 @@
# Activity Worklog Suggestions Design
## Purpose
`verstak.activity` already records public plugin events and displays scoped
activity streams. The next roadmap step is to turn that stream into a small,
useful reconstruction layer that can suggest worklog entries without making
Journal a core feature.
## Scope
This slice adds worklog suggestions to the official Activity plugin only.
It does not create the Journal plugin, reports, billing states, manual time
editing, or final worklog persistence. Those remain the next roadmap item.
## Behavior
- Activity reconstructs suggestions from the same normalized events it already
renders.
- Suggestions are scoped the same way as the Activity view:
- a workspace Activity view uses only that workspace stream;
- the global Activity view aggregates all streams and produces suggestions
per workspace/day.
- Events are grouped by workspace and calendar day using `occurredAt` or
`receivedAt`.
- Each group produces one suggestion with:
- stable `suggestionId`;
- `workspaceRootPath`;
- `date`;
- `title`;
- `summary`;
- `minutes`;
- source `eventIds`.
- Estimated time is conservative:
- one event: 15 minutes;
- multiple events: time span between first and last event, rounded up to
15-minute increments;
- minimum 15 minutes, maximum 480 minutes.
## Runtime Contract
The Activity plugin contributes and registers this command:
```text
verstak.activity.suggestWorklog
```
Arguments:
```json
{
"workspaceRootPath": "Project"
}
```
`workspaceRootPath` is optional. When omitted, the command returns suggestions
for all available activity streams.
Result:
```json
{
"suggestions": [
{
"suggestionId": "worklog:Project:2026-06-27",
"workspaceRootPath": "Project",
"date": "2026-06-27",
"title": "Project work on 2026-06-27",
"summary": "Example Article; Saved note",
"minutes": 30,
"eventIds": ["capture-1", "note-1"]
}
]
}
```
If the command cannot read settings, it returns an empty suggestions array and
does not throw for normal UI consumption.
## UI
Activity shows a compact suggestions band above the event list when suggestions
exist. The band is informational only in this slice. It exposes stable DOM data
attributes for smoke tests and later Journal wiring:
- `data-activity-section="worklog-suggestions"`
- `data-worklog-suggestion="<suggestionId>"`
## Testing
`scripts/smoke-activity-plugin.js` must verify:
- the manifest requests `commands.register`;
- the manifest declares `verstak.activity.suggestWorklog`;
- Activity registers the command when mounted;
- workspace Activity renders a worklog suggestion for multiple events;
- the command returns the same suggestion shape;
- global Activity groups suggestions by workspace and day;
- clearing Activity clears suggestions.

View File

@ -1,97 +0,0 @@
# Binary File Write And Browser Attachments Design
## Purpose
Browser Inbox still cannot complete binary file attachment conversion because
the public Files API can read bounded bytes but cannot write them. This slice
adds a bounded public `files.writeBytes` contract and uses it to convert browser
file captures that contain base64 data.
The goal is not streaming or large-file import. It is a safe, testable binary
write path for small attachments that keeps plugins on public APIs.
## Scope
This slice adds:
- `api.files.writeBytes(relativePath, dataBase64, options)` for enabled plugins
with `files.write`;
- desktop service support for atomic bounded byte writes up to 8 MB;
- sync op payload support for `dataBase64`, while preserving older text
`content` payloads;
- SDK type/mock/test coverage for `files.writeBytes`;
- browser extension file capture that sends `file.dataBase64` for all selected
files up to 8 MB and keeps `file.text` only for text-compatible files;
- local receiver flattening of `fileDataBase64`;
- Browser Inbox conversion that prefers `api.files.writeBytes` when binary data
is present and falls back to `api.files.writeText` for text-only captures.
It does not add chunked streaming, folders, drag-and-drop from pages, download
interception, or binary preview changes.
## Files API Contract
`files.writeBytes(relativePath, dataBase64, options)`:
- accepts canonical vault-relative slash paths;
- rejects traversal, absolute paths, `.verstak`, symlinks, folders, missing
parents, and conflicts using the same path policy as `writeText`;
- rejects invalid base64;
- rejects decoded payloads over `MaxBinaryReadBytes` (8 MB);
- writes atomically through a temp file in the target directory;
- requires `files.write`;
- records the same `file.changed` activity shape as text writes;
- records sync create/update payloads as `{ "path": "...", "dataBase64": "..." }`.
Remote sync apply supports both:
- old text payloads: `{ "path": "...", "content": "..." }`;
- new byte payloads: `{ "path": "...", "dataBase64": "..." }`.
## Browser Capture Contract
For `kind: "file"`, the extension sends:
```json
{
"file": {
"name": "photo.png",
"mime": "image/png",
"size": 1234,
"dataBase64": "iVBORw0KGgo...",
"text": ""
}
}
```
`file.name` is required. A file capture is valid when either `file.dataBase64`
or `file.text` is present. The extension rejects selected files over 8 MB before
sending.
## Browser Inbox Conversion
Browser Inbox stores both `fileText` and `fileDataBase64`. `Create File` writes:
- `api.files.writeBytes(path, fileDataBase64, { createIfMissing: true,
overwrite: false })` when binary data is present;
- otherwise `api.files.writeText(path, fileText, { createIfMissing: true,
overwrite: false })`.
On success it publishes `browser.capture.converted` with
`conversionType: "file"` and removes the capture. On write failure it leaves the
capture in the queue and renders an error.
## Testing
Required checks:
- core Files service writes bytes, rejects invalid/oversized payloads, and
preserves conflict/path behavior;
- desktop API bridge enforces `files.write`, records sync `dataBase64`, and
applies remote binary payloads;
- frontend plugin API exposes `files.writeBytes`;
- SDK types and mock API include `writeBytes`;
- browser extension protocol builds, validates, and queues binary captures;
- receiver accepts file captures with `dataBase64`;
- Browser Inbox smoke proves binary conversion uses `writeBytes`;
- full verification scripts pass for touched repos.

View File

@ -1,50 +0,0 @@
# Browser Inbox Conversion Activity Design
## Purpose
Browser Inbox now publishes `browser.capture.converted` when a capture becomes
a Markdown note. Activity should record that conversion through the same public
event subscription model it already uses for file, note, workspace, and browser
capture events.
## Scope
This slice wires one public event into `verstak.activity`:
- subscribe to `browser.capture.converted`;
- declare the event in the Activity plugin `activityProviders` contribution;
- store and render conversion activity in the existing workspace/global streams;
- include conversion events in worklog reconstruction.
It does not add a direct Browser Inbox to Activity API, special conversion UI,
or new desktop core behavior.
## Event Shape
Browser Inbox emits:
```js
{
captureId: "capture-id",
conversionType: "note",
notePath: "Project/Notes/Example.md",
workspaceRootPath: "Project",
title: "Example",
url: "https://example.com",
sourcePluginId: "verstak.browser-inbox"
}
```
Activity stores the event using its normal event normalization. The title should
come from `payload.title`; summary should naturally include the best available
payload text such as `notePath` through the existing summary logic.
## Testing
`scripts/smoke-activity-plugin.js` must prove:
- `browser.capture.converted` is subscribed by the Activity frontend;
- the Activity manifest advertises the event in `activityProviders`;
- a workspace-scoped conversion event is stored under that workspace key;
- the rendered Activity view includes the conversion title and event type;
- worklog suggestions include the conversion event id.

View File

@ -1,74 +0,0 @@
# Browser Inbox Create Link Conversion Design
## Purpose
Browser captures should be convertible into a lightweight link artifact without
forcing every URL into Notes. This slice adds a public Files API conversion that
creates an ordinary human-readable `.url` file in the workspace.
## Scope
This slice adds one conversion action inside `verstak.browser-inbox`:
- render `Create Link` for captures that have both `workspaceRootPath` and a
URL;
- write a `.url` shortcut file to `<workspaceRootPath>/Links/<safe-title>.url`
through `api.files.writeText`;
- publish `browser.capture.converted` with `conversionType: "link"`;
- remove the converted capture from the inbox queue after a successful write;
- leave the capture in place and show an error if the write fails.
It does not add a core link entity model, a link resolver plugin, bulk
conversion, or file attachment capture.
## Link File Format
The generated file is a plain text Internet shortcut:
```ini
[InternetShortcut]
URL=https://example.com/article
```
The plugin writes with:
```js
api.files.writeText(linkPath, content, {
createIfMissing: true,
overwrite: false
})
```
This preserves explicit conflict behavior. The plugin must not silently append a
suffix when a link file already exists.
## Event Contract
After a successful conversion the plugin publishes:
```js
api.events.publish('browser.capture.converted', {
captureId,
conversionType: 'link',
linkPath,
workspaceRootPath,
title,
url,
sourcePluginId: 'verstak.browser-inbox'
})
```
Activity can record this through the already-supported
`browser.capture.converted` event.
## Testing
`scripts/smoke-browser-inbox-plugin.js` must prove:
- a workspace capture with a URL renders `Create Link`;
- clicking it writes the `.url` file through `api.files.writeText`;
- the write uses `createIfMissing: true` and `overwrite: false`;
- the capture is removed from its queue after success;
- `browser.capture.converted` is published with `conversionType: "link"`;
- when the write rejects, the capture remains in the queue and an error status
is rendered.

View File

@ -1,99 +0,0 @@
# Browser Inbox Create Note Conversion Design
## Purpose
Browser Inbox should turn a captured page, selection, or link into useful vault
material without knowing Notes internals. This first conversion slice creates an
ordinary Markdown note through the public Files API.
## Scope
This slice adds one conversion action inside `verstak.browser-inbox`:
- render `Create Note` for captures that have a `workspaceRootPath`;
- write a Markdown file to `<workspaceRootPath>/Notes/<safe-title>.md` using
`api.files.writeText`;
- publish `browser.capture.converted` after a successful write;
- remove the converted capture from the inbox queue;
- leave the capture in place and show an error if the write fails.
It does not add a separate Notes plugin API, link entity model, file attachment
conversion, bulk conversion, or Activity/Journal-specific UI. Other plugins can
react to the published conversion event through the public event bus.
## Note Path And Content
The generated note filename is a human-readable safe projection of the capture
title:
1. Prefer capture `title`.
2. Fall back to capture `domain`.
3. Fall back to capture `captureId`.
4. Replace unsafe filename characters with `_`.
5. Append `.md`.
The plugin writes with:
```js
api.files.writeText(notePath, markdown, {
createIfMissing: true,
overwrite: false
})
```
This preserves explicit conflict behavior. The plugin must not silently append a
suffix when a note already exists.
Markdown content:
```md
# <title>
Source: <url>
Captured: <capturedAt>
Kind: <kind>
<captured text when present>
```
Missing URL or text sections are omitted.
## Event Contract
After a successful conversion the plugin publishes:
```js
api.events.publish('browser.capture.converted', {
captureId,
conversionType: 'note',
notePath,
workspaceRootPath,
title,
url,
sourcePluginId: 'verstak.browser-inbox'
})
```
The event is informational. Browser Inbox does not require Activity to be
installed and does not directly call Activity, Notes, Journal, or Search.
## Permissions
`verstak.browser-inbox` must add:
- `files.write`;
- `events.publish`.
Existing permissions remain unchanged.
## Testing
`scripts/smoke-browser-inbox-plugin.js` must prove:
- a workspace capture renders `Create Note`;
- clicking it writes the Markdown note through `api.files.writeText`;
- the write uses `createIfMissing: true` and `overwrite: false`;
- the capture is removed from its queue after success;
- `browser.capture.converted` is published with `conversionType: "note"`;
- when the write rejects, the capture remains in the queue and an error status
is rendered.

View File

@ -1,74 +0,0 @@
# Browser Inbox Domain Binding Design
## Purpose
Browser captures often arrive while the user is not focused on the workspace
that should receive them. Domain binding routes unscoped browser captures into a
workspace queue by matching the capture domain to a plugin-owned binding table.
This keeps Browser Inbox behavior in the official plugin. Desktop core still
only publishes browser capture events and may annotate the current workspace
when it knows one.
## Scope
This slice implements domain-to-workspace routing inside `verstak.browser-inbox`:
- store bindings in the Browser Inbox plugin settings namespace;
- match capture domains case-insensitively;
- route only captures that do not already include `workspaceRootPath`;
- preserve the global aggregate view;
- keep the binding model independent from Notes, Files, Activity, and Journal.
It does not implement conversion into notes/links/files/activity, browser UI for
editing bindings, or a desktop core domain binding API.
## Settings Contract
The plugin reads `domainBindings` from its settings object:
```json
{
"domainBindings": {
"example.com": "Project",
"client.example.com": "ClientA"
}
}
```
Keys are hostnames. Values are top-level `workspaceRootPath` strings.
Normalization rules:
- trim whitespace around keys and values;
- lowercase domains for matching;
- strip one or more leading dots from binding keys;
- ignore empty domains and empty workspace roots.
## Routing Rules
For each incoming `browser.capture.page`, `browser.capture.selection`, or
`browser.capture.link` event:
1. If payload already has `workspaceRootPath`, keep it unchanged.
2. Otherwise, derive a domain from `payload.domain` or `payload.url`.
3. If an exact normalized domain binding exists, set `workspaceRootPath` and
`workspaceName` to the bound workspace root before storage.
4. If no binding exists, keep current behavior and store the capture in the
receiving view scope.
Subdomain fallback is intentionally out of scope for this first slice. A
binding for `example.com` does not match `docs.example.com` until a later design
adds explicit wildcard or suffix semantics.
## Testing
`scripts/smoke-browser-inbox-plugin.js` must prove:
- an unscoped capture with `domain: "client.example.com"` and a
`domainBindings` entry for that domain is stored under
`captures:workspace:ClientA`;
- the routed capture appears in the ClientA workspace view;
- the global view still aggregates the routed capture;
- an event that already includes `workspaceRootPath: "Project"` is not
overwritten by a domain binding for another workspace.

View File

@ -1,112 +0,0 @@
# Browser Inbox Text File Attachment Design
## Purpose
Browser Inbox needs a first file attachment path that uses the current public
plugin API instead of adding a private core inbox or binary write shortcut. This
slice supports text file attachments end to end: the browser extension reads a
user-selected text file, the local receiver publishes a `browser.capture.file`
event, and Browser Inbox converts the capture into an ordinary workspace file
through `api.files.writeText`.
Binary attachments remain out of scope until the platform has an explicit
public binary write API.
## Scope
This slice adds:
- extension protocol support for `kind: "file"`;
- popup file selection for text files;
- local receiver validation and event publication for file captures;
- Browser Inbox storage/rendering of file metadata and text content;
- a `Create File` conversion action for workspace-scoped text file captures.
It does not add drag-and-drop from web pages, screenshot capture, native download
interception, binary files, folders, or a core `.verstak/inbox` staging area.
## Capture Contract
The extension sends schema version 1 with a new file object:
```json
{
"schemaVersion": 1,
"captureId": "generated-id",
"capturedAt": "2026-06-29T00:00:00.000Z",
"source": "verstak-browser-extension",
"kind": "file",
"page": {
"url": "https://example.com/current-page",
"title": "Current page",
"domain": "example.com"
},
"file": {
"name": "notes.txt",
"mime": "text/plain",
"size": 120,
"text": "file contents"
},
"browser": {
"name": "Firefox"
}
}
```
`file.name` and `file.text` are required for `kind: "file"`. The extension
limits text content to 2 MB so it stays within the current Files API text-read
size and avoids turning local receiver events into large binary transport.
## Receiver Event
The desktop receiver accepts `kind: "file"` and publishes
`browser.capture.file`. The event payload flattens file fields for Browser Inbox:
```js
{
captureId,
capturedAt,
source,
kind: 'file',
url,
title,
domain,
fileName,
fileMime,
fileSize,
fileText,
browserName
}
```
Current workspace annotation continues to work the same as page, selection, and
link captures.
## Browser Inbox Conversion
Browser Inbox subscribes to `browser.capture.file` and stores file fields in its
plugin-owned queue. In a workspace view, file captures render a `Create File`
action.
On success:
- write `<workspaceRootPath>/Files/<safe-file-name>` through
`api.files.writeText`;
- use `{ createIfMissing: true, overwrite: false }`;
- publish `browser.capture.converted` with `conversionType: "file"` and
`filePath`;
- remove the capture from the queue.
On write failure, Browser Inbox leaves the capture in the queue and renders an
error status.
## Testing
Required checks:
- browser extension protocol test builds and validates `kind: "file"`;
- receiver unit test accepts a file capture and publishes
`browser.capture.file`;
- Browser Inbox smoke test verifies `Create File` write path, content, write
options, queue removal, converted event, and failure behavior;
- full repo checks continue to pass for touched repositories.

View File

@ -1,113 +0,0 @@
# Browser Receiver Pairing Design
## Purpose
The browser extension sends captures to the desktop local receiver with
`X-Verstak-Receiver-Token`. The desktop runtime starts that receiver only in
paired mode, without moving Browser Inbox queues or conversion behavior into
desktop core.
## Scope
This slice covers only the local receiver permission/pairing model:
- generation and local persistence of a receiver token;
- receiver token validation and rotation without a server restart;
- bounded capture payload validation before an event is published;
- Browser Inbox and extension settings for transferring the token.
It does not implement browser UI for pairing QR codes or encrypted keyring
storage. The current token is kept in the installation-local app settings file,
which the manager writes with mode `0600`.
## Model
The production receiver always starts in paired mode. On startup it generates a
32-byte random token when none exists, stores it outside the vault in
`~/.config/verstak/config.json`, and does not log it. If that token cannot be
persisted, the local receiver is disabled rather than opened without a token.
Every capture request must include:
```text
X-Verstak-Receiver-Token: <token>
```
The receiver compares the supplied token to the configured token using a
constant-time comparison. It does not publish browser capture events when the
token is missing or wrong. The package keeps the open constructor for embedded
legacy callers, but `main.go` does not use it.
## HTTP Contract
Endpoint:
```text
POST /api/browser-inbox/v1/captures
```
Successful capture:
```http
202 Accepted
```
```json
{ "status": "accepted", "captureId": "capture-id" }
```
Missing token in paired mode:
```http
401 Unauthorized
```
```json
{ "error": "receiver token required" }
```
Wrong token in paired mode:
```http
401 Unauthorized
```
```json
{ "error": "receiver token invalid" }
```
Other validation behavior remains unchanged: invalid payloads return `400`,
missing Browser Inbox consumers return `503`, and non-POST methods return
`405`. Payloads over 12 MiB return `413`; capture text, file metadata, encoded
binary data, and decoded file content are capped before publication.
## Runtime API
Desktop core exposes the model inside `internal/core/browserreceiver`:
```go
type Options struct {
RequireToken bool
ReceiverToken string
}
func NewWithOptions(bus *events.Bus, options Options, providers ...WorkspaceProvider) *Receiver
```
`New(bus, providers...)` remains the open legacy constructor.
The Wails bridge exposes `PluginBrowserReceiverPairing` and
`PluginRotateBrowserReceiverToken` only to plugins declaring the dangerous
`browser.receiver.manage` permission. `verstak.browser-inbox` presents the
receiver URL, token copy action, and rotation control; the browser extension
stores the values in its local settings.
## Testing
`internal/core/browserreceiver/receiver_test.go` must prove:
- paired receivers reject missing tokens with `401`;
- paired receivers reject wrong tokens with `401`;
- paired receivers accept correct tokens and publish the capture event;
- a token persists across settings reload and changes immediately on rotation;
- oversized or malformed capture payloads do not publish an event;
- Browser Inbox and extension smoke tests cover the settings transfer path.

View File

@ -1,68 +0,0 @@
# Journal Worklog Plugin Design
## Purpose
Add the first official Journal plugin as a replaceable plugin, not a desktop
core feature. It must give users a visible worklog surface and consume Activity
worklog suggestions through the command runtime added in
`verstak.activity.suggestWorklog`.
## Scope
This slice creates `verstak.journal` with workspace-scoped worklog storage,
manual entry creation, and one-click import from Activity suggestions.
It does not implement billing reports, invoice export, timers, sync-specific
conflict handling, or cross-workspace report aggregation.
## Behavior
- Journal appears as both a sidebar view and a workspace item.
- In a workspace, entries are stored under a workspace settings key.
- In the global view, Journal aggregates entries from every workspace key.
- A manual entry contains:
- `entryId`;
- `workspaceRootPath`;
- `date`;
- `title`;
- `summary`;
- `minutes`;
- `billable`;
- optional `sourceSuggestionId`.
- Importing Activity suggestions calls:
```text
api.commands.executeFor(
'verstak.activity',
'verstak.activity.suggestWorklog',
{ workspaceRootPath }
)
```
- Imported suggestions become non-billable entries by default.
- Import is idempotent by `sourceSuggestionId`: importing the same Activity
suggestion twice does not create duplicates.
- If Activity is unavailable, Journal stays usable for manual entries and shows
a status message.
## Storage
Workspace entries use settings keys:
```text
worklog:workspace:<encoded workspace root>
```
Global aggregation reads all settings keys with the `worklog:workspace:`
prefix.
## Testing
`scripts/smoke-journal-plugin.js` must verify:
- manifest identity, capabilities, optional Activity dependency, and UI
contributions;
- the Journal view mounts;
- manual entry creation stores and renders an entry;
- Activity import calls the command runtime and stores suggestions as entries;
- importing the same suggestion twice does not duplicate entries;
- global Journal aggregates workspace entries.

View File

@ -1,331 +0,0 @@
# Persistent Search Index And Runtime Provider Hosting Design
## Context
Phase 4 requires `verstak.search` to move beyond live recursive scans:
- keep search as an official plugin, not a core feature;
- persist a workspace-scoped search index;
- host `searchProviders` contributed by other plugins at runtime;
- preserve the local-first, readable vault model;
- avoid copying code or architecture from the old Verstak repository.
Current implementation status:
- `verstak.search` is a workspace item and contributes `searchProviders`;
- it searches while typing by walking files through `api.files.list`;
- it reads text-like files through `api.files.readText`;
- it opens file results through `api.workbench.openResource`;
- the contribution registry already exposes `searchProviders`;
- `api.commands.executeFor(pluginId, commandId, args)` already hosts frontend
provider handlers for other contribution types;
- desktop backend already has plugin-scoped JSON data methods
`ReadPluginDataJSON` and `WritePluginDataJSON`, but the frontend plugin API
currently exposes only `settings`.
UX direction:
- the primary search entry point belongs in the workspace header next to the
workspace title;
- `verstak.search` remains the owner of indexing and provider hosting;
- a standalone Search workspace item may stay as an expanded results surface,
but it is not the primary entry point.
No visual companion is needed for this design because the decision is about
runtime contracts and data flow, not layout.
## Assumptions
- The next implementation should be a small reversible platform step.
- Search remains replaceable plugin functionality; core should expose generic
storage and contribution execution only.
- `searchProviders[].handler` is treated as a command id. A provider plugin
must declare that command in `contributes.commands` and register its frontend
handler with `api.commands.register`.
- The initial persistent index is JSON-backed plugin data, not SQLite FTS and
not a background sidecar.
- The index is an optimization and discovery layer. User files remain the
source of truth.
## Alternatives Considered
### Recommended: plugin-owned index plus command-backed provider hosting
`verstak.search` owns its local index in plugin storage, uses public Files API
and events to keep it fresh, and fans out to contributed providers with
`api.commands.executeFor`.
Trade-offs:
- smallest core/runtime change;
- matches existing Files/Notes contribution execution pattern;
- keeps user-facing search outside core;
- JSON index is simpler than full text search but sufficient for the current
roadmap item.
### Core search service
Desktop core would own indexing and execute provider searches.
Trade-offs:
- easier to centralize later ranking and indexing;
- violates the current direction that search is a plugin-level user feature;
- makes core understand official search semantics too early.
### Sidecar or SQLite FTS indexer now
Introduce a dedicated indexer process or SQLite FTS schema immediately.
Trade-offs:
- better long-term scalability;
- too large for the current milestone;
- adds migration, lifecycle, and sync/cache policy decisions before the basic
provider runtime contract is proven.
## Chosen Design
Use the recommended approach.
`verstak.search` becomes both:
- the workspace search runtime and expanded results UI;
- the runtime host for all enabled `searchProviders`.
The shell may render the compact input in the workspace header, but it should
call into the Search plugin/runtime contract rather than implement search
semantics in core.
The desktop and SDK expose a generic frontend storage surface:
```ts
api.storage.data.read(name: string): Promise<Record<string, unknown>>
api.storage.data.write(name: string, data: Record<string, unknown>): Promise<void>
```
This maps to existing plugin-scoped backend methods. It requires
`storage.namespace`, follows current plugin ownership rules, and does not let a
plugin read another plugin's namespace.
The Search plugin stores its persistent index as plugin data named
`search-index`. Settings remain for user preferences only.
## Provider Runtime Contract
`searchProviders` keep their current manifest shape:
```json
{
"id": "verstak.search.vault-text",
"label": "Vault Text Search",
"handler": "verstak.search.searchVaultText"
}
```
The `handler` value must name a command declared by the same plugin:
```json
{
"contributes": {
"commands": [
{
"id": "verstak.search.searchVaultText",
"title": "Search Vault Text",
"handler": "searchVaultText"
}
],
"searchProviders": [
{
"id": "verstak.search.vault-text",
"label": "Vault Text Search",
"handler": "verstak.search.searchVaultText"
}
]
}
}
```
The provider registers the command handler at mount time:
```js
api.commands.register('verstak.search.searchVaultText', searchVaultText)
```
Runtime availability:
- this milestone does not auto-start unloaded frontend bundles or sidecars;
- a provider is executable only after its command handler is registered in the
current frontend runtime;
- declared but unregistered providers are skipped and reported as unavailable;
- later sidecar/background activation can extend this without changing the
provider manifest shape.
Provider input:
```ts
{
source: 'search',
providerId: string,
query: string,
workspaceRootPath: string,
limit: number
}
```
Provider output:
```ts
{
results: SearchResult[]
}
```
`SearchResult`:
```ts
{
id?: string,
path?: string,
title?: string,
snippet?: string,
matchType?: string,
providerId?: string,
providerLabel?: string,
type?: 'file' | 'folder' | 'activity' | 'worklog' | string,
openable?: boolean,
line?: number,
score?: number,
resource?: {
kind: 'vault-file',
path: string,
mode?: 'view' | 'edit'
}
}
```
The Search host normalizes missing optional fields. Invalid provider responses
are ignored with a visible status warning; they do not fail the whole search.
## Persistent Index Shape
The first index version is intentionally small:
```json
{
"version": 1,
"workspaceRootPath": "Project",
"builtAt": "2026-06-29T00:00:00Z",
"entries": [
{
"path": "Project/Docs/case.md",
"name": "case.md",
"type": "file",
"extension": "md",
"size": 1234,
"modifiedAt": "2026-06-29T00:00:00Z",
"text": "short normalized searchable text or snippet"
}
]
}
```
Rules:
- index only the current workspace root;
- store vault-relative slash paths;
- index folders and regular files;
- read content only for text-like files already handled by current Search;
- rely on the host `readText` limit for large text files;
- store short normalized text, not full arbitrary binary data;
- rebuild when version or workspace root differs.
## Index Lifecycle
On mount:
1. Read `api.storage.data.read('search-index')`.
2. If version/root matches, use it immediately.
3. If missing or stale, build an index from `api.files.list` and text reads.
4. Write the built index with `api.storage.data.write`.
5. Subscribe to `file.changed`.
On `file.changed`:
- if event path is outside the workspace, ignore it;
- for create/update, refresh that path's metadata/content if readable;
- for delete/trash, remove the path and descendants;
- for move, remove `fromPath` when present and refresh the new path;
- write the updated index after the change is applied.
If an incremental update fails, mark the index stale in UI and continue serving
the last usable index until rebuild succeeds.
## Search Flow
For a query shorter than two characters, return no results.
For a valid query:
1. Query local persistent index for path, name, folder, and text matches.
2. List enabled `searchProviders` through `api.contributions.list`.
3. Call each provider except duplicates that would recurse into the same
in-flight command.
4. Merge and normalize results.
5. Sort by score, then provider order, then path/title.
6. Render provider label, match type, path/title, snippet, and open action when
the result has a supported resource.
Provider failures are isolated. The status line reports how many providers
failed without hiding successful results.
## Error Handling
- Missing `storage` API: fall back to live scanning and show a degraded status.
- Missing `storage.namespace` permission: Search runs without persistence and
reports degraded persistence.
- Corrupt index JSON: ignore it, rebuild, and overwrite only after a successful
rebuild.
- Provider command not declared or not registered: skip that provider and show a
warning count.
- File read errors: skip that file, matching the current Search behavior.
- Event subscription errors: keep search usable, mark index refresh as manual.
## Testing And Verification
Expected test coverage:
- SDK types include `api.storage.data.read/write` and search result/provider
contracts.
- SDK mock API stores plugin data separately from settings.
- Desktop frontend bridge exposes `api.storage.data.read/write`.
- Desktop bridge smoke test verifies plugin data round-trip through Wails mock.
- Search plugin smoke test verifies:
- persisted index is read before scanning;
- missing/stale index triggers build and write;
- query uses persisted entries;
- `file.changed` updates or removes entries;
- provider fan-out uses `contributions.list('searchProviders')` and
`commands.executeFor`;
- provider failure does not hide local results.
- Roadmap and official plugin docs are updated after implementation.
Manual smoke after implementation:
- start desktop frontend/dev flow used by the repository;
- open Search workspace item;
- search a known text file;
- reload/remount Search and confirm results come from stored index;
- enable a test provider and confirm its results appear with provider label.
## Out Of Scope
- SQLite FTS;
- typo/layout tolerant search;
- binary OCR or PDF extraction;
- background sidecar indexing;
- cross-workspace global search;
- sync policy for index cache;
- journal/worklog/activity reconstruction implementation.
Those remain later roadmap items.

View File

@ -1,384 +0,0 @@
# Platform Localization Design
## Context
Verstak v2 currently renders most desktop shell and official plugin text in
English. Application settings do not store a language, the frontend plugin API
has no localization contract, and plugin manifests expose only literal English
metadata. The sync server already has separate Russian and English catalogs and
is outside the scope of this desktop milestone. The browser extension also has
its own lifecycle and is explicitly deferred to a later milestone.
The first localization milestone covers:
- the `verstak-desktop` shell and core-owned screens;
- every plugin in `verstak-official-plugins`, including `platform-test`;
- the public SDK and manifest contract required by future third-party plugins;
- Russian and English;
- a persisted language selector in the desktop settings menu.
The existing `~/git/verstak` project remains a visual reference only. Its code
and localization architecture are not inputs to this implementation.
## Product Decisions
- The stored language preference is `system`, `ru`, or `en`.
- A new installation defaults to `system`.
- System locales beginning with `ru` resolve to Russian; all other system
locales resolve to English.
- The settings menu offers System, Russian, and English choices.
- Changing the preference updates the running UI without restarting the app.
- The preference is installation-local and is not stored in or synchronized
with a vault.
- Browser extension localization is a separate follow-up.
- Sync server localization remains independent from the desktop preference.
## Alternatives Considered
### Recommended: platform contract with plugin-owned catalogs
The shell owns shell translations. Each plugin owns its catalogs and declares
them in its manifest. The public plugin API exposes the resolved locale,
translation, and locale-change subscription.
Trade-offs:
- preserves repository and plugin ownership boundaries;
- gives third-party plugins the same mechanism as official plugins;
- requires coordinated SDK, desktop, manifest, and plugin changes;
- avoids teaching desktop core the UI vocabulary of official plugins.
### One desktop-wide catalog
Desktop would contain translations for shell and all official plugins.
Trade-offs:
- initially faster;
- couples core releases to plugin text and IDs;
- prevents independently distributed plugins from owning their localization;
- contradicts the dynamic plugin architecture.
### Independent dictionaries without a platform API
Every plugin would read a global value or DOM event and implement translation
on its own.
Trade-offs:
- small initial runtime change;
- inconsistent fallback and interpolation behavior;
- no stable SDK contract for third-party plugins;
- makes live locale changes and manifest metadata unreliable.
## Chosen Architecture
Use the platform contract with plugin-owned catalogs.
Localization is a UI platform service, not a user feature plugin. Desktop owns
language preference resolution, catalog loading, shell translation, and the
generic runtime bridge. It does not own plugin message content.
The data flow is:
```text
app config language preference
|
v
desktop locale store -----> shell catalogs
|
+------------------> localized manifest contributions
|
+------------------> api.i18n for each plugin
|
v
plugin-owned catalogs
```
## Application Setting
Add `Language string` to the desktop application config and expose it as
`language` through `GetAppSettings` and `UpdateAppSettings`.
Accepted persisted values:
```text
system
ru
en
```
Rules:
- missing or empty values load as `system`;
- unknown values are rejected by the update API and never persisted;
- an existing config requires no schema migration beyond applying the missing
default;
- updating language must not reset `devMode`, theme, workbench preferences, or
other unrelated settings;
- `system` is resolved in the frontend from `navigator.languages` or
`navigator.language`, with `en` as the safe fallback.
The settings menu renders language names in their own language so the user can
recover from an accidental switch:
```text
System / Системный
English
Русский
```
The active option is visibly marked and uses menu radio semantics.
## Desktop Localization Service
Add one small frontend localization module that owns:
- the stored preference;
- the resolved locale (`ru` or `en`);
- the English and Russian shell catalogs;
- loaded plugin catalogs keyed by plugin ID and locale;
- a subscription API for Svelte shell components and plugin bridges;
- named parameter interpolation.
The shell-facing API is synchronous after initialization:
```js
getLanguagePreference()
getLocale()
t(key, params?, fallback?)
setLanguagePreference(preference)
subscribe(listener)
loadPluginCatalog(pluginId, localizationConfig)
translatePlugin(pluginId, key, params?, fallback?)
```
Application startup loads the persisted preference before the main interactive
shell is shown. Catalog-loading failures do not prevent startup.
Parameter interpolation supports named placeholders such as `{count}`. Missing
parameters leave the placeholder visible so catalog mistakes are detectable.
Catalog strings are always assigned through existing safe text rendering APIs;
translation values are not treated as HTML.
## Plugin Manifest Contract
Add an optional top-level manifest field:
```json
{
"localization": {
"defaultLocale": "en",
"locales": {
"en": "locales/en.json",
"ru": "locales/ru.json"
}
}
}
```
The field belongs at the top level because manifest metadata and contributions
can be localized even when a plugin has no frontend bundle.
Validation rules:
- `defaultLocale` and locale map keys use lower-case supported locale tags;
- the default locale must have a declared catalog;
- catalog paths must be plugin-relative safe paths;
- absolute paths, traversal, backslashes, and paths outside the plugin root are
rejected;
- localization is optional, preserving compatibility with existing and
third-party plugins.
Catalog files are flat JSON string maps. Official plugins provide both `en`
and `ru` catalogs. The English manifest literals stay in place as readable
fallbacks and for compatibility with hosts that do not implement localization.
## Manifest And Contribution Keys
Desktop derives stable keys rather than embedding translation tokens in fields.
Reserved keys:
```text
manifest.name
manifest.description
contributions.views.<id>.title
contributions.commands.<id>.title
contributions.settingsPanels.<id>.title
contributions.sidebarItems.<id>.title
contributions.fileActions.<id>.title
contributions.noteActions.<id>.title
contributions.contextMenuEntries.<id>.title
contributions.searchProviders.<id>.label
contributions.statusBarItems.<id>.label
contributions.workspaceItems.<id>.title
```
The complete set follows contribution fields defined by the SDK schema. IDs
are used verbatim after the contribution type prefix. Missing keys retain the
literal manifest value.
The desktop localizes copies returned to frontend presentation code; it does
not mutate the backend registry or change IDs, handlers, capabilities, or
permission logic.
## Plugin Runtime API
Extend `VerstakPluginAPI` with:
```ts
i18n: {
getLocale(): 'ru' | 'en';
t(key: string, params?: Record<string, string | number>, fallback?: string): string;
onDidChangeLocale(listener: (locale: 'ru' | 'en') => void): Unsubscribe;
}
```
The desktop preloads a plugin catalog before mounting its component. `t` is
therefore synchronous during rendering. The API automatically disposes locale
subscriptions with the rest of the plugin API.
Official plugin components subscribe once when mounted and rerender only their
text when the locale changes. Locale changes must not clear form values,
selection, dirty editor state, loaded data, or navigation state. The host does
not solve localization by unmounting and remounting plugin components.
Plugins without localization metadata continue to work. Their `t` calls return
the supplied fallback or key, and their literal manifest metadata remains
visible.
## Catalog Loading Boundary
Desktop exposes a dedicated read-only backend method for a declared plugin
catalog. It locates the enabled/discovered plugin, selects only a path declared
by its localization manifest, validates containment within the plugin root, and
returns a parsed string map or an error.
Do not repurpose arbitrary plugin settings or make localization catalogs
writable at runtime. Do not give plugins access to another plugin's catalog.
Catalog errors are isolated:
- malformed selected-locale catalog: fall back to default locale;
- missing default catalog: use manifest literals and key/fallback values;
- one broken plugin catalog: report a diagnostic without affecting other
plugins or the shell.
## Fallback Rules
Shell translation fallback:
```text
resolved locale -> English catalog -> explicit fallback -> key
```
Plugin translation fallback:
```text
resolved locale -> plugin default locale -> explicit fallback -> key
```
Manifest/contribution fallback:
```text
resolved locale -> plugin default locale -> original manifest literal
```
Russian and English catalogs must have the same keys in CI for the shell and
official plugins. Third-party plugins may provide only their default locale.
## Scope Of Translated UI
The first milestone translates:
- vault onboarding and shell navigation;
- status bar, settings menu, command palette, global search chrome, dialogs,
plugin host states, and Plugin Manager;
- all normal, empty, loading, confirmation, warning, and known validation
states in official plugins;
- official plugin names, descriptions, contribution titles, and labels;
- accessibility labels and tooltips owned by shell or official plugins.
Arbitrary backend, operating-system, filesystem, network, and third-party error
messages remain in their source language. The surrounding user-facing prefix
and recovery instruction are translated. Stable known error codes may receive
localized messages in later milestones.
User data, filenames, workspace names, provider values, logs, identifiers, API
names, and developer diagnostics are never translated.
## Testing Strategy
Use TDD for each production change.
### SDK
- manifest schema accepts valid localization declarations;
- schema rejects unsafe or incomplete declarations;
- TypeScript types expose localization metadata and `api.i18n`;
- mock API implements locale reads, translations, and subscriptions;
- interpolation and fallback behavior are covered.
### Desktop backend
- missing language defaults to `system`;
- all three accepted values persist and reload;
- invalid values are rejected;
- language updates preserve unrelated settings;
- catalog reads accept only declared safe paths;
- malformed, missing, and traversal catalog cases fail safely.
### Desktop frontend
- system locale resolution maps `ru-*` to `ru` and others to `en`;
- shell fallback and interpolation are deterministic;
- language menu shows and persists the selected option;
- changing language updates visible shell text without reload;
- plugin API receives the locale and a working catalog;
- contribution labels change without changing contribution identity;
- missing plugin catalogs preserve English manifest literals.
### Official plugins
- every official manifest declares English and Russian catalogs;
- catalog key parity is checked;
- every manifest/contribution localization key is present;
- focused smoke tests exercise English and Russian rendering;
- a mounted component reacts to a locale change without losing its state.
### End-to-end
- first launch with a mocked Russian system locale renders Russian;
- first launch with another locale renders English;
- English -> Russian -> System selection persists through reload;
- shell and representative plain-JS and Svelte plugins switch together;
- plugin enable/disable and failed-plugin isolation still work.
## Rollout Order
1. SDK manifest/types/mock contract.
2. Desktop setting and catalog security boundary.
3. Desktop localization service and language menu.
4. Desktop shell catalog migration.
5. Official plugin manifests, catalogs, and runtime UI migration.
6. Full unit, smoke, build, and mocked-Wails E2E verification.
7. Real Wails/WebKit GUI smoke when the native environment is available.
The work remains split into small commits per repository. Existing unrelated
changes, including generated Wails bindings already present in the desktop
worktree, must not be included.
## Follow-up Work
After this localization milestone:
1. add browser-extension `_locales` catalogs and its independent language
behavior;
2. introduce stable workspace IDs so renames do not orphan plugin scopes;
3. scope sync blobs by tenant/vault and make operation-sequence writes
transactional;
4. make concurrent settings updates lossless;
5. remove remaining hard-coded official plugin IDs and schemas from shell;
6. reduce drift between E2E mock bundles and real plugin packages;
7. include whole-workspace entries in global Trash where appropriate.

View File

@ -1,510 +0,0 @@
# First Alpha Product UX Design
## Status and scope
This is the approved product-UX tranche for the first public alpha. It covers
the Desktop application, official Activity, Journal and Browser Inbox plugins,
and the browser extension. Release packaging, public repository documentation,
licensing, and sync-server hardening are specified separately in the
`alpha-release` tranche.
The user-visible Russian term is **Дело**. Existing platform and storage names
such as `workspaceRootPath` remain internal compatibility details in this
tranche.
## Durable identity of a Дело
Every managed Дело has an immutable UUID v4 `workspaceId`. It is the identity for
relations; `workspaceRootPath` is only the current or historical filesystem
address and presentation label. Inbox assignments, domain bindings, Activity
events/sessions/candidates, Journal source references, and Overview state store
`workspaceId` as their primary relation key and retain a path only as a cached
display value.
The UUID lives in a small immutable marker inside the case folder,
`<Дело>/.verstak/workspace.json`, and is also indexed in Desktop metadata. The
inside-folder marker survives a Desktop rename, Trash move, restore, and an
external filesystem rename. It prevents a newly created folder with the same
path from inheriting old links.
On first alpha startup, each writable legacy or externally created top-level
case without a marker receives a new UUID and its path-keyed relation data is
migrated to that UUID. If a folder is not writable, it remains viewable but
cannot be selected as a durable relation target until its marker can be
created. If two active folders contain the same UUID (for example after a
filesystem copy), Desktop shows an identity-repair action and does not
automatically attach Inbox, binding, or Activity data to either duplicate.
The repair UI asks which folder retains the existing identity; it generates a
new marker UUID for the other folder and leaves old relations with the retained
identity. It never silently merges the two folders' histories.
Workspace lifecycle events carry both `workspaceId` and current path. A rename
updates only the path cache. Trash and restore retain the same ID and use the
trash ID only to match the particular trash operation. When a folder is removed
outside Desktop, its relations retain the UUID and become unavailable; a new
folder at the old path has a newly generated UUID and cannot take them over.
This document supersedes conflicting decisions in the following older narrow
designs:
- `2026-06-29-activity-worklog-suggestions-design.md` for candidate lifecycle
and background availability;
- `2026-06-29-browser-inbox-domain-binding-design.md` for the relation between
bindings, global Inbox, and browser activity;
- `2026-07-11-platform-localization-design.md` only where it would leave
developer-facing plugin IDs visible in the normal user interface.
## Product decisions
- A new Дело is created **only** by an explicit user action. Browser Inbox,
Activity, and Journal never create a case, candidate case, or implicit case.
- A manual extension send is a Browser Inbox capture. Passive browsing activity
is never an Inbox capture.
- Passive browser activity records only a normalized hostname and measured
duration. It never records a page URL, title, selection, page content,
keystrokes, or browsing history.
- Passive browser tracking is disabled on a new installation. It starts only
after explicit, informed consent in extension settings.
- Passive browser activity counts only the active tab in a focused browser
window. Background tabs and unfocused browser windows contribute no time.
- Activity can suggest a Journal record, but the user reviews and saves it.
Journal entries are never created automatically.
- An unassigned activity can be saved only after the user picks an existing
Дело. Picking it for a single Journal entry does not silently create a domain
binding.
- The first alpha retains one optional Дело assignment per Browser Inbox
capture, matching current product semantics. It does not introduce sharing a
capture between several cases.
## Browser extension: manual capture and passive activity
### Manual capture
The existing explicit popup/context-menu actions remain the only way to create
`browser.capture.*` events and Browser Inbox records. Their protocol and
retry queue remain separate from passive activity data.
### Domain activity tracker
Passive tracking is an opt-in extension setting, `passiveActivityEnabled`, with
a default of `false`. The first extension settings view includes a concise
consent card and an unchecked switch. It states that the feature sends only a
normalized hostname and duration, and explicitly states that it does **not**
collect or send URLs, titles, page content, selections, keystrokes, or browser
history. The same explanation remains next to the switch after onboarding.
Until the user enables that switch, the extension does not subscribe to
tracking events, create activity state, or send activity records. Disabling it
stops tracking immediately, clears only the mutable unflushed accumulator, and
leaves already acknowledged Desktop activity unchanged. The user can either
retry or discard already-created pending batches through an explicit settings
action; disabling tracking never silently loses them.
When enabled, the extension uses a persisted tracker:
1. When an HTTP(S) page becomes the active tab of a focused browser window,
start timing its normalized lowercase hostname.
2. On tab activation, hostname change, window focus loss, browser idle/lock,
or a five-minute alarm, write a checkpoint and calculate elapsed time. The
idle detection threshold is explicitly ten minutes; a locked state pauses
immediately.
3. Ignore browser-internal pages, invalid URLs, and excluded hostnames. An
exclusion `youtube.com` matches that hostname and every subdomain; the same
rule applies to `x.com`.
4. On a flush, freeze one record per hostname as a `browser.activity.domain`
batch. The payload contains a schema version, idempotency ID, observed
period bounds, hostname, and `durationSeconds`; it contains no URL-like
field.
The persisted state has three distinct parts:
```text
activeAccumulator: mutable, unsent duration and bounds by hostname
pendingBatches: immutable payloads, keyed by idempotency ID
acknowledgedIds: bounded recent acknowledgement IDs
```
Flushing copies a hostname's current accumulator into a new immutable pending
batch with a newly generated ID, then clears only that copied accumulator. New
time for the same hostname accumulates in `activeAccumulator` and can become a
later batch. It never mutates an already sent batch. Retries send the stored
payload byte-for-byte. A successful acknowledgement removes only the matching
pending batch and records its ID; it cannot remove newer time. Pending batches
are sent oldest first. `acknowledgedIds` is a 30-day bounded LRU set used to
handle replayed acknowledgement messages safely; Desktop maintains its own
idempotency store as the authority.
The tracker uses timestamp arithmetic but applies a conservative ambiguity
limit. A checkpoint contributes elapsed time only if wall-clock time is
monotonic and the gap from the previous trustworthy checkpoint is no more than
ten minutes. A negative clock delta, a gap over ten minutes, an idle/locked
state, or a browser startup after a crash discards the ambiguous interval and
establishes a fresh baseline. WebExtensions have no portable system
suspend/resume event, so the first post-suspend observation is deliberately
handled by this gap rule; platform idle/lock events add an earlier pause where
available. This can undercount ambiguous work but cannot turn an overnight
sleep or a clock change into working time. A browser startup never carries an
active interval across process death; pending batches and accumulated completed
intervals are retained.
The extension persists timestamps and the three state parts in local extension
storage, so a Manifest V3 service-worker restart can continue safely. It sets
the `idle` API detection interval to ten minutes. Its manifest gains the
`windows`, `alarms`, and `idle` permissions needed for this flow; existing tab
access is retained. Firefox uses the equivalent WebExtension events when
available and otherwise applies the same ten-minute checkpoint limit.
The settings page gets an **Excluded domains** list. It accepts one hostname per
item through the canonical normalization below, and explains that a hostname
excludes its subdomains. The default list is empty. Adding an exclusion stops
the matching active measurement immediately and discards only its mutable
unflushed time; immutable pending batches remain available for the user's
explicit retry or discard decision.
### Canonical hostname normalization
Every extension event, exclusion, and Desktop domain binding uses
`hostname-normalization-v1`. It is a normative shared contract, not an
implementation detail of one component:
1. An activity source must be an HTTP(S) URL. Its port, user info, path, query,
and fragment are discarded before hostname normalization. A binding or
exclusion must be a bare hostname; schemes, paths, queries, fragments, and
ports are rejected.
2. Trim surrounding whitespace, lowercase, and remove exactly one DNS root
trailing dot. `example.com.` therefore becomes `example.com`.
3. Convert DNS names using non-transitional UTS #46 / IDNA lookup processing to
an ASCII A-label. The canonical stored and compared form of `пример.рф` is
its punycode A-label; the settings UI may render the Unicode display form.
4. IPv4 addresses are accepted in canonical dotted-decimal form. IPv6 literals
are accepted (bracketed only at URL/settings input), stored without brackets
in canonical lower-case RFC 5952 form, and never include a port. `localhost`
and syntactically valid single-label internal names are also accepted.
5. Reject empty values, malformed IP literals, invalid labels, empty labels,
labels over 63 ASCII bytes, DNS names over 253 ASCII bytes, and any input
that fails URL/IDNA parsing. Invalid activity is not counted; invalid
settings input gets an inline validation error and is not saved.
The SDK owns a versioned `hostname-normalization-v1` test-vector corpus. Desktop
and the browser extension vendor the byte-identical corpus in their tests; the
coordinated `build-all` verification checks its hash. Go and JavaScript have
separate implementations, but both must pass every vector, including trailing
dots, Unicode/punycode equivalence, IPv4, IPv6, localhost, internal names,
ports, malformed input, and excessive lengths.
### Desktop receiver
Desktop exposes an authenticated activity receiver separate from the capture
receiver. It normalizes and validates a hostname with
`hostname-normalization-v1`, validates a positive bounded duration, ISO time
fields, schema version, and idempotency ID before it publishes
`browser.activity.domain`. Invalid and duplicate records do not enter Activity.
The existing local pairing token gates the endpoint; the token is never placed
in Activity storage or UI.
The receiver annotates a valid activity with an existing exact hostname-to-Дело
binding when one exists. A binding stores `workspaceId` as identity and the
current root path as display cache. Bindings remain explicit:
`client.example.com` does not imply `example.com` or the reverse. This is
deliberately different from the exclusion-list suffix rule. Unbound activity is
stored with the explicit `unassigned` session scope.
## Activity and Journal
### Background processing
Official plugins gain a lifecycle-safe background-service contribution. It is
loaded when the plugin host starts, not only while the plugin view is mounted.
The Activity service subscribes once to public events, normalizes and persists
them, rebuilds candidates, and releases subscriptions on host teardown.
The platform provides unsubscribe-capable event subscriptions. Command
registration is owned by the background service so commands remain available
without opening the Activity view.
Raw Activity data is not held in `settings.json`. The Desktop storage layer
provides a lifecycle-safe, plugin-scoped append-only event log for
`verstak.activity` under plugin data. Appending one event does not rewrite the
whole log. Plugin settings retain only compact preferences; plugin data retains
candidate watermarks and indexes. Appends and compaction are serialized by the
storage layer, preventing lost updates.
### Candidate rules
Activity presents chronological sessions rather than a raw log by default. A
browser activity session displays, for example, `admin.client-site.ru · 1 ч
32 мин`; it exposes no hidden URL/title data.
A logical session has one explicit scope:
```text
{ kind: "workspace", workspaceId, workspaceRootPath } | { kind: "unassigned" }
```
An unassigned session is a normal temporal scope, not an exception. It is shown
in Activity and may open Journal review, where the user must choose an existing
active Дело. That one-time choice does not create a binding. A workspace session
is split only by a different workspace ID, a transition to/from unassigned, a
20-minute idle gap, or 120 minutes of total session span. It is not split
merely because midnight passes. A session is ready when either:
- meaningful events in its session cover at least ten minutes and include at
least two events; or
- it contains one or more browser-domain records totaling at least ten minutes.
`workspace.selected`, `file.opened`, and `note.opened` are diagnostic context,
not meaningful work by themselves.
Duration is normative rather than the wall-clock span from first to last event.
Sort meaningful events by `{occurredAt, activityId}` within a session:
- A browser-domain record contributes its explicit validated
`durationSeconds`.
- A file/note event has no inherent duration. For each adjacent pair of
zero-duration meaningful events, add `min(time difference, 10 minutes)` only
when no explicit-duration browser record lies between that pair in the
ordered session.
- A gap over 20 minutes has already split the session, so it contributes no
implicit duration. The first and final standalone point event each contribute
zero seconds.
- Candidate duration is the sum of explicit browser duration and these implicit
point-event intervals, capped at the 120-minute session maximum. It is never
inferred from the overall first-to-last span.
Thus a note saved at 10:00 and a file changed at 10:09 estimate nine minutes;
the same events at 10:00 and 10:50 form separate zero-duration sessions rather
than a fictional 50-minute block.
Each logical session receives a newly generated immutable UUID `sessionId` at
creation and stores an immutable anchor `{scope, firstSeenAt, firstActivityId}`.
The service stores that ID on each appended event and preserves the anchor
through log compaction. A late event may join a same-scope session only when it
falls within that session's 20-minute boundary; otherwise it starts a new
session. It never recomputes an existing session ID or anchor. Each session
persists an ordered handled watermark
`{ occurredAt, activityId }` and optional state for the latest reviewed slice.
A candidate contains only source events after that watermark:
- **Accepting** a candidate stores its `sessionId` and handled watermark on the
Journal entry. Later activity can create a candidate only for the additional
interval after that watermark and only when that new interval reaches the
normal threshold.
- **Dismissing** consumes the current candidate slice through its watermark.
`A+B` therefore cannot reappear as `A+B+C`; only qualifying new work after
`B` may later be suggested.
- A dismissal is not a permanent ban on future work in the same logical
session. At least ten new meaningful minutes are required before it can be
suggested again.
An event arriving late at or before a handled watermark remains in the raw
diagnostic log but never reopens an accepted or dismissed candidate slice.
An across-midnight logical session remains one candidate. The review displays
its time apportioned by local date, preselects the date containing the largest
share of the duration (ties use the start date), and lets the user choose the
Journal date. This preserves a 23:5000:30 session instead of losing its first
ten minutes to an artificial threshold.
The Journal review action opens the existing journal editor with candidate
duration, date, and a concise domain/event summary. The user may edit all
content and must explicitly save. For global/unassigned activity, the review
requires choosing one existing Дело first. Missing or disabled Journal support
produces a visible, actionable message rather than failing silently.
Activity has two clear views:
- **Сессии** — default, with candidate cards and activity summaries;
- **События** — a secondary diagnostic stream for technical event inspection.
### Retention and deletion
The alpha retains raw Activity events for at most 60 days, 10,000 events, or
8 MiB of log data, whichever limit is reached first. On append and at service
startup, bounded compaction removes the oldest entries until all limits hold;
it does not block the UI. Candidate state is pruned once its session and every
related Journal source watermark are older than 60 days.
Saving a Journal entry does not delete its source Activity events. The Journal
entry retains the compact source session/watermark reference after raw-event
retention removes those events. **Clear activity for this Дело** removes only
that case's raw events, sessions, and candidate state after confirmation.
**Clear all activity** is a separate global confirmed operation.
## Browser Inbox lifecycle
### Record state
A capture remains one canonical record with independent fields:
```text
globalState: active | archived
workspaceId: optional immutable UUID of assigned Дело
workspaceState: unassigned | active | trashed | unavailable | orphaned
workspaceRootPath: optional current or historical Дело path cache
workspaceTrashId: optional stable trash identity
```
Existing records migrate to `globalState: active`. An assignment whose current
path resolves to an active marker receives that marker's `workspaceId`; an
unresolved legacy path becomes `unavailable` rather than attaching to a future
folder of the same name. The current test vault may be discarded, but the
migration is non-destructive for an alpha user vault.
### Actions
- **Assign to Дело** retains the capture in global Inbox and shows it in the
selected Дело's Inbox.
- **Убрать из общих входящих** changes `globalState` to `archived`. It never
removes the assignment, so an assigned capture remains available from its
Дело.
- **Открепить от дела** clears `workspaceId`, `workspaceRootPath`, and
`workspaceTrashId`, and sets `workspaceState` to `unassigned`. If the capture is
still globally active it returns to the global Inbox; otherwise it remains in
archive.
- **Удалить везде** removes the canonical capture and its assignment. It is a
separate destructive action and requires confirmation that names the affected
Дело when assigned.
- **Сохранить ссылку в деле** is available only for a valid HTTP(S) capture URL.
It opens a small save dialog with an editable proposed filename derived from
the capture title, then its hostname, then `Link`. The stem is sanitized for
cross-platform forbidden characters and control characters, limited to 96
characters, and receives `.url`. The file uses InternetShortcut format and
is created through the plugin `files.write` capability under the case's
`Links/` directory, creating that directory first. A name collision never
overwrites silently: the dialog offers an explicit `name (2).url` alternative
or cancellation. Read-only/error cases leave the capture unchanged and show
the failure. Success publishes the normal safe file/link Activity event; the
link is opened through the dedicated URL-opening behaviour below, not by
relying on a Linux `.url` file association.
Bulk actions operate only on the visibly filtered capture set, show the count,
and use archive rather than permanent deletion. Every permanent deletion and
Journal deletion has a cancellation-safe confirmation dialog.
When a Дело is renamed, the Inbox service updates the path cache of captures
and exact domain bindings with that `workspaceId`; no relation is keyed by the
old root path. It does not update a binding that has been manually changed to a
different workspace ID during that operation.
When a Дело goes to Trash, assignments and bindings for its `workspaceId`
become `trashed` with the Desktop trash ID. They remain visible as unavailable
historical context but are not used for automatic routing. A restore event
carrying that trash ID restores the same ID's assignment/binding, including a
restored path changed by a collision. A permanent trash purge changes captures
to unassigned and changes bindings to a visible `orphaned` state that the user
can reassign or remove. Activity remains historical and labels the case as
deleted. If a case disappears by external filesystem change, active references
become `unavailable`, automatic routing is disabled, and the user must
explicitly reassign or remove them; a newly created folder with the same name
has a different `workspaceId` and never steals those references.
### Archive and filters
Global Browser Inbox has **Active**, **Archive**, and **All** status filters;
Active is the default. Search applies within the chosen filter, so Archive is
searchable deliberately through Archive or All rather than unexpectedly
appearing in the normal queue. **Restore to Inbox** changes `globalState` back
to `active`. Archive supports a visible filtered bulk restore with a count and
confirmation. A capture assigned to a Дело remains visible in that Дело's Inbox
regardless of its global archive state, with an Archive badge.
### Opening saved links on Linux
The platform adds a user-initiated `urls.openExternal` capability and API. It
accepts only a validated HTTP(S) URL and opens that URL through the system
browser opener (`xdg-open` in the Linux alpha); it never passes a `.url` file
path to the opener. Browser Inbox uses this capability for **Open link** and
for a saved `.url` after parsing and validating its `URL=` value. The Files
surface recognizes a valid `.url` file and uses the same URL-opening path. This
does not depend on desktop file-association support for InternetShortcut files.
## Alpha interface
- Use Russian product labels consistently: **Дела**, **Входящие**,
**Активности**, and **Журнал**. User-facing dates use the local time zone.
- With a selected Дело, the overview reads only records explicitly scoped to
that Дело; an unscoped global event never leaks into every case. With no
selected Дело, it shows a short prompt to select/create one plus up to five
active unassigned Inbox records; it does not fabricate a blended continuation
feed.
- **Needs attention** shows at most five entries: ready Journal candidates,
active unprocessed Inbox records, and existing urgent Todos, in that order
within their respective priority. An Inbox record is new/needs attention
while it is active and unprocessed, without an arbitrary age cutoff.
- Todo rows are included only when a loaded plugin exposes the `todo.workspace`
capability. A missing or disabled Todo plugin is not an Overview error and
contributes no placeholder rows.
- **Continue work** shows at most four distinct case-scoped entities from the
last 14 days: unfinished Todo, unprocessed capture, and the most recent
note/file/Journal entity. Every item carries `lastMeaningfulAt`; items sort
descending by that value, with an unprocessed capture, Todo, Journal, note,
then file as deterministic ties. Multiple `file.changed` events for the same
entity collapse to one item. Opening an item does not mark it complete. Its
overflow action **Hide recommendation** stores a non-destructive
entity/event dismissal; a later meaningful change to that entity makes it
eligible again.
- **Recent changes** shows at most eight distinct case-scoped records from the
last seven days. Included events are note save/create, file create/rename or
last change per file, saved browser link, and Journal create. Technical
selection/open events are excluded. The empty state says that there were no
changes in that period.
- Empty states give a next action, not an empty pane.
- Clear, delete, and archive actions name their scope and consequences.
Plugin IDs are diagnostic information. Normal tabs display the manifest's
human title, such as `Заметки` or `Изображение`, never
`verstak.default-editor.notes-markdown` or `verstak.file-preview.image`.
Add a persisted **Settings → Debug → Show plugin IDs** preference, defaulting
to false. The Desktop application-specific `--debug` command-line argument
enables the same display only for that run. When either is active, show the ID
adjacent to the human title and in diagnostic errors. Neither mode alters data,
plugin permissions, or release behaviour.
## Error handling and privacy
- A failed extension delivery keeps the accumulated hostname time locally and
reports the pending-batch count and a non-blocking retry state in extension
settings; it never falls back to Browser Inbox capture.
- Receiver authentication/validation errors provide a safe status to the
extension without echoing the pairing token or untrusted payload.
- Activity never manufactures a case from missing assignment data.
- User-visible errors explain the failed action and offer the next safe action.
## Verification
Automated checks must cover:
- browser tracker explicit consent/disable behaviour, focused-tab-only
accounting, exclusions, canonical hostname vectors, immutable pending
batches, acknowledgement-only reset, retry persistence, payload privacy,
negative clock changes, long gaps, restart, lock, and suspend/resume;
- Desktop activity receiver authentication, canonical normalization,
validation, idempotency, binding, and event publication;
- append-only Activity log retention/compaction, background subscription
lifecycle, UUID workspace/unassigned session scopes, point-event duration
calculation, immutable session IDs/late events, handled watermarks,
across-midnight review, Journal handoff, local dates, case-scoped clear, and
missing-Journal feedback;
- assigning, archiving, restoring, unlinking, permanent deletion, `.url`
naming/collision/readonly behaviour, filtered bulk operations, rename, trash
restore/purge, external-workspace unavailability, duplicate-workspace-ID
repair, and direct Linux URL opening without `.url` association;
- normal and debug-mode plugin tab labels;
- the corrected frontend Wails mock, deterministic Overview limits/scoping,
plus the end-to-end flows activity-to-Journal and Inbox-to-Дело.
Manual GUI smoke testing verifies Russian normal-mode labels, hidden plugin
IDs, candidate review, Inbox preservation, and extension domain exclusions in
the Linux Desktop build.
## Out of scope
- automatic creation of a Дело;
- background-tab or browser-history tracking;
- URL/content/title collection for passive activity;
- shared/multi-case captures;
- automated Journal saving, billing, or time-sheet generation;
- browser-extension localization and a general analytics product;
- release packaging, licensing, public GitHub README, and sync-server release
security, which belong to the next alpha-release design.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB