# Skills and protocols

The catalog behind the agent's methodologies: reusable **skills** (how to approach
a kind of task) and **protocols** (a strict, pre-agreed order of actions). One
registry, one document format, one place in settings.

## Skills vs protocols vs manuals

| | manual | skill | protocol |
|---|---|---|---|
| answers | "how does this tool work" | "how do I approach this" | "what exactly to do, in what order" |
| written by | developers | the agent, freely | the agent proposes, the user accepts |
| strictness | reference | advice, adapt as needed | commitment; deviation is an error to report |
| storage | `manuals/*.md` in the repo | catalog row | catalog row |

A skill is injected as instructions and nothing more — no run, no report. A
protocol has an explicit start, mandatory checks, and an explicit finish with a
report (the run machinery builds on the todo plan; see
[todo-store.md](todo-store.md)).

## Document format

Markdown with a YAML front matter block:

```markdown
---
kind: protocol
name: Корректное выключение инфраструктуры
description: Останавливает сервисы в порядке зависимостей и проверяет диск.
area: infrastructure
tags: [power, shutdown]
triggers:
  - "пользователь просит выключить сервер"
steps:
  - "Остановить приложения"
  - "Остановить базы данных"
  - "Синхронизировать и отмонтировать диски"
preconditions:
  - "Все пользователи предупреждены"
post_checks:
  - "Прикладных юнитов не осталось"
on_deviation: "Не продолжать; вернуться к предыдущему шагу и доложить."
on_blocked: "Остановить процесс и доложить, что именно не дало завершить."
on_already_done: "Проверить статус; если результат уже есть — отметить шаг."
---

Контекст: порядок зависимостей, известные ловушки, что занимает время.
```

### Fields

| Field | skill | protocol |
|---|---|---|
| `kind` | required | required |
| `name` | required, unique per owner | required, unique per owner |
| `description` | recommended | recommended |
| `triggers` | optional list — when to use it | optional list — when the protocol applies |
| `tags` | optional | optional |
| `area` | `home` \| `infrastructure` \| `workflows` \| `general` (default) | same |
| `steps` | — | **required** |
| `preconditions` | — | optional list |
| `post_checks` | — | **required** — how each step's result is verified |
| `on_deviation` | — | **required** — the run drifted from the plan |
| `on_blocked` | — | **required** — the protocol cannot be completed |
| `on_already_done` | — | **required** — a step's result already exists |
| body | **required** — the instructions | the context around the steps |

Validation is strict on purpose: a protocol without its three escape hatches
cannot be executed honestly when reality differs from the plan, so the store
refuses it with a message naming the missing keys.

Unknown front matter keys are preserved in the JSONB copy, so extra metadata
(`owner_team`, `reviewed_by`) survives a round trip through the editor.

## Storage

Four tables (PostgreSQL, lazy DDL on first use):

```sql
CREATE TABLE skills (
    id TEXT PRIMARY KEY, kind TEXT, name TEXT, description TEXT,
    triggers TEXT[], tags TEXT[], area TEXT,
    visibility TEXT, owner_id TEXT,      -- 'personal' | 'shared'
    content TEXT, frontmatter JSONB,
    version INTEGER, content_hash TEXT,
    created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ, updated_by TEXT
);
CREATE TABLE skill_versions (
    id BIGSERIAL PRIMARY KEY, skill_id TEXT REFERENCES skills(id) ON DELETE CASCADE,
    version INTEGER, content TEXT, frontmatter JSONB, content_hash TEXT,
    edited_by TEXT, reason TEXT, created_at TIMESTAMPTZ,
    UNIQUE (skill_id, version)
);
CREATE TABLE skill_proposals (
    id TEXT PRIMARY KEY, owner_id TEXT, session_id TEXT, kind TEXT, name TEXT,
    content TEXT, frontmatter JSONB, replaces_id TEXT,
    status TEXT,        -- 'pending' | 'accepted' | 'rejected'
    skill_id TEXT, error TEXT,
    created_at TIMESTAMPTZ, decided_at TIMESTAMPTZ, decided_by TEXT
);
CREATE TABLE protocol_runs (
    id TEXT PRIMARY KEY,
    protocol_id TEXT, protocol_name TEXT, protocol_version INTEGER,
    session_id TEXT, user_id TEXT,
    status TEXT,        -- 'running' | 'done' | 'failed' | 'cancelled'
    steps JSONB, post_checks JSONB,      -- snapshot at start
    report TEXT, reason TEXT,
    started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_protocol_runs_one_active
    ON protocol_runs (session_id) WHERE status = 'running';
```

- **Ownership**: personal rows have `owner_id = <user>`; shared rows have
  `owner_id = ''` and are writable only by an admin. An admin creates both; a
  regular user creates personal entries only.
- **Uniqueness**: `(owner_id, kind, name)` — the same name may exist once
  personally and once shared without colliding.
- **Versioning**: every create and update appends to `skill_versions`; `version`
  on the row is the latest. History keeps `edited_by` and `reason`.
- **`content_hash`** is the SHA-256 of the document. It is the only
  hive-synchronization hook committed so far; the sync service itself (instance
  registry in the hive) is future work.

## Search

