# plan-panel.mjs

## Огляд

Модуль `plan-panel.mjs` реалізує **agent↔agent brainstorm** — багатоперсональну панель субагентів, які формують спільну відповідь на задачу. Концептуально це поєднання двох підходів:

- **bmad party-mode** — кілька «персон» одночасно дивляться на проблему з різних кутів;
- **superpowers dispatching** — окремий суддя-субагент синтезує думки персон в єдину фінальну відповідь.

Панель є спільним механізмом для двох фаз життєвого циклу задачі:

- `mode: 'spec'` — генерує 2–3 текстові підходи до розв’язання у форматі Markdown (для фази специфікації);
- `mode: 'plan'` — генерує покроковий JSON-план (для фази планування).

Модуль повторно використовує **runner-інтерфейс Фасада B**, який очікує об’єкт із методом `runStep(prompt, opts) => { ok, output }` (контракт, аналогічний `planner.mjs` і `active.mjs` у тому ж каталозі).

Принципово важлива поведінкова характеристика — **HITL (human-in-the-loop)**: панель тільки **повертає синтез** і ніколи не зберігає артефакт самостійно. Апрув людини та фіксація результату належать до контракту `flow.mdc` і виконуються окремими командами (`flow spec` / `flow plan`).

## Експорти / API

Модуль має один публічний експорт:

- `runPanel(input)` — асинхронна функція, що проводить панель і повертає синтез або `null` у разі провалу.

Внутрішні артефакти модуля (не експортуються):

- константа `PERSONAS` — список персон панелі;
- функція `judgePrompt(mode, proposals, task)` — формує промпт для судді залежно від режиму.

## Функції

### `runPanel({ task, cwd, runner, log, mode })`

**Сигнатура:**

```
async function runPanel({
  task,        // string — опис задачі для персон і судді
  cwd,         // string — робочий каталог, передається в runner.runStep як opts.cwd
  runner,      // { runStep: (prompt, opts?) => { ok, output } | Promise<{ ok, output }> }
  log,         // (msg: string) => void — логер помилок; за замовчуванням console.error
  mode         // 'spec' | 'plan' — режим синтезу; за замовчуванням 'plan'
}) => Promise<
  | { task: string, acceptance?: string }[]   // коли mode === 'plan' і JSON парситься
  | string                                    // коли mode === 'spec'
  | null                                      // у разі будь-якого фейлу
>
```

**Параметри:**

- `task` — текстовий опис задачі. Підставляється в системний промпт кожної персони (`<sys>\n\nЗадача: <task>`) та в промпт судді (рядок `Задача: <task>` у кінці).
- `cwd` — робоча директорія; модуль не використовує її напряму, лише передає у `runner.runStep` через `{ cwd }`.
- `runner` — обов’язкова ін’єкція з методом `runStep`. Якщо відсутній, функція логуює повідомлення `panel: нема runner — режим --panel недоступний` і повертає `null`.
- `log` — функція логування непомилкового рівня (фактично використовується тільки для діагностичних повідомлень про помилки). За замовчуванням — `console.error`.
- `mode` — `'spec'` або `'plan'`. За замовчуванням `'plan'`. Впливає на:
  - інструкцію в `judgePrompt`;
  - формат повернення (текст або розпарсений JSON-масив).

**Повертає:**

- Для `mode === 'plan'`: масив об’єктів `{ task: string, acceptance?: string }` після успішного `JSON.parse` фрагмента між першим `[` і останнім `]` у відповіді судді.
- Для `mode === 'spec'`: рядок `judge.output` (Markdown від судді) як є.
- `null` — у будь-якому з провальних сценаріїв (див. далі).

**Сценарії повернення `null`:**

1. Не передано `runner` → лог `panel: нема runner — режим --panel недоступний`.
2. Виклик судді завершився `{ ok: false }` → лог `panel: суддя-синтез завершився помилкою`.
3. Режим `plan`, але у виводі судді не знайдено JSON-меж (`[` / `]`) або `end < start` → лог `panel: суддя не повернув JSON-план`.
4. Режим `plan`, JSON знайдено, але `JSON.parse` кинув виняток → лог `panel: невалідний JSON синтезу`. Виняток ловиться `catch {}` і не пробрасується далі.

**Алгоритм (по кроках):**

1. Перевірити наявність `runner`; якщо нема — залогувати й вийти з `null`.
2. Паралельно через `Promise.all` опитати всі персони з `PERSONAS`:
   - для кожної персони викликати `runner.runStep(<sys>\n\nЗадача: <task>, { cwd })`;
   - сформувати рядок `### <name>\n<output або "(порожньо)" якщо !ok>`.
3. Склеїти всі думки персон через `\n\n` і викликати суддю промптом, побудованим у `judgePrompt(mode, proposals, task)`.
4. Якщо суддя `!ok` — лог і `null`.
5. Якщо `mode === 'spec'` — повернути `judge.output` без обробки.
6. Інакше (plan): знайти межі JSON-масиву (`indexOf('[')` та `lastIndexOf(']')`), валідувати їх; розпарсити `slice(start, end + 1)` і повернути; у разі помилки парсингу — лог і `null`.

**Side effects:**

- **Зовнішні виклики моделей через `runner.runStep`**: персон — `PERSONAS.length` (зараз 3), плюс один виклик судді. Усього 4 запити на один прогін `runPanel`.
- **Логування**: виключно через переданий `log` (за замовчуванням `console.error`). Інших звернень до stdout/stderr нема.
- **Файлову систему не чіпає**, мережу безпосередньо не використовує (опосередковано — через `runner`), артефактів не зберігає (HITL — фіксацію робить агент за `flow.mdc`).
- **Конкурентність**: запити персон виконуються паралельно (`Promise.all` + `map(async …)`); виклик судді — послідовно після завершення всіх персон.

