# Skills

On-demand modular knowledge between role and tools. The framework's answer to: **how do I keep multi-step procedures and domain knowledge out of the always-on system prompt without losing them when they're actually relevant?**

The `com.tnsai.skills` package in `tnsai-core` ships the primitives:

- **`Skill`** — record carrying a name, a description (the trigger phrase the resolver scores against), the markdown body, and per-skill scoping (`allowedTools`, `userInvocable`, `disableModelInvocation`).
- **`SkillStore`** — SPI for discovery; the framework ships `InMemorySkillStore` (test seam) and `FileSystemSkillStore` (Claude Code-compatible `<root>/<skill>/SKILL.md` layout).
- **`SkillResolver`** — picks top-N candidates per user turn; defaults to `KeywordSkillResolver` (no extra round-trip), upgradeable to `LLMSkillResolver` (semantic match, one extra LLM call per turn).
- **`SkillManager`** — per-agent facade combining store + resolver + session state. Handles `/skill-name` parsing, invocation source gating, and prompt-section rendering.
- **`SkillSession` + `ActiveSkill`** — runtime state tracking which skills the agent has activated this session.
- **`SkillScopedToolCallFilter`** — `ToolCallFilter` that enforces the union of `allowed-tools` across active skills.

Skills sit between the always-on **role** layer and the always-on **tool** layer:

| Layer | Loaded | Granularity |
|---|---|---|
| Role / CLAUDE.md | Always | Stable agent identity |
| Tool | Always | Atomic action |
| Capability | Always (compile-time) | Method-level declaration |
| RAG hit | Per query (similarity) | Evidence text |
| **Skill** | **On demand (intent-matched)** | **Multi-step procedure** |
| Hook | Always (system gate) | Cross-cutting policy |

This is the "Skills" layer in Claude Code's 5-layer architecture (CLAUDE.md / Skills / Hooks / Subagents / Plugins). TnsAI was missing it before TNS-289.

## Why a separate layer

Three forces motivate skills as a distinct primitive:

1. **Token budget hygiene** — `RoleSpec` and `CLAUDE.md` content rides every prompt. Procedures that only matter when invoked ("deploy procedure", "API design conventions", "customer escalation protocol") shouldn't pin tokens on every turn.
2. **Author-time portability** — Claude Code, Cursor, and other tools are converging on the `agentskills.io` `SKILL.md` standard. A skill authored once works across every framework that supports it.
3. **Per-skill scoping** — `allowed-tools` declares which tools a skill expects to use; `userInvocable` and `disableModelInvocation` declare who may activate it. Both are recorded on the activation event and enforceable through `SkillScopedToolCallFilter`.

## Quick start

FileSystemSkillStore reads the Claude Code-compatible layout
`<root>/<skill-name>/SKILL.md`. Wiring a store auto-upgrades the
policy from `OFF` to `AUTO`. Override with `.skillResolverPolicy(...)`
if you want `MANUAL_ONLY` or `OFF`. `build()` requires the accountability trio (TNS-298).

<!-- java-contract: src/main/java/com/example/tnsai/docs/SkillsBuilderExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.accountability.AuthorityScope;
import com.tnsai.accountability.LiabilitySink;
import com.tnsai.agents.Agent;
import com.tnsai.agents.AgentBuilder;
import com.tnsai.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.roles.Role;
import com.tnsai.skills.FileSystemSkillStore;
import com.tnsai.skills.SkillStore;

import java.nio.file.Path;

public final class SkillsBuilderExample {
    private SkillsBuilderExample() {}

    public static Agent build(
        LLMClient llm,
        Role myRole,
        AgentPrincipal principal,
        LiabilitySink sink,
        AuthorityScope scope
    ) {
        SkillStore store = new FileSystemSkillStore(Path.of(".tnsai/skills"));
        return AgentBuilder.create()
            .id("research-agent")
            .llm(llm)
            .role(myRole)
            .skillStore(store)
            .principal(principal)
            .liabilitySink(sink)
            .authorityScope(scope)
            .build();
    }
}
```

From this point on:

- The resolver runs on every chat turn against the registered skill descriptions; the top `maxActiveSkills` candidates appear in the per-message system prompt under `## Skill candidates for this turn`.
- The user types `/deploy staging` to manually activate a skill — the framework intercepts BEFORE the LLM round-trip, so manual invocation costs zero LLM tokens.
- Once activated, the skill's substituted body lives in the system prompt under `# Skills > ## Active skill bodies` for the remainder of the session.
- `agent.invokeSkill("name", args, env)` lets framework code activate a skill programmatically, bypassing `userInvocable=false`.

