workhorse-ai-mcp 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,8 +11,7 @@ is never the same as *accepted* (the orchestrator verified it).
11
11
 
12
12
  ## Install
13
13
 
14
- The npm package is not published yet — the final name is pending a rebrand.
15
- Once published, add the server to your `.mcp.json`:
14
+ Add the server to your `.mcp.json`:
16
15
 
17
16
  ```json
18
17
  {
@@ -25,7 +24,7 @@ Once published, add the server to your `.mcp.json`:
25
24
  }
26
25
  ```
27
26
 
28
- Until then, run it straight from the monorepo:
27
+ Or run it straight from a checkout:
29
28
 
30
29
  ```json
31
30
  {
@@ -38,8 +37,9 @@ Until then, run it straight from the monorepo:
38
37
  }
39
38
  ```
40
39
 
41
- Data lives in `~/.workhorse/workhorse.db` (override with `WORKHORSE_DB`).
40
+ Data lives in `~/.workhorse-ai/journal.db` (override with `WORKHORSE_DB`).
42
41
  On first start the server creates the directory and the database itself.
42
+ `sync.json` always sits next to the database.
43
43
 
44
44
  ## Connect to the cloud (optional)
45
45
 
@@ -60,6 +60,17 @@ a normalized `<username>-<hostname>`. After that, every journal write is
60
60
  auto-pushed; `sync` forces a push, and `inbox`/`take` pull task intents
61
61
  from the cloud.
62
62
 
63
+ ## Orchestration skill
64
+
65
+ The package ships `skills/workhorse-ai/SKILL.md` (in Russian) — the delegation
66
+ discipline the journal is built around: orchestrator/worker roles, the
67
+ `REPORTED != ACCEPTED` invariant, the mandatory project bootstrap, and the
68
+ working order from `search_precedents` to `accept`.
69
+
70
+ Copy it into your agent's skill directory (for Claude Code:
71
+ `~/.claude/skills/workhorse-ai/SKILL.md`), or just hand the file to the agent
72
+ as instructions.
73
+
63
74
  ## License
64
75
 
65
76
  MIT
package/mcp/server.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // workhorse-mcp — MCP-сервер журнала делегирования оркестратор ↔ рабочая лошадка.
3
- // Хранилище: ~/.workhorse/workhorse.db (event sourcing: append-only events,
4
- // проекции tasks/incidents через триггеры, FTS5). Zero deps: node:sqlite (Node >= 22.5).
3
+ // Хранилище: ~/.workhorse-ai/journal.db (event sourcing: append-only events,
4
+ // проекции tasks/incidents через триггеры, FTS5); путь перекрывается
5
+ // WORKHORSE_DB — см. resolveDbPath в sync.mjs.
6
+ // Zero deps: node:sqlite (Node >= 22.5).
5
7
 
6
8
  import { DatabaseSync } from "node:sqlite";
7
9
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
8
- import { homedir } from "node:os";
9
10
  import { join, dirname } from "node:path";
10
11
  import { fileURLToPath } from "node:url";
11
12
 
@@ -14,10 +15,11 @@ import {
14
15
  inboxUrlFromSyncUrl,
15
16
  loadSyncConfig,
16
17
  pushJournal,
18
+ resolveDbPath,
17
19
  writeSyncConfig,
18
20
  } from "./sync.mjs";
19
21
 
20
- const DB_PATH = process.env.WORKHORSE_DB ?? join(homedir(), ".workhorse", "workhorse.db");
22
+ const DB_PATH = resolveDbPath();
21
23
  const SCHEMA_PATH =
22
24
  process.env.WORKHORSE_SCHEMA ?? join(dirname(fileURLToPath(import.meta.url)), "..", "schema.sql");
23
25
 