### `judgePrompt(mode, proposals, task)`

**Сигнатура:**

```
function judgePrompt(
  mode: 'spec' | 'plan',
  proposals: string,
  task: string
): string
```

**Параметри:**

- `mode` — режим синтезу. Визначає інструкцію в «голові» промпту:
  - `'plan'` — три рядки інструкції: вимога ОДНОГО покрокового плану, обмеження ≤ 5 хв на крок із критерієм приймання, вимога повернути **ЛИШЕ JSON-масив без коментарів** у форматі `[{ "task": "...", "acceptance": "..." }, ...]`.
  - `'spec'` (та будь-яке інше значення) — дві інструкції: 2–3 підходи з рекомендацією й коротким дизайном, повернути людино-читабельний Markdown.
- `proposals` — попередньо склеєні думки персон (зазвичай `proposals.join('\n\n')` із масиву `### name\noutput`).
- `task` — оригінальний опис задачі (рядок `Задача: <task>` додається в кінець промпту).

**Повертає:** рядок промпту, склеєний через `\n` у вигляді:

```
<head-line-1>
<head-line-2>
[<head-line-3 — лише для plan>]

<proposals>

Задача: <task>
```

**Side effects:** чиста функція без побічних ефектів і зовнішніх викликів.

## Залежності

### Зовнішні модулі / пакети

- **Жодних `import`** із Node.js core, npm-пакетів чи інших файлів проєкту. Модуль повністю самодостатній.

### Глобали

- `console.error` — використовується як **дефолтне** значення параметра `log`. Якщо викликач передасть власний `log`, `console` не використовується.
- `Promise`, `JSON`, `Array.isArray` (не використовується), `String.prototype.indexOf` / `lastIndexOf` / `slice` — стандартні JS-примітиви.

### Контрактні (поведінкові) залежності

- **Runner-інтерфейс Фасада B**: `runner.runStep(prompt: string, opts?: { cwd: string }) => { ok: boolean, output: string } | Promise<{ ok, output }>`. Цей контракт реалізують сусідні модулі — наприклад, `planner.mjs` і `active.mjs` у каталозі `lib/`.
- **HITL за `flow.mdc`**: правила Flow вимагають, щоб фіксацію артефакту (`flow spec` / `flow plan`) виконував агент окремою командою; панель сама артефактів не пише.

### Внутрішні константи

- `PERSONAS` — масив із трьох записів `[name, systemPrompt]`:
  - `architect` — «Запропонуй найчистішу архітектуру розв’язання. Стисло, по суті.»
  - `skeptic` — «Назви ризики, граничні випадки і що може піти не так. Стисло.»
  - `tester` — «Опиши, які тести доведуть коректність. Стисло.»

## Потік виконання / Використання

### Типовий потік виклику

1. Викликач (наприклад, CLI-команда фази `spec` чи `plan`) отримує `runner`, що інкапсулює запуск субагента (модель + транспорт), і поточний `cwd`.
2. Викликач формулює `task` і обирає `mode` (`'spec'` або `'plan'`).
3. Викликає `await runPanel({ task, cwd, runner, mode, log? })`.
4. Внутрішньо панель:
   - паралельно опитує `architect`, `skeptic`, `tester` — кожна повертає текстову думку (`### <name>\n<output>` або `(порожньо)` у разі `!ok`);
   - формує промпт судді через `judgePrompt`, який отримує всі думки + оригінальне завдання;
   - запускає суддю одним викликом `runner.runStep`;
   - на основі `mode` повертає або сирий Markdown (`spec`), або розпарсений JSON-масив кроків (`plan`).
5. Викликач отримує результат і:
   - для `null` — повідомляє користувача та припиняє/повторює фазу;
   - для `spec` (рядок) — показує користувачу й чекає `flow spec` для фіксації;
   - для `plan` (масив кроків) — передає далі (виконавцю/планеру) і чекає `flow plan` для фіксації.

### Очікувані формати виводу судді

- **`plan`**: модель повинна повернути рядок, у якому міститься JSON-масив об’єктів вигляду `{ "task": "...", "acceptance": "..." }`. Парсер толерантний до «обгортки» (текст до першого `[` і після останнього `]` ігнорується), але самий JSON має бути валідним.
- **`spec`**: довільний Markdown-текст; жодних обмежень формату на стороні модуля.

### Гарантії та обмеження

- **Ніколи не кидає виняток назовні**: усі провальні шляхи завершуються `null` + лог через `log()`. `try/catch` навколо `JSON.parse` ловить навіть synthax-помилки парсингу.
- **Паралелізм персон**: усі персони запускаються одночасно — це впливає на навантаження `runner` (зараз 3 паралельні виклики моделі) та сумарну затримку (≈ max(persona-latency) + judge-latency, а не сума).
- **Не зберігає стан** між викликами; кожен виклик `runPanel` повністю самодостатній.
- **Не валідує внутрішню структуру** елементів JSON-плану — тип `{ task: string, acceptance?: string }[]` зазначений у JSDoc, але рантайм-перевірки полів немає. Викликач відповідає за подальшу валідацію.

### Розширюваність

- Додати персону: дописати запис у масив `PERSONAS`. Жодних інших змін не потрібно — обхід йде через `map`.
- Додати режим: розширити умову в `judgePrompt` (та, за потреби, гілку обробки виводу у `runPanel`).
- Підмінити модель/транспорт: реалізувати інший `runner` із тим самим контрактом `runStep` — модуль працює з будь-яким об’єктом, що задовольняє інтерфейс.
