feat(skills): rebuild the skill system for the multi-user model
Nightly Build / build (push) Successful in 8m6s

Per blueprint/skill-project.md: the old single-namespace, hand-maintained
index is gone, replaced by a read-only, two-scope tree whose index is a
runtime function of its content.

- skills/ index generated at runtime (crates/skald-core/src/skills/:
  inventory, install, validate, watch), injected through the new
  <!-- SKILLS_LIST --> placeholder in AGENT.md (agents/common/skills.md);
  meta.json inject_skills flag removed. 11 chat/task agents carry the
  include, the 4 system agents do not.
- Two trees, both read-only in both directions: skills/shared/{id} (the
  group's) and skills/{username}/{id} (one member's own, on the stable
  userid). The root is closed too: UserFs::SkillMounts + RouteError (alias
  probe, plain-denied paths, no home fallback) and a per-user
  .skills-root/{userid} container mount with the two scope mounts nested
  inside, plus the fifth self-heal axis (skills_mounted).
- Agent verbs: skill_register/skill_delete (Config group, global scope
  behind the new skill.manage capability), fetch_repo for public repos,
  list_items(type="skills"); reads are plain read_file on the printed
  path. Seeded @fs_read skills/* allow.
- Freshness: a digest-gated watcher on the two trees emits
  SystemEvent::SkillsChanged, whose subscriber rebuilds the frozen prompt
  prefix via Skald::invalidate_prompt_prefix; in-process writers invalidate
  directly.
- The build ships no skills: the three bundled skills (ics2json,
  mcp-builder, skill-creator) and skills/index.md are removed, skills/ is
  instance data (gitignored, not packaged, no longer pruned by update.sh).
- Docs: skills.md, agents.md, shared-folders.md added; docs/index.md and
  agents/README.md updated.
This commit is contained in:
Daniele
2026-08-08 23:05:35 +01:00
parent 71e1a26b08
commit c27da4e6ab
88 changed files with 4624 additions and 9546 deletions
+81
View File
@@ -0,0 +1,81 @@
# Agents
An **agent** is a role the assistant can play: a name, an icon, a system prompt that shapes its personality and skills, and a set of tools. Every conversation with the assistant is a conversation with **one** agent — the same engine, a different persona.
Agents are defined as plain files in the `agents/` folder on the server (one subfolder per agent: a `meta.json` with the name and description, an `AGENT.md` with the prompt, optionally an icon). The app discovers them at startup and **re-reads the prompt files on every use**, so editing an agent's `AGENT.md` on the server takes effect without restarting anything.
There are three kinds of agent, and the difference is *who starts the conversation*:
| Kind | Who talks to it | Count |
|------|-----------------|-------|
| **Chat** | You, directly | 3 |
| **Task** | The assistant, on your behalf (delegation) | 8 |
| **System** | Nobody — it runs on a schedule, in the background | 4 |
## Which agent are you talking to?
When you start a conversation, which chat agent it lands on is decided by your **role** — not by you picking one from a list:
- Members of most roles get the **Assistant** — the general-purpose agent that helps with anything, remembers what matters in memory, and delegates specialised work (see below).
- Members of the **children's role** get the **Companion** — a warmer, gentler assistant that adapts its tone and vocabulary to the child's age.
- Conversations about a **project** get the **Project Coordinator** — the same agent who runs the project's chat, holding the project's full context.
An admin can change a role's default assistant in the role editor (sidebar → **Roles** → edit a role → **Default assistant**). Leaving it empty means "the Assistant". A role change applies to **new** conversations, from the member's next login — an existing conversation keeps the agent it started with.
If a user asks "who am I talking to?" or "why does my assistant talk differently from theirs?", the answer is: the agent their role defaults to, and it can be changed by the admin — not by the user, and not by asking the assistant (the assistant cannot change its own role).
## The Agents page
Sidebar → **Agents** shows every agent on this instance, in three sections (Chat / Task / System), as cards with the agent's icon, name, and a short description.
Clicking an agent opens its detail page with:
- **The prompt** — the full `AGENT.md` text the agent runs under, rendered as Markdown. This is not secret: it is what the agent is told to be and do, and reading it is a good way to understand why an agent behaves the way it does. (The Assistant's prompt, for example, tells it to keep personal facts in memory, to prefer `user-memory/` notes, and to delegate to task agents when a job needs a specialist.)
- **The models** — which LLMs the agent can run on, and how it picks one (see [How the model is chosen](#how-the-model-is-chosen)).
The System section lists the background agents too — they are invisible in the chat but visible here, with their prompts. For what they *do* and when they run, see [system-agents.md](system-agents.md).
## The task agents: the specialists
Eight agents exist to do specific jobs, and the chat agent calls on them automatically when the job matches — you never talk to them directly. You *trigger* one simply by asking: *"use the researcher to find me…"*, *"get the code explorer to look at…"*, or just describing the task in a way that matches a specialist's job. The assistant recognises the match, delegates, and brings the result back into the conversation.
| Agent | What it is for | Where its output goes |
|-------|----------------|-----------------------|
| **Researcher** | Multi-step web research, with sources | A structured summary in the chat; findings saved to the scratchpad, optionally to `data/research/` |
| **Business Analyst** | Stress-tests a business idea or plan against the evidence you provide; GO / NO-GO / PIVOT verdict | A critique report (path you choose, or the scratchpad) |
| **Code Explorer** | Studies code, investigates bugs, analyses architecture — analysis only, never edits | A structured Markdown report in `data/explorer/` |
| **Spec Writer** | Turns a rough idea into a detailed, unambiguous written specification | A Markdown spec document (never code) |
| **Software Architect** | Plans a code change end-to-end before anything is touched | An implementation plan, possibly delegating the edits to the engineer |
| **Software Engineer** | Writes and edits source files to implement a decided change | The code changes themselves |
| **Tech Lead** | Takes project requirements and builds the whole thing, decomposing the work and orchestrating architect + engineer | The completed implementation |
| **Generalist** | Carries out well-defined hands-on work — file edits, shell commands, batch operations — exactly as instructed | The finished work |
Three things worth knowing about delegation:
- **It is ordinary conversation.** The child agent's work happens in its own context, but what you see is the flow: the assistant calls the specialist, the specialist reports back, the assistant answers you. You can watch it happen in the chat.
- **The specialists have the same rules.** They run with the same tool set, the same security groups and the same approval cards — a specialist wanting to write a file in a shared folder will ask for confirmation exactly as the assistant would. (The Conversation Review agent is the exception by design: it has no tools at all, see [system-agents.md](system-agents.md).)
- **They cannot be summoned from the void.** The assistant decides whether and when to delegate — there is no user-facing list to pick a specialist from, and calling one yourself is not a thing you can do (nor should need to).
## How the model is chosen
Every agent declares a **strength** — how powerful a model it should run on (from *very low* to *very high*). When no model is pinned, the app picks the best available model at or above that strength — so a lightweight background task uses a small model, and the most demanding specialists (Architect, Tech Lead) ask for the strongest.
A specific conversation can **pin** a model instead, per conversation, with the model picker in the chat. Pinning overrides the strength choice for that conversation only.
## Custom agents (admin)
Agents are data, not code — an admin can add a new one by creating a folder on the server:
- `agents/<id>/meta.json` — the name, description, type (`chat`, `task` or `system`), strength, and optionally an icon file.
- `agents/<id>/AGENT.md` — the system prompt (the `name` given in `meta.json` is what users will see; the folder name is the internal id).
No restart and no rebuild: the app discovers the new agent and picks up prompt edits on the next use. An icon (a square image) is served automatically when declared in `meta.json` — optional, but it is what shows on the Agents page and in the chat.
If a user asks for "a different assistant", the honest answer is: there is no UI to create one — an admin can add a custom agent by hand on the server, and this guide tells them how, or the user can be pointed at the role's default-assistant setting instead.
## Notes
- **What the Assistant knows about you.** At the start of a conversation, the chat agent reads a few memory notes automatically: your profile and the private-memory index (`user-memory/`), and the group's shared-memory index. That is *in addition to* whatever you say — it is how the agent "remembers" between conversations. The Project Coordinator additionally reads the project's own `SKALD.md` when one exists, so it arrives already knowing the project. See [memory.md](memory.md).
- **The system agents are invisible on purpose.** They run on a schedule, not in a conversation, and they never talk to you — they notify you when something needs attention, and their work and settings live on the System agents page (`#system-agents`). See [system-agents.md](system-agents.md).
- **Prompts are not secrets.** Nothing on the Agents page is hidden from the user who can see it — if someone asks "what is the assistant told to do?", the answer is "read it on the Agents page".
- **Agents cannot change themselves.** An agent's prompt is a file on the server; the agent cannot edit it, and a user cannot make an agent "become" another agent by asking. New conversations, new agent — the mapping is decided by the role.
+4 -1
View File
@@ -4,19 +4,22 @@ This folder is written for **you, the assistant**, not for the human directly. I
Keep answers grounded in what's actually enabled and configured for this instance — check with the relevant tool (e.g. list installed/enabled plugins) rather than assuming everything described here is turned on. A feature documented here may not be enabled on this particular instance.
This index will grow over time. Right now it covers the interface, memory, projects, background tasks, system agents, access grants, connectors, voice input and plugins; more sections (agents, security groups, shared folders…) will be added later.
This index will grow over time. Right now it covers the interface, agents, memory, projects, shared folders, background tasks, system agents, access grants, connectors, skills, voice input and plugins; more sections (security groups…) will be added later.
## Features
| Document | What it covers |
| --- | --- |
| [memory.md](memory.md) | Private and shared memory: what goes where, the indexes and history log, why some shared facts can't be changed on request |
| [agents.md](agents.md) | Agents: the three kinds (chat, task, system), which one you are talking to and why, the specialist agents the assistant delegates to, how the model is chosen, and adding a custom agent |
| [projects.md](projects.md) | Projects: shared folders with their own assistant chat, a live file explorer, and member sharing |
| [shared-folders.md](shared-folders.md) | Shared folders: admin-managed folders with no chat of their own — who sees them, read vs write access, why the assistant asks before touching them, and when to choose a project instead |
| [system-agents.md](system-agents.md) | Background agents that run on a schedule (event triage, the two memory lints, the nightly conversation review of a supervised account): what they watch, why they only ever report, why a run can be skipped, and their settings |
| [tasks.md](tasks.md) | Background tasks: the strip above the message box, following one live, stopping one, answering the approvals and questions they raise, and how every outcome comes back to the conversation |
| [settings.md](settings.md) | The admin's Config page: interface language, the compaction model picker, debug mode |
| [access.md](access.md) | Who can use which plugin or connector: the open default, removing access per person, and the role switch that keeps children out of it |
| [connectors.md](connectors.md) | Connectors (MCP servers): shared vs per-user, setting one up in the UI, the sign-in and QR-pairing flows, and what to do when one is not working |
| [skills.md](skills.md) | Skills: instruction folders the assistant loads on demand — where they live, how to read and run one, and the contract for writing, installing and downloading one |
| [voice.md](voice.md) | Voice input: configuring a transcription model, and why the microphone button does nothing unless the page is served over HTTPS or localhost |
| [interface.md](interface.md) | The desktop interface: collapsing the sidebar to an icon-only strip to make room for documents |
+90
View File
@@ -0,0 +1,90 @@
# Shared folders
A **shared folder** is a folder on the server that several members of the group can use together — a single place for documents everybody needs, instead of each person keeping their own copy and asking for the latest version by hand.
Examples: a shared recipe collection, the household's documents (bills, contracts, manuals), a folder where members drop files for the whole group to see.
Shared folders are **managed by an admin**: an admin creates them, decides who is a member and what each member may do in them. There is **no owner** — the person who created a folder has no special rights over it; the admin can change or remove anyone, themselves included.
Two things shared folders are *not*, so expectations stay right:
- They have **no chat of their own** — the files are shared, not a conversation. (That is what Projects are for; see [Shared folders vs Projects](#shared-folders-vs-projects) below.)
- They have **no file explorer page** in the web app. Members work with the files through the assistant, and open individual files in the file viewer when the assistant shows them (see [Working with the files](#working-with-the-files)).
## Creating a folder (admin)
1. Open **Shared Folders** in the sidebar (admin only; members do not see this page).
2. Click **New Folder** and fill in:
- **Name** — a short, simple name with no spaces or punctuation: `recipes`, `documents`, `holiday-pics`. The name becomes the folder's path, so it must be a single word (no `/`, `\`, `.` or `..`). It **cannot be changed later** — pick carefully.
- **Description** — what the folder is for, in plain words. This is not decoration: it is what the assistant reads to understand the folder (see [What the assistant knows](#what-the-assistant-knows)). A good description: *"Family documents: bills, contracts, manuals — everyone can read, only Marta writes."*
3. Save. The folder is created on the server immediately.
There is no step 4: a folder starts **empty**, with **no members** — even the admin is not a member until added. Add members next (see below).
## Who can see it: members
A folder is visible only to its members. The admin adds members from the folder's row on the Shared Folders page, choosing each person's access level:
- **Read** — can open and read the files, and ask the assistant to work with them. Cannot create, edit or delete anything.
- **Read & write** — everything Read gives, plus creating, editing and deleting files (directly or through the assistant).
Two things worth knowing about membership:
- **Changes apply immediately.** Adding, removing or changing a member takes effect right away — the other person does not need to log out and back in.
- **Removing access is the only way to take files away** — and it works: a person who is no longer a member can no longer see the folder, its files, or ask the assistant about them.
## Working with the files
There is no file explorer for shared folders — no grid of files, no upload button. The files live on the server, and members reach them through the assistant:
- **Ask the assistant** — "what's in the recipes folder?", "add this note to documents", "send me the manual for the boiler". The assistant knows which folders you belong to, can list their contents, open and search files, and — if you have read & write — create and edit them.
- **Open a file** — when the assistant shows you a file from a shared folder, it opens in the usual file viewer (Markdown rendered, images, PDFs, text), exactly like any other file. You can read it there; editing in the viewer is available if you have read & write access.
A practical consequence: if a member wants a file *from* a shared folder, the assistant is the way to get it — there is no download button on the folder itself. (An admin can of course reach the folder directly on the server, but members should not need to.)
## What the assistant knows
At the start of every conversation, the assistant sees a table of the shared folders you belong to, with four columns:
| Path | Access | Shared with | Description |
|------|--------|-------------|-------------|
| `shared/recipes` | read-write | — | Family recipes, everyone can add |
| `shared/documents` | read-only | Anna, Luca | Bills and contracts |
So the assistant knows the folder exists, **your** access level in it, **who else** can see it, and what the admin wrote in the description — and nothing else. It does not read the files on its own: it looks inside only when you ask, and it may ask you to confirm that something belongs in a shared folder before putting it there (see below).
This is why the description matters: it is the folder's only explanation, and a folder with an empty description is a folder the assistant cannot reason about. If you are an admin and a shared folder has no description, editing it (Shared Folders → the folder → edit) is the most useful thing you can do with it.
## The approval cards
The assistant never changes a shared folder on its own initiative — and even on your request, **reading and writing in a shared folder asks for your confirmation first**, as a small card you answer in the chat (or in the Inbox, or from your phone, depending on where you are talking).
This is deliberate and it applies to *everyone*, the admin included:
- **Reads ask too**, not just writes. A shared folder may contain things other members wrote, and the system does not assume you want the assistant browsing it freely.
- The card shows exactly what the assistant wants to do — open this file, create that one, change this line — with **Approve** and **Deny** buttons. Answering is the whole flow; you do not need to do anything else.
- **If you deny**, the assistant simply does not do it and moves on — nothing is forced.
If this feels like a lot of questions, remember the trade-off is the point: shared folders are the one place where one person's words become *everyone's* files, so every step is a conscious one. (Projects work differently — see below.)
## Shared folders vs Projects
The two features look similar — a shared place for files with per-member access — but they solve different problems:
| | Shared folder | Project |
|---|---|---|
| Who manages it | An admin (no owner; anyone can be removed) | The owner (a member) and read & write members |
| Where it lives in the chat | No chat of its own | Its own conversation with the assistant (`project-{id}`), plus extra tabs |
| Files | No explorer page; work through the assistant | A live file explorer with upload, rename, delete, ZIP download |
| Assistant's access | Every read/write asks for confirmation | Reads and writes are frictionless (only the folder's membership limits them) |
| Typical use | A place to *keep* shared documents | A place to *work together* on something |
When to use which, in one line: if the point is "we have documents here", a shared folder; if the point is "we are working on something here", a project. And the two combine naturally — a project's chat can refer to shared folders, and the assistant can copy files between them if you have the right access.
## Notes
- **The name never changes.** A shared folder cannot be renamed (there is no rename button). To "rename" one, an admin creates a new folder and moves the files into it — which changes the folder's path and requires re-adding members.
- **Deleting a folder does not delete the files.** When an admin deletes a shared folder, only the sharing is removed: the folder stays on the server, and an admin can remove it by hand if that is really intended. Say this plainly if a user believes deleting the folder destroyed its contents.
- **The Shared Folders page is admin-only.** Members never see it, and a member cannot create folders, add members, or change access levels. Direct them to the admin rather than trying to do any of it on their behalf.
- **The assistant can copy files *into* a shared folder** (with your approval) — a good way to publish something from your private space to the group. The reverse works too, if you are a member.
- **A description is not a substitute for access.** Writing "everyone may read this" in the description tells the assistant the intent, but membership is still decided by the admin on the Shared Folders page — the assistant cannot grant access, only tell you who currently has it.
+69
View File
@@ -0,0 +1,69 @@
# Skills
A skill is a **folder of instructions and resources** that you load on demand, when a task calls for it — a procedure written once and followed every time, instead of re-deriving the steps in each conversation. A skill adds no tools and starts no processes: it is knowledge you read, then apply with the tools you already have.
## Where skills live
Skills are installed in one of two read-only trees:
| Path | Whose | Who installs |
| --- | --- | --- |
| `skills/shared/<id>/` | the whole group — every member sees these | an admin |
| `skills/<username>/` | one member's own | that member |
Both trees are **read-only**, everywhere and for everyone. You cannot create or edit files under `skills/` — not with the file tools, not from the shell, not even with `sudo`. A skill is **installed**, never written in place; the only way in is `skill_register` (below). To modify a skill you copy it out, edit the copy, and register it again.
## Using a skill
Your prompt already carries the index of the skills you can see: one line each, with the full path of its `SKILL.md` and a short description of when to use it. When a task matches — even partially — **read the skill before doing the work**: `read_file skills/<scope>/<id>/SKILL.md`, then follow its instructions.
To run a skill's script, use `execute_cmd` with `workdir` set to the skill's folder (e.g. `workdir: "skills/shared/ics-import"`). A skill cannot write next to itself — the tree is read-only — so scripts must write their output, caches and state to your home (`~`) or `/tmp`, never into the skill folder.
To see exactly what is installed, with full descriptions and per-skill health: `list_items` with `type="skills"`.
## Creating a skill — the authoring contract
A skill folder looks like this:
```
<skill-name>/
├── SKILL.md # required
├── scripts/ or *.py, *.js in the root # optional executables
├── references/ # optional documents to read on demand
└── assets/ # optional templates, examples
```
`SKILL.md` opens with a YAML frontmatter block, then the instructions in Markdown:
```markdown
---
name: ics-import
description: Download an iCalendar (ICS) feed and turn it into JSON or a table. Use whenever the user gives you a calendar URL or asks to import, inspect or summarize events from an ICS link.
---
# ICS import
1. Run `python3 scripts/ics2json.py <url>` from this folder...
```
Rules — every one of them is **checked at installation**, and a folder that breaks one is refused with a message naming the problem:
- **`name`**: lowercase letters, digits and hyphens only, at most 64 characters. It becomes the installed folder's name — so the working copy may be called `draft-2`, but what gets installed is named after the frontmatter.
- **`description`**: required, at most 1000 characters. This is the *use condition* — the only thing the model sees when deciding whether the skill is relevant. Write it assertively: say exactly **when** to reach for the skill, and err on the side of triggering too easily rather than too rarely. Save the detail for the body.
- **Write it in English** — the instructions, the frontmatter, the comments. Everything the model reads is English.
- **Self-contained**: no symbolic links; at most 500 files and 8 MiB in total. A skill holds instructions, scripts and reference documents — bulk data belongs in a home or a project.
- Create the folder **somewhere you can write** — your home, a project or a shared folder. **Not in `/tmp`** or anywhere else in the container-only filesystem: installation copies the folder from the host side and cannot see those paths.
- A script that sticks to the Python/Node standard library and the preinstalled command-line tools works always. One that needs PyPI or npm packages **must say so in the body** — those packages are installed by hand (`sudo pip install …`) and are lost when the container is recreated.
## Installing, updating, deleting
All three go through tools, and all three ask a human for approval first — the approval card shows the full `SKILL.md`, the file list and where the skill is going:
- **Install**: `skill_register(scope, path)``scope` is `"mine"` (your own tree) or `"global"` (everyone's; requires the admin capability). The folder is validated, copied in one atomic move, and appears in the prompt index immediately.
- **Update**: register again with the same `name` in the same scope — the old copy is replaced. That is the only way a skill changes.
- **Promote to the group**: register an already-installed skill with `scope: "global"`, e.g. `skill_register("global", "skills/maria/ics-import")`.
- **Delete**: `skill_delete(scope, id)` — the folder is removed, with no recycle bin. Use `list_items` with `type="skills"` to find ids.
## Getting a skill from a public repository
`fetch_repo(url, sub_path, destination)` downloads a subtree of a **public git repository** into a writable folder of yours — shallow, without the `.git` history. It installs nothing: if what you downloaded is a skill, review the files and then call `skill_register` on the destination folder. Every download leaves a `.source.json` ticket in the destination recording the URL, the sub-path and the exact commit it came from, so "where did this come from?" always has an answer.