mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
docs: document skills
README and the example config cover the [skills] section, the five tools, and the read/write split; AGENTS.md gains the skills/ package, the tool family, the config fields, and the /skills + /status surface; architecture.md lists the package. doc/skills.md is the author guide: layout, the frontmatter subset and how a broken one degrades, roots and shadowing, what the agent sees, the grant story for reads and writes, and the warning that a skill is data — third-party skill directories are untrusted input.
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project overview
|
## Project overview
|
||||||
|
|
||||||
Plyngent is an LLM chat and agent toolkit (Python 3.14+, PDM-managed). Single-user CLI is usable: protocol clients, config, async memory, agent tool loop, workspace tools, and REPL/one-shot chat. Multi-tenant `router/` / web remain Phase H.
|
Plyngent is an LLM chat and agent toolkit (Python 3.14+, PDM-managed). Single-user CLI is usable: protocol clients, config, async memory, agent tool loop, workspace tools, skill discovery, and REPL/one-shot chat. Multi-tenant `router/` / web remain Phase H.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ lmproto/deepseek/anthropic/ ← DeepSeek Anthropic client (extends Anthropic
|
|||||||
|
|
||||||
### Config (`config/`)
|
### Config (`config/`)
|
||||||
|
|
||||||
TOML load/store (`ConfigStore`): `[providers]` tagged union presets, `[database]` section. Default path via platformdirs. Providers may set `timeout` as a float or `{ connect, read }` (`HttpTimeoutConfig`); omitted → product defaults (10s connect / 600s read). `DeepseekProvider.convention` (Literal: `""`/`"openai"`/`"anthropic"`/`"responses"`) selects the DeepSeek API surface — `"responses"` → OpenAI Responses, `"anthropic"` → Anthropic Messages on `https://api.deepseek.com/anthropic` (default url follows the convention); a per-model `ModelConfig.convention` overrides it (empty = inherit). `[mcp]` (`McpConfig`): `servers.<name>` stdio server definitions (`McpServerConfig`: `command`/`args`/`env`/`cwd`/`timeout`/`read_only`, `url` reserved for streamable HTTP) + `disable` gate; defining a server opts it in.
|
TOML load/store (`ConfigStore`): `[providers]` tagged union presets, `[database]` section. Default path via platformdirs. Providers may set `timeout` as a float or `{ connect, read }` (`HttpTimeoutConfig`); omitted → product defaults (10s connect / 600s read). `DeepseekProvider.convention` (Literal: `""`/`"openai"`/`"anthropic"`/`"responses"`) selects the DeepSeek API surface — `"responses"` → OpenAI Responses, `"anthropic"` → Anthropic Messages on `https://api.deepseek.com/anthropic` (default url follows the convention); a per-model `ModelConfig.convention` overrides it (empty = inherit). `[mcp]` (`McpConfig`): `servers.<name>` stdio server definitions (`McpServerConfig`: `command`/`args`/`env`/`cwd`/`timeout`/`read_only`, `url` reserved for streamable HTTP) + `disable` gate; defining a server opts it in. `[skills]` (`SkillsConfig`): `paths` (explicit roots first), `discover` (`plyngent` user dir + `claude` dirs) with `include_project_roots`, `inject_catalog` / `max_catalog_skills`, `enabled`, and `allow_write` (standing write grant); a malformed section degrades to defaults like `[agent]` / `[mcp]`.
|
||||||
|
|
||||||
### Runtime (`runtime/`)
|
### Runtime (`runtime/`)
|
||||||
|
|
||||||
@@ -64,6 +64,10 @@ TOML load/store (`ConfigStore`): `[providers]` tagged union presets, `[database]
|
|||||||
|
|
||||||
MCP (`runtime/mcp_client.py`): `McpManager` owns one `McpServerConnection` per `[mcp]` server — a spawned stdio subprocess speaking newline-delimited JSON-RPC 2.0: initialize handshake, `tools/list` (cursor pagination), `tools/call`, `notifications/tools/list_changed` → `tools_stale`. Spawn/handshake failures record `connection.error` (status string), never raise; `ensure_started` / `restart(config)` / `aclose` / `refresh_tools`; `statuses()` → `(server, status, tool_count)` rows; `instructions()` → `(server, text)` for every connected server that returned optional `InitializeResult.instructions` (usage guidance captured per connection); per-request timeout from config (default 30s).
|
MCP (`runtime/mcp_client.py`): `McpManager` owns one `McpServerConnection` per `[mcp]` server — a spawned stdio subprocess speaking newline-delimited JSON-RPC 2.0: initialize handshake, `tools/list` (cursor pagination), `tools/call`, `notifications/tools/list_changed` → `tools_stale`. Spawn/handshake failures record `connection.error` (status string), never raise; `ensure_started` / `restart(config)` / `aclose` / `refresh_tools`; `statuses()` → `(server, status, tool_count)` rows; `instructions()` → `(server, text)` for every connected server that returned optional `InitializeResult.instructions` (usage guidance captured per connection); per-request timeout from config (default 30s).
|
||||||
|
|
||||||
|
### Skills (`skills/`)
|
||||||
|
|
||||||
|
Discovery and parsing, independent of the agent: `build_roots(config, workspace=…, user_config_dir=…, home=…)` resolves `[skills]` into ordered `SkillRoot`s (`config` explicit paths → `plyngent` user dir → `claude` user/project dirs), `SkillStore` scans each root for directories with a `SKILL.md` (a root may itself be one), parses the frontmatter subset (`frontmatter.py`), and caches the records against the directory list and its `SKILL.md` timestamps. `skills` returns the visible records sorted by name (first root wins), `shadowed()` the losing copies, `find(name)` exact-then-case-insensitive, `default_write_root()` our own directory (or the first root), `reload()` forces a rescan; `resolve_inside` / `resolve_skill_file` keep every read and write inside a skill directory (absolute paths and symlink escapes are refused). Records carry `issues` (unusable declared name, no description, unparseable frontmatter) so a repaired third-party skill stays visible instead of being silently fixed.
|
||||||
|
|
||||||
### Memory (`memory/`)
|
### Memory (`memory/`)
|
||||||
|
|
||||||
Async SQLAlchemy + aiosqlite. `MemoryStore`: schema init (+ lightweight SQLite `ALTER` for new columns; `PRAGMA user_version` 4), default local user, sessions (bound to `workspace` path; optional `provider_name`/`model`; `todo_stack` + `access_grants` JSON columns), messages stored as msgspec chat message JSON.
|
Async SQLAlchemy + aiosqlite. `MemoryStore`: schema init (+ lightweight SQLite `ALTER` for new columns; `PRAGMA user_version` 4), default local user, sessions (bound to `workspace` path; optional `provider_name`/`model`; `todo_stack` + `access_grants` JSON columns), messages stored as msgspec chat message JSON.
|
||||||
@@ -80,7 +84,7 @@ Async SQLAlchemy + aiosqlite. `MemoryStore`: schema init (+ lightweight SQLite `
|
|||||||
- **`/compact`**: soft-compact tool dumps → model summary (no tools) via the same kind dispatch → **new** session seeded with summary message. Soft-compact keeps the last **12** tool results full-size by default (`DEFAULT_RECENT_TOOL_RESULTS`); older tool payloads shrink first.
|
- **`/compact`**: soft-compact tool dumps → model summary (no tools) via the same kind dispatch → **new** session seeded with summary message. Soft-compact keeps the last **12** tool results full-size by default (`DEFAULT_RECENT_TOOL_RESULTS`); older tool payloads shrink first.
|
||||||
- Events: text_delta, **reasoning_delta**, assistant_message, tool_call/result, max_rounds, **error** (`retryable`/`source`), **cancelled** (`reason`), **usage** (`TokenUsage`).
|
- Events: text_delta, **reasoning_delta**, assistant_message, tool_call/result, max_rounds, **error** (`retryable`/`source`), **cancelled** (`reason`), **usage** (`TokenUsage`).
|
||||||
- Usage: API `usage` from completions (stream with `include_usage`); **char≈token fallback** (~4 chars/token) when omitted; **context size** = last request ``prompt_tokens`` (API preferred); `last_turn_usage` / `session_usage` are **billed sums** (tool rounds re-send history); CLI end-of-turn + `/status`.
|
- Usage: API `usage` from completions (stream with `include_usage`); **char≈token fallback** (~4 chars/token) when omitted; **context size** = last request ``prompt_tokens`` (API preferred); `last_turn_usage` / `session_usage` are **billed sums** (tool rounds re-send history); CLI end-of-turn + `/status`.
|
||||||
- Config ``[agent]``: `system_prompt` (persona; default `DEFAULT_SYSTEM_PROMPT`; `""` omits persona), `tool_directives` (tool playbook; default `DEFAULT_TOOL_DIRECTIVES`; `""` omits playbook; both empty → no system), `mcp_instructions` (fold connected MCP servers' initialize ``instructions`` into the system prompt when tools on; default true), `allow_paths` (static out-of-workspace pre-allow `path → read|write|exec`; never prompts), `auto_continue_limits` (auto-raise tool/PTY limits without prompting; default false), `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_tokens` (default 200k est. tokens). Truncated tool results carry a model hint (`[Truncated (N chars max; M omitted). You may ask for user to increase the limit via configuration file.]`) so the model can ask the user to raise `max_tool_result_chars`; request-time compact shrinks (`hint=False`) keep the terse `...[truncated N characters]` marker.
|
- Config ``[agent]``: `system_prompt` (persona; default `DEFAULT_SYSTEM_PROMPT`; `""` omits persona), `tool_directives` (tool playbook; default `DEFAULT_TOOL_DIRECTIVES`; `""` omits playbook; both empty → no system), `mcp_instructions` (fold connected MCP servers' initialize ``instructions`` into the system prompt when tools on; default true), `[skills] inject_catalog` (fold the skill catalog — name/source/description only — the same way), `allow_paths` (static out-of-workspace pre-allow `path → read|write|exec`; never prompts), `auto_continue_limits` (auto-raise tool/PTY limits without prompting; default false), `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_tokens` (default 200k est. tokens). Truncated tool results carry a model hint (`[Truncated (N chars max; M omitted). You may ask for user to increase the limit via configuration file.]`) so the model can ask the user to raise `max_tool_result_chars`; request-time compact shrinks (`hint=False`) keep the terse `...[truncated N characters]` marker.
|
||||||
|
|
||||||
### Tools (`tools/`)
|
### Tools (`tools/`)
|
||||||
|
|
||||||
@@ -98,7 +102,8 @@ Module-level `@tool` handlers. Call `set_workspace_root()` before use.
|
|||||||
- **`todo`**: LIFO stack of **task groups** (not a queue of tasks) — `todo_push` creates one group of siblings; `todo_pop` removes the whole top group; `todo_update` by id; DFS breakdown push[T1,T2]→push[T1.1…]→pop→push[T2.1…]; stored on session row; open items = unfinished work (`[TODO OPEN WORK]`); all-terminal non-empty = hygiene (`[TODO HYGIENE]`); nag channel via `[agent] todo_nag_strategy` (`developer`/`user` = prose nag; `synthetic_tool` = forged `todo_list` call + **real** `stack.render()` result + ToolCall/Result events; `none`).
|
- **`todo`**: LIFO stack of **task groups** (not a queue of tasks) — `todo_push` creates one group of siblings; `todo_pop` removes the whole top group; `todo_update` by id; DFS breakdown push[T1,T2]→push[T1.1…]→pop→push[T2.1…]; stored on session row; open items = unfinished work (`[TODO OPEN WORK]`); all-terminal non-empty = hygiene (`[TODO HYGIENE]`); nag channel via `[agent] todo_nag_strategy` (`developer`/`user` = prose nag; `synthetic_tool` = forged `todo_list` call + **real** `stack.render()` result + ToolCall/Result events; `none`).
|
||||||
- **`net` / `fetch`**: HTTP GET/POST/PUT/DELETE via niquests (isolated session, not the LLM client). Manual redirects with per-hop SSRF checks (`asyncio` `getaddrinfo`). Private/loopback hosts need instance policy grant (CLI timed confirm; **not** YOLO). Public cleartext HTTP and mutating methods soft-confirm (`YOLO`/`TRUSTABLE`). HTTPS→HTTP redirects blocked unless `allow_http_downgrade`. `user_agent` tool arg (or headers) is never replaced by the default when provided. Caps: body bytes/chars, request body size.
|
- **`net` / `fetch`**: HTTP GET/POST/PUT/DELETE via niquests (isolated session, not the LLM client). Manual redirects with per-hop SSRF checks (`asyncio` `getaddrinfo`). Private/loopback hosts need instance policy grant (CLI timed confirm; **not** YOLO). Public cleartext HTTP and mutating methods soft-confirm (`YOLO`/`TRUSTABLE`). HTTPS→HTTP redirects blocked unless `allow_http_downgrade`. `user_agent` tool arg (or headers) is never replaced by the default when provided. Caps: body bytes/chars, request body size.
|
||||||
- **`mcp`** (`tools/mcp.py`): register one server's advertised tools into the catalog as namespaced `mcp__<server>__<tool>` definitions with `ToolSource(kind="mcp", plugin_id=server)`; tags LOCAL (+ READ_ONLY when the server config sets `read_only`) — never YOLO/TRUSTABLE. `register_mcp_tools` purges prior MCP entries then re-registers every connected server (hosts call it on tool-registry rebuild); connections must already be started by `McpManager`.
|
- **`mcp`** (`tools/mcp.py`): register one server's advertised tools into the catalog as namespaced `mcp__<server>__<tool>` definitions with `ToolSource(kind="mcp", plugin_id=server)`; tags LOCAL (+ READ_ONLY when the server config sets `read_only`) — never YOLO/TRUSTABLE. `register_mcp_tools` purges prior MCP entries then re-registers every connected server (hosts call it on tool-registry rebuild); connections must already be started by `McpManager`.
|
||||||
- **`DEFAULT_TOOLS` / catalog**: access + file + process + vcs + chat + todo + net (`fetch`) for local surface. `ToolTag.READ_ONLY` marks non-mutating tools (`read_file`, `get_truncated`, `listdir`, `glob_paths`, `grep_files`, `tree`, `todo_list`, `wait`, `ask_user_*`, `vcs_*`, `fetch`); `ToolRegistry.clone(read_only_only=True)` selects just those for safe side turns / planning, and `read_only_context()` (in `tools/context.py`) makes `fetch` accept GET only.
|
- **`skills`** (`tools/skills.py`): discovery, reading, and writing of skill directories (`plyngent.skills` owns roots, the `SKILL.md` frontmatter subset, the mtime-cached store, and the inside-the-skill path rule). `skill_list` (names, source root, directory, description, shadowed copies, frontmatter notes), `skill_read` (a `SKILL.md` by name, or one bundled file — reuses `read_file` for ranges/line numbers/`truncate_token`), `skill_search` (regex over every visible skill via `grep_files`, hits rebased on `<skill>/<file>`), `skill_create` (new `SKILL.md` + optional `files`), `skill_edit` (replace / `append` / rewrite one file; a rewritten `SKILL.md` is re-parsed and notes a lost description). Reads need no grant (the host publishes the roots as `WorkspacePolicy.static_read`, so `read_file`/`grep_files` see them while writes stay denied); writes go through **`SkillWritePolicy`** on the instance — denylist first, then `SessionState.skill_write_roots` (a `session` answer), then `[skills].allow_write`, then the host's confirm hook (CLI `prompt_skill_write_confirm`: `o`nce/`s`ession/`n`o, timed, default deny, **not** skipped by YOLO); a host with no hook denies. Grants are per **skill directory**; `[skills].enabled = false` drops the tools from the registry (`SKILL_TOOL_NAMES`).
|
||||||
|
- **`DEFAULT_TOOLS` / catalog**: access + file + process + vcs + chat + todo + skills + net (`fetch`) for local surface. `ToolTag.READ_ONLY` marks non-mutating tools (`read_file`, `get_truncated`, `listdir`, `glob_paths`, `grep_files`, `tree`, `todo_list`, `wait`, `ask_user_*`, `vcs_*`, `skill_list`/`skill_read`/`skill_search`, `fetch`); `ToolRegistry.clone(read_only_only=True)` selects just those for safe side turns / planning, and `read_only_context()` (in `tools/context.py`) makes `fetch` accept GET only.
|
||||||
|
|
||||||
### Prompting (`prompting.py`)
|
### Prompting (`prompting.py`)
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ Shared interactive I/O: `ask` / `choose` / `form` / `confirm` with pluggable bac
|
|||||||
Click app + readline REPL. Entry: `plyngent` / `python -m plyngent`.
|
Click app + readline REPL. Entry: `plyngent` / `python -m plyngent`.
|
||||||
|
|
||||||
- **`plyngent chat`**: provider/model (flags or interactive; Tab via readline in `prompting`); sessions store `provider_name`/`model` and restore on resume; SQLite via `[database]` (file DB under user data when url unset; explicit `url = ":memory:"` kept + warn); workspace-bound; resume latest for cwd/`--workspace` by default (`--new` / `--session`). One-shot: `-p/--prompt` and non-TTY stdin; exit codes 0/1/2/3; `--yes` (YOLO on), `--auto-continue` (auto-raise limits), `--stream/--no-stream`, `--quiet`. Root `--log-level`. Enabled `[mcp]` servers start before the REPL (`_start_mcp_manager`, per-server failures warn on stderr) and are closed on exit (`McpManager.aclose`); each server spawns in its own session/process group (`start_new_session` / `CREATE_NEW_PROCESS_GROUP`), so a terminal Ctrl+C does not kill it.
|
- **`plyngent chat`**: provider/model (flags or interactive; Tab via readline in `prompting`); sessions store `provider_name`/`model` and restore on resume; SQLite via `[database]` (file DB under user data when url unset; explicit `url = ":memory:"` kept + warn); workspace-bound; resume latest for cwd/`--workspace` by default (`--new` / `--session`). One-shot: `-p/--prompt` and non-TTY stdin; exit codes 0/1/2/3; `--yes` (YOLO on), `--auto-continue` (auto-raise limits), `--stream/--no-stream`, `--quiet`. Root `--log-level`. Enabled `[mcp]` servers start before the REPL (`_start_mcp_manager`, per-server failures warn on stderr) and are closed on exit (`McpManager.aclose`); each server spawns in its own session/process group (`start_new_session` / `CREATE_NEW_PROCESS_GROUP`), so a terminal Ctrl+C does not kill it.
|
||||||
- Slash: Click group in `cli/slash.py` + `awaitlet` for async work; Tab completer from registry + ParamType `shell_complete`. `/history` is turn-oriented (`cli/transcript.py`): a turn runs from a user row to the next one, rows carry **display ordinals** (never DB row numbers; restart after `/clear`), the turn's ends (user + final answer) always print in full, `-v` expands every row (one-liners), `-vv` prints full bodies + local rows, `--preview` collapses every row, and `N`/`last [N]` address turns while `--message N` addresses one row. Multiline `"""` … `"""`; `/edit` via `$VISUAL`/`$EDITOR` (blocking only). `/yolo on|off|once` for soft destructive confirms. `/model --persist` / `/models --persist` write model catalog entries into TOML. `/todos` for human show/push/pop/clear of the todo stack. `/mcp` (`list` default) prints per-server status/tool counts + an `instructions` preview when the server sent usage guidance, + last stderr lines; `/mcp reconnect` re-reads config, restarts every enabled server, and rebuilds the tool registry. `/grants` lists directory-access grants (`revoke <index|all>`; config grants point at the TOML); `/status` includes the grant count.
|
- Slash: Click group in `cli/slash.py` + `awaitlet` for async work; Tab completer from registry + ParamType `shell_complete`. `/history` is turn-oriented (`cli/transcript.py`): a turn runs from a user row to the next one, rows carry **display ordinals** (never DB row numbers; restart after `/clear`), the turn's ends (user + final answer) always print in full, `-v` expands every row (one-liners), `-vv` prints full bodies + local rows, `--preview` collapses every row, and `N`/`last [N]` address turns while `--message N` addresses one row. Multiline `"""` … `"""`; `/edit` via `$VISUAL`/`$EDITOR` (blocking only). `/yolo on|off|once` for soft destructive confirms. `/model --persist` / `/models --persist` write model catalog entries into TOML. `/todos` for human show/push/pop/clear of the todo stack. `/skills` (`list` default) prints the discovered skills with their roots, shadowed copies, and frontmatter notes; `/skills search PATTERN` and `/skills read NAME [--file F]` run the matching tool handlers (the slash path binds the tool context itself); `/skills reload` rescans the roots and rebuilds tools. `/mcp` (`list` default) prints per-server status/tool counts + an `instructions` preview when the server sent usage guidance, + last stderr lines; `/mcp reconnect` re-reads config, restarts every enabled server, and rebuilds the tool registry. `/grants` lists directory-access grants (`revoke <index|all>`; config grants point at the TOML); `/status` includes the grant count and the visible skill count (or `off` when `[skills].enabled` is false).
|
||||||
- `/btw` side questions: forked session; main transcript/DB untouched. `--tools=read` (default) exposes only `READ_ONLY` tools under `read_only_context()` (fetch=GET) with the main session view; `--tools=no` off; `--tools=full` whole-registry clone with fresh session. Tools off: default falls back to no tools; explicit `read`/`full` errors. The model gets an `aside` notice as its last message before the question (unsaved exchange + tool scope), shown to the user as `[notice] aside: …`.
|
- `/btw` side questions: forked session; main transcript/DB untouched. `--tools=read` (default) exposes only `READ_ONLY` tools under `read_only_context()` (fetch=GET) with the main session view; `--tools=no` off; `--tools=full` whole-registry clone with fresh session. Tools off: default falls back to no tools; explicit `read`/`full` errors. The model gets an `aside` notice as its last message before the question (unsaved exchange + tool scope), shown to the user as `[notice] aside: …`.
|
||||||
- Explicit `/resume` or `--session` from another workspace prompts: **keep** / **update** / **abort**. A session resumed by a **freshly started process** (`--session` / default latest) also gets a `resume` notice about what a restart drops (PTY sessions, running commands, temp workspaces) — resuming inside a live process pushes nothing.
|
- Explicit `/resume` or `--session` from another workspace prompts: **keep** / **update** / **abort**. A session resumed by a **freshly started process** (`--session` / default latest) also gets a `resume` notice about what a restart drops (PTY sessions, running commands, temp workspaces) — resuming inside a live process pushes nothing.
|
||||||
- Host notices (`agent/notices.py`): system-level messages appended as trailing `developer` rows, never system-prompt edits — `Notice.to_message()` for the model, `.summary` for the user via `cli.display.echo_notice` (`--quiet` hides the user line only). Durable hosts persist them (`ChatAgent.push_notice`); a side agent without memory keeps its notice local. Trailing developer messages never end a turn, so `/retry` still sees an interrupted turn behind a notice or checkpoint.
|
- Host notices (`agent/notices.py`): system-level messages appended as trailing `developer` rows, never system-prompt edits — `Notice.to_message()` for the model, `.summary` for the user via `cli.display.echo_notice` (`--quiet` hides the user line only). Durable hosts persist them (`ChatAgent.push_notice`); a side agent without memory keeps its notice local. Trailing developer messages never end a turn, so `/retry` still sees an interrupted turn behind a notice or checkpoint.
|
||||||
|
|||||||
@@ -155,6 +155,16 @@ max_context_tokens = 200000
|
|||||||
# Auto-raise tool/PTY limits without prompting (same as answering yyy).
|
# Auto-raise tool/PTY limits without prompting (same as answering yyy).
|
||||||
# auto_continue_limits = false
|
# auto_continue_limits = false
|
||||||
|
|
||||||
|
# Optional skills (instruction directories). See doc/skills.md.
|
||||||
|
# [skills]
|
||||||
|
# enabled = true # false = no skill tools, no prompt catalog
|
||||||
|
# paths = ["~/team-skills"] # explicit roots, highest priority
|
||||||
|
# discover = ["plyngent", "claude"] # our user dir + other harnesses' dirs
|
||||||
|
# include_project_roots = true # also <workspace>/.claude/skills
|
||||||
|
# inject_catalog = true # fold name + description into the system prompt
|
||||||
|
# max_catalog_skills = 50
|
||||||
|
# allow_write = false # true = skip the per-write grant prompt
|
||||||
|
|
||||||
# Optional plugins (entry-point names); default load none. See doc/plugins.md.
|
# Optional plugins (entry-point names); default load none. See doc/plugins.md.
|
||||||
# [plugins]
|
# [plugins]
|
||||||
# enable = ["acme"]
|
# enable = ["acme"]
|
||||||
@@ -164,6 +174,8 @@ Per-provider **`timeout`** is passed to the HTTP session for chat/completions, R
|
|||||||
|
|
||||||
Third-party **plugins**: install a package that declares `project.entry-points."plyngent.tools"` (and later other groups), then allowlist the entry-point name under **`[plugins].enable`**. Details: [doc/plugins.md](doc/plugins.md).
|
Third-party **plugins**: install a package that declares `project.entry-points."plyngent.tools"` (and later other groups), then allowlist the entry-point name under **`[plugins].enable`**. Details: [doc/plugins.md](doc/plugins.md).
|
||||||
|
|
||||||
|
**Skills** (instruction directories with a `SKILL.md`): discovered under `[skills].paths`, our own user skills directory, and other harnesses' skill directories (`~/.claude/skills`, plus `<workspace>/.claude/skills` when `include_project_roots`). The catalog — name, source, description — is folded into the system prompt when tools are on; the model reads a `SKILL.md` with `skill_read`, searches with `skill_search`, and creates or updates skills with `skill_create` / `skill_edit` behind a timed `once`/`session`/`no` grant (`[skills].allow_write = true` for a standing one; the path denylist still wins and `--yes`/`/yolo` never skips it). Reading needs no grant: skill roots are read-only roots, so `read_file` / `grep_files` see them too while writes stay denied. Details: [doc/skills.md](doc/skills.md).
|
||||||
|
|
||||||
**MCP servers** (Model Context Protocol over stdio): define servers under `[mcp.servers.<name>]` (`command` + `args`, optional `env`/`cwd`/`timeout`/`read_only`) and their tools become agent tools namespaced `mcp__<server>__<tool>` (LOCAL tags; `read_only = true` also marks them READ_ONLY, eligible for `/btw --tools=read`). Names in `[mcp].disable` stay disconnected. A server may also return optional `instructions` (usage guidance) in its MCP `initialize` response; when tools are on those are folded into the agent system prompt as `MCP server <name> instructions:` blocks (`[agent] mcp_instructions = false` disables), and `/mcp` previews them. In the REPL, `/mcp` lists per-server status and tool counts; `/mcp reconnect` re-reads the config file and restarts every enabled server (adopts newly added ones).
|
**MCP servers** (Model Context Protocol over stdio): define servers under `[mcp.servers.<name>]` (`command` + `args`, optional `env`/`cwd`/`timeout`/`read_only`) and their tools become agent tools namespaced `mcp__<server>__<tool>` (LOCAL tags; `read_only = true` also marks them READ_ONLY, eligible for `/btw --tools=read`). Names in `[mcp].disable` stay disconnected. A server may also return optional `instructions` (usage guidance) in its MCP `initialize` response; when tools are on those are folded into the agent system prompt as `MCP server <name> instructions:` blocks (`[agent] mcp_instructions = false` disables), and `/mcp` previews them. In the REPL, `/mcp` lists per-server status and tool counts; `/mcp reconnect` re-reads the config file and restarts every enabled server (adopts newly added ones).
|
||||||
|
|
||||||
Supported provider presets today:
|
Supported provider presets today:
|
||||||
@@ -272,6 +284,7 @@ Type `/help` in the REPL for the live list. Common ones:
|
|||||||
| `/models` | List config + remote `GET /models` (always re-fetches) |
|
| `/models` | List config + remote `GET /models` (always re-fetches) |
|
||||||
| `/models --persist` | Merge remote catalog into TOML for this provider |
|
| `/models --persist` | Merge remote catalog into TOML for this provider |
|
||||||
| `/todos` | Todo/task stack: list, push, pop, done, clear |
|
| `/todos` | Todo/task stack: list, push, pop, done, clear |
|
||||||
|
| `/skills [list\|search\|read\|reload]` | List discovered skills, regex-search them, print a `SKILL.md` (or one bundled file), or rescan the roots |
|
||||||
| `/grants` | Directory-access grants: list, or `revoke <index\|all>` |
|
| `/grants` | Directory-access grants: list, or `revoke <index\|all>` |
|
||||||
| `/config` | Edit `plyngent.toml` ($VISUAL/$EDITOR or system open); reload after blocking editor |
|
| `/config` | Edit `plyngent.toml` ($VISUAL/$EDITOR or system open); reload after blocking editor |
|
||||||
| `/quit` | Leave the REPL |
|
| `/quit` | Leave the REPL |
|
||||||
@@ -289,7 +302,9 @@ Ctrl+C cancels the in-flight turn, and during the auto-retry countdown it cancel
|
|||||||
|
|
||||||
## Tools (when enabled)
|
## Tools (when enabled)
|
||||||
|
|
||||||
Default registry: file ops (including `tree` with a markdown bullet-list default, `flat` paths or classic `decorated` on demand; default noise-dir skips), `run_argv` / `run_argv_batch` / PTY (POSIX openpty; Windows ConPTY via pywinpty), read-only VCS (git), HTTP `fetch` (GET/POST/PUT/DELETE via niquests; private/loopback hosts need a human policy grant, not YOLO), human prompts (`ask_user_line` / `ask_user_choice` / `ask_user_form`), `wait` (line prompt with timeout; Enter disturbs), `request_directory_access` (out-of-workspace access grants), `get_truncated` (resume any truncated result via its `truncate_token=...`), and todo stack tools (`todo_list` / `todo_push` / `todo_pop` / `todo_update` / `todo_clear`).
|
Default registry: file ops (including `tree` with a markdown bullet-list default, `flat` paths or classic `decorated` on demand; default noise-dir skips), `run_argv` / `run_argv_batch` / PTY (POSIX openpty; Windows ConPTY via pywinpty), read-only VCS (git), HTTP `fetch` (GET/POST/PUT/DELETE via niquests; private/loopback hosts need a human policy grant, not YOLO), human prompts (`ask_user_line` / `ask_user_choice` / `ask_user_form`), `wait` (line prompt with timeout; Enter disturbs), `request_directory_access` (out-of-workspace access grants), `get_truncated` (resume any truncated result via its `truncate_token=...`), skills (`skill_list` / `skill_read` / `skill_search` / `skill_create` / `skill_edit`), and todo stack tools (`todo_list` / `todo_push` / `todo_pop` / `todo_update` / `todo_clear`).
|
||||||
|
|
||||||
|
**Skills** are instruction directories shared with other harnesses: a folder with a `SKILL.md` (YAML frontmatter — `name`, `description`, … — plus a Markdown body) and optional bundled files (scripts, references). Discovery, roots, grants, and the authoring format are documented in [doc/skills.md](doc/skills.md); the short version is that the catalog (name + description) goes into the system prompt, the body is read on demand with `skill_read`, and writing a skill is a separate grant (a timed `once`/`session`/`no` prompt, or `[skills].allow_write`) that is **not** a directory-access grant and is never skipped by YOLO.
|
||||||
|
|
||||||
Safety defaults:
|
Safety defaults:
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -23,11 +23,15 @@
|
|||||||
- config: Plyngent configuration center (TOML), including ``[plugins]`` and
|
- config: Plyngent configuration center (TOML), including ``[plugins]`` and
|
||||||
``[networking]`` (e.g. fetch SSRF Fake-IP CIDR exemptions). Model entries
|
``[networking]`` (e.g. fetch SSRF Fake-IP CIDR exemptions). Model entries
|
||||||
may override provider ``preset`` / ``url`` for mixed API routing.
|
may override provider ``preset`` / ``url`` for mixed API routing.
|
||||||
|
``[skills]`` holds the skill roots, discovery sources, catalog limits, and
|
||||||
|
the standing write grant.
|
||||||
- runtime: ``create_client`` maps effective provider preset → protocol client.
|
- runtime: ``create_client`` maps effective provider preset → protocol client.
|
||||||
|
- skills: Skill discovery (ordered roots, ``SKILL.md`` frontmatter subset,
|
||||||
|
mtime-cached store) and the read/write path rules the tools use.
|
||||||
- agent: Kind-based tool loop, streaming, usage, compact; bridges:
|
- agent: Kind-based tool loop, streaming, usage, compact; bridges:
|
||||||
``responses_bridge``/``responses_dispatch``, ``messages_bridge``/``messages_dispatch``;
|
``responses_bridge``/``responses_dispatch``, ``messages_bridge``/``messages_dispatch``;
|
||||||
``@tool`` / tags / registry. History stays chat-completions-shaped.
|
``@tool`` / tags / registry. History stays chat-completions-shaped.
|
||||||
- tools: Workspace file/process/VCS/chat/todo/net tools + directory-access
|
- tools: Workspace file/process/VCS/chat/todo/net/skill tools + directory-access
|
||||||
grants (`access`); catalog; plugins; instance/session context and views.
|
grants (`access`); catalog; plugins; instance/session context and views.
|
||||||
- prompting: Shared ask/choose/form for CLI and tools.
|
- prompting: Shared ask/choose/form for CLI and tools.
|
||||||
- cli: Click entry, slash registry, REPL, one-shot chat.
|
- cli: Click entry, slash registry, REPL, one-shot chat.
|
||||||
|
|||||||
@@ -26,6 +26,24 @@
|
|||||||
# [networking]
|
# [networking]
|
||||||
# ssrf_assume_public_cidrs = ["198.18.0.0/15"]
|
# ssrf_assume_public_cidrs = ["198.18.0.0/15"]
|
||||||
|
|
||||||
|
# Skills: instruction directories holding a SKILL.md (YAML frontmatter with at
|
||||||
|
# least name/description, then a Markdown body) plus optional bundled files.
|
||||||
|
# Discovery roots in priority order: explicit `paths`, our own user config
|
||||||
|
# directory (<config dir>/skills), other harnesses' skill directories
|
||||||
|
# (~/.claude/skills; plus <workspace>/.claude/skills when include_project_roots).
|
||||||
|
# The catalog (name + description) is folded into the system prompt when tools
|
||||||
|
# are on; the model reads bodies with skill_read and searches with skill_search.
|
||||||
|
# Reading needs no grant (the roots are read-only roots); creating or editing a
|
||||||
|
# skill asks once/session, or stands with allow_write = true. See doc/skills.md.
|
||||||
|
# [skills]
|
||||||
|
# enabled = true
|
||||||
|
# paths = ["~/team-skills", "/srv/skills"]
|
||||||
|
# discover = ["plyngent", "claude"]
|
||||||
|
# include_project_roots = true
|
||||||
|
# inject_catalog = true
|
||||||
|
# max_catalog_skills = 50
|
||||||
|
# allow_write = false
|
||||||
|
|
||||||
# MCP (Model Context Protocol) servers exposed as agent tools.
|
# MCP (Model Context Protocol) servers exposed as agent tools.
|
||||||
# Defining a server opts it in; tools appear as mcp__<server>__<tool> with
|
# Defining a server opts it in; tools appear as mcp__<server>__<tool> with
|
||||||
# LOCAL tags (confirm flow applies). Names in disable stay disconnected.
|
# LOCAL tags (confirm flow applies). Names in disable stay disconnected.
|
||||||
|
|||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
# Skills
|
||||||
|
|
||||||
|
A **skill** is a directory of instructions the agent can find, read, and follow:
|
||||||
|
a `SKILL.md` (frontmatter + Markdown body) plus whatever bundled files it needs
|
||||||
|
— scripts, references, templates. Plyngent discovers skills from your own user
|
||||||
|
directory and from the directories other harnesses install into, so one skill
|
||||||
|
folder can serve several agents.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
<root>/
|
||||||
|
pdf-processing/
|
||||||
|
SKILL.md # required: frontmatter + instructions
|
||||||
|
scripts/extract.sh # optional bundled files
|
||||||
|
references/forms.md
|
||||||
|
```
|
||||||
|
|
||||||
|
`SKILL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: pdf-processing
|
||||||
|
description: Extract text and tables from PDFs; fill forms.
|
||||||
|
version: 1
|
||||||
|
allowed-tools: [run_argv, read_file]
|
||||||
|
---
|
||||||
|
|
||||||
|
# PDF processing
|
||||||
|
|
||||||
|
Use `scripts/extract.sh <file>` for the text layer, then ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roots and discovery
|
||||||
|
|
||||||
|
Roots are scanned in priority order; the **first** skill with a given name wins
|
||||||
|
and the copies it shadows stay listed (`skill_list`, `/skills`) instead of
|
||||||
|
disappearing:
|
||||||
|
|
||||||
|
1. `[skills].paths` — explicit roots (highest priority)
|
||||||
|
2. our own user directory: `<user config>/skills` (`plyngent config path` shows
|
||||||
|
the directory that contains `plyngent.toml`)
|
||||||
|
3. other harnesses: `~/.claude/skills`, and `<workspace>/.claude/skills` when
|
||||||
|
`include_project_roots` is on (`discover = ["plyngent", "claude"]` by default)
|
||||||
|
|
||||||
|
A root may also *be* a skill directory (its own `SKILL.md`), which is handy when
|
||||||
|
you point `paths` at a single skill. Missing roots are not an error; `/skills`
|
||||||
|
lists the ones with no directory.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[skills]
|
||||||
|
enabled = true # false = no skill tools and no prompt catalog
|
||||||
|
paths = ["~/team-skills"] # explicit roots; `~` expands to your home
|
||||||
|
discover = ["plyngent", "claude"]
|
||||||
|
include_project_roots = true
|
||||||
|
inject_catalog = true # fold the catalog into the system prompt
|
||||||
|
max_catalog_skills = 50
|
||||||
|
allow_write = false # true = standing grant for skill writes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontmatter
|
||||||
|
|
||||||
|
The reader covers the YAML subset those files use — top-level `key: value`
|
||||||
|
scalars (quoted, plain, `true`/`false`, integers), inline `[a, b]` lists, block
|
||||||
|
`- item` lists, and `|` / `>` blocks. A plain scalar folds its indented
|
||||||
|
continuation lines. A nested mapping (say a `metadata:` block) is skipped rather
|
||||||
|
than guessed at, and a structurally broken block degrades to "no metadata" with
|
||||||
|
a note: the skill stays listable instead of vanishing.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `name` | Skill name; must be lowercase letters, digits, and single hyphens (max 64). A name that cannot be used falls back to the directory name, with a note. |
|
||||||
|
| `description` | One line shown in the catalog and in `skill_list`. Missing → the first non-heading paragraph of the body. |
|
||||||
|
| `version`, `license`, `allowed-tools`, … | Kept and shown by `skill_list`; `allowed-tools` is **recorded, not enforced** today. |
|
||||||
|
|
||||||
|
## What the agent sees
|
||||||
|
|
||||||
|
- **Catalog** (system prompt, `inject_catalog`): names, sources, descriptions —
|
||||||
|
never the bodies, so a turn only pays for the skill it actually reads.
|
||||||
|
- `skill_list` — what exists, where it came from, shadowed copies, and the notes
|
||||||
|
from repaired frontmatter.
|
||||||
|
- `skill_read NAME [file]` — the `SKILL.md` body (ranges, line numbers, and a
|
||||||
|
continuation token like any other read) or one bundled file, resolved inside
|
||||||
|
the skill directory only.
|
||||||
|
- `skill_search PATTERN [skill]` — regex over every visible skill; hits come back
|
||||||
|
as `<skill>/<file>:<line>: content`.
|
||||||
|
|
||||||
|
## Grants: reading vs writing
|
||||||
|
|
||||||
|
- **Reading needs no grant.** The host registers the roots as read-only roots, so
|
||||||
|
`read_file`, `grep_files`, and `listdir` see skill files too. Writes are denied
|
||||||
|
by the path policy — a skill directory never becomes a write grant.
|
||||||
|
- **Writing needs a grant.** `skill_create` / `skill_edit` ask the human for the
|
||||||
|
skill directory: `o` once, `s` for the rest of the chat, anything else (or a
|
||||||
|
timeout) denies. The prompt is independent of YOLO: `--yes` / `/yolo` never
|
||||||
|
skips it. `[skills].allow_write = true` is the standing grant, and the path
|
||||||
|
denylist (`[agent].path_denylist`) still wins over everything.
|
||||||
|
|
||||||
|
`skill_create` writes a new `SKILL.md` (name, description, body) plus any
|
||||||
|
`files` you list; `skill_edit` changes one file by literal replacement
|
||||||
|
(`old_string` → `new_string`, all matches with `replace_all`), by `append`, or
|
||||||
|
by rewriting it. A rewritten `SKILL.md` is re-parsed, and the reply says so when
|
||||||
|
the description (or the whole frontmatter) is gone.
|
||||||
|
|
||||||
|
## Care and safety
|
||||||
|
|
||||||
|
- A skill is **data, not code**: nothing in a skill body bypasses the confirm
|
||||||
|
flow, the path denylist, or the command denylist. Bundled scripts run through
|
||||||
|
the normal process tools, so they still need whatever grant those require.
|
||||||
|
- Third-party skills (other harnesses' directories, shared repos) are untrusted
|
||||||
|
input. Read before you follow, keep `paths` pointed at directories you trust,
|
||||||
|
and prefer `allow_write = false` + per-write prompts when a skill comes from
|
||||||
|
outside.
|
||||||
|
- Keep skills small and specific; long bodies are better split into a short
|
||||||
|
`SKILL.md` plus reference files the model reads on demand.
|
||||||
Reference in New Issue
Block a user