## SKILL.md format

```markdown
---
name: deploy
description: Production deploy procedure with rollback support
when-to-use: When the user asks to ship a build to prod or staging
allowed-tools:
  - bash
  - kubectl
argument-hint: <environment>
arguments:
  - environment
disable-model-invocation: false
user-invocable: true
---
# Deploy procedure

1. Verify CI is green.
2. `kubectl apply -f manifests/$0/`
```

The `$0` placeholder gets substituted with the first positional argument when the skill is invoked. See [Skill format](skill-format.md) for the full frontmatter reference.

## Resolver policies

| Policy | Behaviour |
|---|---|
| `AUTO` (default when a store is wired) | Resolver runs on every user turn; top-N candidate descriptions appear in the system prompt; the LLM may invoke via the synthetic `invoke_skill` tool; users may invoke via `/skill-name`. |
| `MANUAL_ONLY` | Resolver does NOT run; only `/skill-name` and `agent.invokeSkill(...)` activate skills. Useful when the deployment wants deterministic skill loading. |
| `OFF` | Skill layer disabled. Framework default when no store is wired. |

## Lifecycle

1. **Discovery** — store enumerates registered skills at startup. Only the `description` field is in the always-on system prompt.
2. **Resolution** — per user message, `SkillResolver` ranks candidates by relevance.
3. **Activation** — user invocation or model tool call moves the full body into context for the rest of the session.
4. **Re-activation** — invoking an active skill again replaces the previous snapshot; the latest invocation's substituted body wins.

`SkillActivationEvent` (sealed branch of `TnsAIEvent`) is emitted on every activation, carrying the source (`USER_SLASH_COMMAND` / `MODEL_TOOL_CALL` / `PROGRAMMATIC`), the skill name, and the supplied arguments.

## Per-skill tool scope

When a skill declares `allowed-tools`, callers can attach `SkillScopedToolCallFilter` to the agent so tool calls outside the union of active-skill `allowed-tools` are blocked with a `Guide` action that names the active skills:

```java
agent.setToolCallFilter(new SkillScopedToolCallFilter(agent.getSkillManager()));
```

The filter is permissive when no active skill declares `allowed-tools` — the field is opt-in scoping, not a default constraint.

## Subagent context fork

`SkillManager.preloadFrom(parent)` copies the parent's active-skill snapshots into a child manager so `AgentGroup`-spawned subagents inherit the procedural context their parent had loaded. Mirrors Claude Code's `context: fork` pattern. Parent's snapshot wins on name collision; the child's pre-existing activations are preserved on top.

## What's not in this layer (deferred)

- **`paths` glob auto-activation** — file-context-aware activation; v2 (the v1 trigger surface is user-message-aware via the resolver)
- **Plugin distribution** — packaging skills into the Plugins layer; tracked separately
- **`context: fork` semantics for full isolation** — current preload is a copy, not a fork; v2
- **GUI skill registry** — visual catalog in `TnsAI.Web`; v3
- **Live file-watcher** — call `FileSystemSkillStore.refresh()` instead

## See also

- [Skill format](skill-format.md) — full SKILL.md frontmatter reference and substitution rules
- [Registration](registration.md) — `AgentBuilder` API + custom resolvers + custom stores
- [Hooks](../../security/index.md) — skill activation events flow through the hook bus
- [Approvals and Annotations](../../security/approvals-and-annotations.md) — `@ApprovalRequired` works alongside skills (approvals gate access; skills supply the procedure)
- [Accountability](../../security/accountability.md) — `SkillActivationEvent` rides the same trace as the resulting liability records
