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.
| 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).
Markdown with a YAML front matter block:
--- kind: protocol name: Корректное выключение инфраструктуры description: Останавливает сервисы в порядке зависимостей и проверяет диск. area: infrastructure tags: [power, shutdown] triggers: - "пользователь просит выключить сервер" steps: - "Остановить приложения" - "Остановить базы данных" - "Синхронизировать и отмонтировать диски" preconditions: - "Все пользователи предупреждены" post_checks: - "Прикладных юнитов не осталось" on_deviation: "Не продолжать; вернуться к предыдущему шагу и доложить." on_blocked: "Остановить процесс и доложить, что именно не дало завершить." on_already_done: "Проверить статус; если результат уже есть — отметить шаг." --- Контекст: порядок зависимостей, известные ловушки, что занимает время.
| 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.
Four tables (PostgreSQL, lazy DDL on first use):
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';
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.(owner_id, kind, name) — the same name may exist once personally and once shared without colliding.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.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.
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".
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.
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.
skillssearch, load, create, update, propose:
{"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.
protocolstart, finish, cancel, fail, status — the run machinery described above:
{"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 assistant, developer, server_admin and discuss; sub-agents get neither, because a protocol belongs to the session's plan, not to a delegated run.
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.
content_hash, version).protocol_runs is written, but the tab shows the catalog only).