`SkillStore.list(user_id, kind?, query?, tag?, area?, limit)` returns what the
user may see (own + shared), personal first, then most recently updated.
`query` matches name, description, triggers and tags. The agent's `skills` tool
uses the same query; the settings tab uses the HTTP API.

## REST API

All endpoints require a logged-in user.

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/skills?kind=&q=&tag=&area=&limit=` | catalog for the current user |
| `GET` | `/skills/{id}` | one entry with the full document |
| `POST` | `/skills` | create; body `{content, visibility?, reason?}` |
| `PUT` | `/skills/{id}` | edit; body `{content, reason?, visibility?}` |
| `DELETE` | `/skills/{id}` | remove (history cascades) |
| `GET` | `/skills/{id}/versions?limit=` | edit history, newest first |
| `GET` | `/skills/proposals/{id}` | a protocol proposal (owner or admin) |

`visibility: shared` on create or change requires `role == "admin"`.

## Proposals

A protocol is never created by the agent directly. `skills(op="propose")` writes
a row to `skill_proposals` (owner, session, document, optional `replaces_id`) and
returns a card; the catalog row appears only when the user clicks **Добавить**.

The decision is handled by the server, not by the model: the browser sends
`{"type": "skill_proposal", "proposal_id": "...", "action": "accept" | "reject"}`
over the WebSocket, `SkillStore.decide_proposal` writes (or does not write) the
catalog row, and the answer comes back as `skill_proposal_decided`. Accepting
saves the *stored* document — the client cannot swap the text — and names the
clicking user as the author of the new version. A proposal the store then refuses
(a duplicate name, say) stays `pending` with the reason on the row, and the card
shows it.

Rejecting leaves the catalog untouched. Either way the agent learns the outcome
as a hidden user message, the same way a `navi_ui` form submission reaches it, so
it can continue the conversation with the fact in hand.

Pending proposals do not show up in search or `skills(op="load")` — a document
the user has not agreed to must never reach another turn as a methodology.

## Runs

Loading a protocol only shows its text. Executing one is the `protocol` tool:

| op | what it does |
|---|---|
| `start` | opens a run and splices the steps into the todo plan after the agent's current position |
| `finish` | closes the run as `done`; **requires a report** and refuses while any step of the run is open |
| `cancel` | closes it as `cancelled` with a reason; still-open run steps become `skipped` |
| `fail` | closes it as `failed` with a reason; a run with a failed step can only end this way |
| `status` | the active run and the state of its steps |

A run row snapshots the protocol's `version`, `steps` and `post_checks` at
start, so editing the catalog entry mid-run cannot change what the running
agent owes the user; the log still says which version was executed. One run per
session is `running` at a time (a partial unique index enforces it).

The steps are ordinary todo tasks, tagged with `source="protocol"`,
`protocol_run_id` and a stable `step_id`; `insert_steps` keeps the agent's own
progress and statuses intact. Marking one `done` still goes through
`todo(op="update")` and still needs a `validation`.

While a run is `running`, every model turn gets a system reminder with the
protocol, its steps and the way to close it (`navi/core/agent.py`, next to the
todo reminder). This is what survives compression: a run started ten turns ago
is otherwise invisible to the model once the todo lines lose their context. The
final-turn intercept also fires when a run is open even though all its steps are
closed — a bare-text turn cannot leave it running.

The log is a row per run with `report`/`reason`; there is no separate UI yet —
`protocol(op="status")` and the table are the current view.

## Agent tools

### `skills`

`search`, `load`, `create`, `update`, `propose`:

```json
{"op": "search", "query": "выключение", "area": "infrastructure"}
{"op": "load", "id": "3f2a9c1d8e4b7a60"}
{"op": "create", "content": "---\nkind: skill\n..."}
{"op": "update", "id": "3f2a9c1d8e4b7a60", "content": "<document>"}
{"op": "propose", "content": "<protocol document>", "replaces_id": "9c4d2b7a1e6f8035"}
```

`load` renders a compact document: for a protocol, preconditions, numbered steps,
post-checks and the three escape hatches; for a skill, the instructions. `create`
and `update` accept skills only — a protocol document sent there is refused with
the reason, and `update` of a protocol points at `propose` with `replaces_id`.

### `protocol`

`start`, `finish`, `cancel`, `fail`, `status` — the run machinery described
above:

```json
{"op": "start", "id": "9c4d2b7a1e6f8035"}
{"op": "finish", "report": "<what was done, each post-check and its result>"}
{"op": "fail", "reason": "<which step, why, what state things are in>"}
```

Both tools are available to every profile that runs user work — `assistant`,
`designer_3d`, `developer`, `discuss`, `modeler_3d`, `navi_code` and
`server_admin`. Two exceptions: `dispatcher` is a single-turn internal router
with `max_iterations: 1` and no tools at all, and sub-agents get neither,
because a protocol belongs to the session's plan, not to a delegated run.

## Settings tab

Settings → **Скилы и протоколы**: the catalog list with a search box and kind/
area filters, a document editor that shows the server's validation error
verbatim, create from a skill or protocol template, delete, and the edit history.
An admin can also move an entry between personal and shared; for everyone else a
shared entry opens read-only.

## Not implemented yet

- Hive synchronization of shared entries (schema is ready: `content_hash`,
  `version`).
- A run-history view in the webclient (`protocol_runs` is written, but the tab
  shows the catalog only).