@@ -844,7 +846,7 @@ function handle(msg) {
844
846
  respond(id, {
845
847
  protocolVersion: params?.protocolVersion ?? "2024-11-05",
846
848
  capabilities: { tools: {}, prompts: {} },
847
- serverInfo: { name: "workhorse-mcp", version: "0.7.1" },
849
+ serverInfo: { name: "workhorse-mcp", version: "0.7.4" },
848
850
  instructions: SERVER_INSTRUCTIONS,
849
851
  });
850
852
  } else if (method === "prompts/list") {
package/mcp/sync.mjs CHANGED
@@ -12,8 +12,15 @@ import { pathToFileURL } from "node:url";
12
12
 
13
13
  export const BATCH_SIZE = 200;
14
14
 
15
- function defaultDbPath(env) {
16
- return env.WORKHORSE_DB ?? join(homedir(), ".workhorse", "workhorse.db");
15
+ // Данные под бренд Workhorse AI: директория по бренду, файл по смыслу.
16
+ export const DEFAULT_DB_DIR = ".workhorse-ai";
17
+ export const DEFAULT_DB_FILE = "journal.db";
18
+
19
+ // Единственный резолвер пути к базе (сервер и CLI пушера используют его же):
20
+ // WORKHORSE_DB перекрывает всё, иначе ~/.workhorse-ai/journal.db.
21
+ // Чистая функция — env и homedir подменяемы, тестируется без реальной ФС.
22
+ export function resolveDbPath({ env = process.env, homedir: home = homedir() } = {}) {
23
+ return env.WORKHORSE_DB ?? join(home, DEFAULT_DB_DIR, DEFAULT_DB_FILE);
17
24
  }
18
25
 
19
26
  // Конфиг синка: JSON-файл {url, token, journalId} рядом с базой (sync.json),
@@ -21,7 +28,7 @@ function defaultDbPath(env) {
21
28
  // WORKHORSE_SYNC_URL / WORKHORSE_SYNC_TOKEN / WORKHORSE_SYNC_JOURNAL_ID
22
29
  // перекрывают значения файла. Нет ни файла, ни env → синк выключен (null) — это норма.
23
30
  export function loadSyncConfig({ dbPath, env = process.env, log = console.error } = {}) {
24
- const resolvedDbPath = dbPath ?? defaultDbPath(env);
31
+ const resolvedDbPath = dbPath ?? resolveDbPath({ env });
25
32
  const configPath = env.WORKHORSE_SYNC_CONFIG ?? join(dirname(resolvedDbPath), "sync.json");
26
33
 
27
34
  let fileConfig = {};
@@ -48,7 +55,7 @@ export function loadSyncConfig({ dbPath, env = process.env, log = console.error
48
55
  // Перезапись существующего файла — осознанное действие пользователя (connect).
49
56
  // Возвращает путь записанного файла.
50
57
  export function writeSyncConfig({ dbPath, env = process.env, config }) {
51
- const resolvedDbPath = dbPath ?? defaultDbPath(env);
58
+ const resolvedDbPath = dbPath ?? resolveDbPath({ env });
52
59
  const configPath = env.WORKHORSE_SYNC_CONFIG ?? join(dirname(resolvedDbPath), "sync.json");
53
60
  writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`);
54
61
  return configPath;
@@ -164,7 +171,7 @@ const isCli =
164
171
  process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
165
172
 
166
173
  if (isCli) {
167
- const dbPath = defaultDbPath(process.env);
174
+ const dbPath = resolveDbPath();
168
175
  const config = loadSyncConfig({ dbPath });
169
176
  if (!config) {
170
177
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workhorse-ai-mcp",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Delegation journal for orchestrator/worker AI workflows: event sourcing over SQLite with an MCP interface. Zero dependencies.",
@@ -21,11 +21,13 @@
21
21
  },
22
22
  "files": [
23
23
  "mcp/",
24
+ "skills/",
24
25
  "schema.sql",
25
26
  "README.md",
26
27
  "LICENSE"
27
28
  ],
28
29
  "bin": {
30
+ "workhorse-ai-mcp": "mcp/server.mjs",
29
31
  "workhorse-mcp": "mcp/server.mjs",
30
32
  "workhorse-sync": "mcp/sync.mjs"
31
33
  },
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: workhorse-ai
3
+ description: Use when delegating substantive implementation work (feature, bugfix, refactor, tests) to another agent and accepting the result — the discipline of the workhorse delegation journal (draft → delegate → report → own verification → accept).
4
+ ---
5
+
6
+ # Workhorse AI — дисциплина оркестрации
7
+
8
+ ## Обзор
9
+
10
+ Работа разделена на две роли: **оркестратор** (планирует, ставит задание,
11
+ верифицирует, принимает) и **исполнитель** (пишет код по готовому заданию).
12
+ Ядро дисциплины: *доверие исполнителю — ноль, доверие своей верификации — всё*.
13
+ Исполнитель может честно ошибаться о собственном успехе — его прогон тестов
14
+ это информация, а не доказательство.
15
+
16
+ Журнал делегирования — MCP-сервер `workhorse` (инструменты `mcp__workhorse__*`):
17
+ append-only лог событий поверх SQLite, проекции задач и инцидентов, полнотекстовый
18
+ поиск. Журнал валидирует переходы статусов, так что руками в базу лезть не нужно
19
+ и нельзя. Файлов задач в репозитории проекта не создавать — состояние живёт
20
+ только в журнале.
21
+
22
+ ## Роли
23
+
24
+ | Этап | Кто | Почему |
25
+ |---|---|---|
26
+ | Диагноз, root cause | оркестратор | Задание без доказанной причины = исполнитель уходит в исследование и дрейфует |
27
+ | План и декомпозиция | оркестратор | Scope-контроль до работы, а не после |
28
+ | Написание кода | исполнитель | По готовому заданию, с явными запретами |
29
+ | Ревью диффа | оркестратор | Построчно, до любого прогона |
30
+ | Сборка и полный прогон тестов | **только оркестратор** | Не делегируется никогда |
31
+ | Коммит / push / деплой | **только оркестратор** | После собственного зелёного прогона |
32
+
33
+ Мелочь в одну-три строки с уже готовым диагнозом делается инлайн: делегирование
34
+ дороже самой правки.
35
+
36
+ ## Статусы и ключевой инвариант
37
+
38
+ ```
39
+ DRAFT → DELEGATED → REPORTED → ACCEPTED | REWORK (→ DELEGATED …) | FAILED
40
+ ```
41
+
42
+ `REPORTED` («исполнитель считает, что готово») **≠** `ACCEPTED`
43
+ («оркестратор верифицировал сам»). Это главный инвариант журнала: между этими
44
+ статусами обязан лежать собственный полный прогон тестов оркестратора.
45
+
46
+ ## Bootstrap — жёсткое precondition
47
+
48
+ Пока проект не прошёл bootstrap, с ним не работать вообще: ни `draft_task`,
49
+ ни делегаций, ни артефактов. Bootstrap состоит из двух шагов:
50
+
51
+ 1. `register_project` — имя-неймспейс и `root_path`. Перед регистрацией
52
+ обязательно `resolve_project` (по пути и/или имени): почти-дубли
53
+ (`foo` ~ `foo-app`, вложенные пути) сервер отобьёт; осознанный обход —
54
+ `force: true`.
55
+ 2. Артефакт `Project baseline: <project>` (kind `spec`): стек и конвенции,
56
+ команды сборки и тестов, **базовая цифра полного прогона**, запреты для
57
+ исполнителей.
58
+
59
+ Проверка при каждом входе в делегацию: проект есть в `list_projects` **и**
60
+ baseline есть в `list_artifacts`. Нет — остановиться и сделать bootstrap
61
+ (с реальным полным прогоном тестов), только потом продолжать. «Задача
62
+ маленькая» не исключение: без эталонной цифры прогона приёмка недоказуема.
63
+ Проект, где журнал не нужен, — просто не регистрировать.
64
+
65
+ После существенных изменений проекта baseline обновить: та же `title` = новая
66
+ версия артефакта, старая остаётся в истории.
67
+
68
+ ## Порядок работы
69
+
70
+ 1. **`search_precedents`** — обязательно ДО постановки: похожие задачи,
71
+ артефакты и инциденты по всем проектам. Дешевле вспомнить, чем повторить.
72
+ 2. **`record_artifact`** — если решению предшествовало обсуждение, зафиксировать
73
+ спеку / план / ADR / решение (kind: `spec`/`plan`/`adr`/`decision`/`note`)
74
+ ДО делегации. Значимое решение фиксировать сразу — иначе оно умрёт вместе
75
+ с сессией.
76
+ 3. **`draft_task`** — полный текст задания в `task_text`. Хорошее задание состоит,
77
+ по порядку, из:
78
+ - **контекста**: репозиторий, ветка, «прочитай инструкции проекта», стек-правила;
79
+ - **готового root cause**: точные `file:line`, доказательства, эталон поведения —
80
+ «не переисследуй»;
81
+ - **порядка работ**: TDD — падающий тест (зафиксируй вывод) → минимальный фикс →
82
+ точечный прогон → полный прогон с указанием базовой цифры («база N/N, любое
83
+ новое падение — твоё»);
84
+ - **запретов**: не коммитить, не пушить, не трогать перечисленные файлы и
85
+ каталоги, не расширять scope, не добавлять зависимости;
86
+ - **формата отчёта**: по каждому пункту «падал → прошёл» с выводом команд,
87
+ список изменённых файлов, находки.
88
+
89
+ Если исполнитель должен что-то оценить сам — дать критерий остановки
90
+ («нашёл потребителя → не удаляй, доложи»).
91
+ 4. **`delegate`** (+ запуск исполнителя). В промпт исполнителя входят: `task_id`,
92
+ текст задания и три обязанности журнала — `search_precedents` до старта;
93
+ прогресс по ходу через `record_artifact` (kind `note`, привязан `task_id`,
94
+ стабильный `title` вида `progress: <slug>` — версионируется); `submit_report`
95
+ в конце. Отчёт полезно дублировать файлом: файл переживает крах shell'а
96
+ исполнителя, а событие в журнале при крахе до `submit_report` — нет.
97
+ 5. Пока исполнитель работает, оркестратор не ждёт вслепую: поллит `get_task` и
98
+ `list_artifacts` по `task_id`. Если исполнитель умер до `submit_report` —
99
+ оркестратор сдаёт отчёт в журнал сам из файла.
100
+ 6. **Приёмка**: прочитать отчёт → **дифф построчно**, сверить со scope задания →
101
+ собственная сборка и **полный** прогон тестов → сравнить с базовой цифрой.
102
+ Новые падения → сначала подозревать интерференцию (осиротевшие фоновые
103
+ сборки, гонка за артефактами, флейки инфраструктуры), потом код.
104
+ 7. Ровно одно из: **`accept`** (только после зелёного прогона и коммита,
105
+ с `verify_commit`) / **`request_rework`** (затем снова `delegate`) /
106
+ **`mark_failed`**.
107
+ 8. Были грабли → **`record_incident`**: описание симптома и урок на будущее.
108
+
109
+ Обзор состояния: `get_task` (задача + история событий), `list_tasks`
110
+ (фильтры по статусу и проекту), `list_artifacts`.
111
+
112
+ ## Правила журнала
113
+
114
+ - Статус меняется только событием через MCP. Прямой INSERT мимо сервера
115
+ обходит валидацию переходов.
116
+ - Артефакты версионируются: повторная запись с тем же `title` = новая версия,
117
+ старая остаётся в истории.
118
+ - Продолжение уже закрытой задачи — всегда **новая** задача + `link_tasks`
119
+ (`kind: continues`) на старую; rework-цикл живёт только внутри незакрытой.
120
+ Работа, найденная по ходу другой, — новая задача + `discovered_from`.
121
+ - Журнал — информация для поиска и ретроспективы. Доказательство приёмки —
122
+ собственный прогон оркестратора, а не запись в базе.
123
+
124
+ ## Известные грабли
125
+
126
+ | Симптом / соблазн | Реальность |
127
+ |---|---|
128
+ | «Исполнитель написал: все тесты прошли» | Прогон исполнителя — информация, не доказательство. Песочница умеет съедать падение таймаутом. |
129
+ | Исполнитель завис на сборке или тестах | Не ждать: остановить задачу, код обычно уже написан, верификация всё равно твоя. |
130
+ | Внезапная пачка падений, прогон в разы дольше обычного | Почти всегда интерференция с осиротевшими процессами сборки, а не дифф. Прибить, перегнать. |
131
+ | Два-три зелёных прогона подряд «доказали», что база стабильна | При флейке такая серия выпадает сама собой. Связывать падения с правкой только после A/B (stash → прогон → pop → прогон), минимум три пары. |
132
+ | «Задание простое, root cause пусть найдёт сам» | Без готового диагноза исполнитель переисследует, дрейфует и жжёт лимиты. Диагноз — работа оркестратора. |
133
+ | «Закоммичу его коммитом, он же автор» | Коммит = принятая ответственность. Коммитит тот, кто верифицировал. |
134
+ | «Заведу проект в реестре на глаз» | Почти-дубли плодятся мгновенно, а миграция-переименование базы однажды уносит данные. Сначала `resolve_project`; переносы базы — только с бэкапом. |
135
+ | Перепечатать токен руками, «тут же одна строка» | Одна опечатка = битые креды. Только копирование, после записи — сверка. |
136
+ | Смешать в одной команде работу в worktree и в основном репозитории | Merge ответит «Already up to date», прогон отработает вхолостую. Разносить по отдельным вызовам. |
137
+
138
+ ## Облако — опциональный слой
139
+
140
+ Журнал полностью работает офлайн; синхронизация с облаком добавляется поверх
141
+ и ничего не меняет в дисциплине.
142
+
143
+ - **`connect`** — проверяет соединение и записывает конфиг синка рядом с базой.
144
+ После этого каждая запись в журнал уезжает наверх fire-and-forget.
145
+ - **`sync`** — форсированный пуш (направление строго вверх: журнал наверх,
146
+ журнал не переписывается облаком).
147
+ - **`inbox`** / **`take`** — намерения задач приходят из облака: `inbox` показывает
148
+ очередь, `take` забирает намерение в работу. Намерение — это ещё не задание:
149
+ оно проходит обычный путь через `draft_task` с полным текстом.
150
+
151
+ Недоступное облако не должно блокировать работу: локальный журнал — источник
152
+ истины, пуш — транспорт.