Feature Specification: Skill Manager
Feature Branch: feature/skill-managerCreated: 2026-05-22 Status: Accepted Input: User description: "Coffer manages portable AI skills using the open AgentSkills standard (agentskills.io). One canonical store lives at ~/.coffer/skills/; per-agent visibility is a directory symlink/junction into the skills/ subfolder of each agent's config directory. Users can import skills from local paths then enable or disable each skill per registered agent. v1 supports Claude Code and Codex CLI as sync targets (each registered as a Resource of kind agent per spec 004-agent-registry)."
User Scenarios & Testing
User Story 1 — Import an existing skill folder (Priority: P1)
A developer already has skills in ~/.claude/skills/ (or elsewhere on disk). They import them into Coffer to make them portable across agents and centrally managed.
Why this priority: Migrating existing assets is most users' day-zero need. Without import, Coffer has no skills to manage.
Independent Test: From the command line, import an existing skill folder; verify the canonical copy exists at ~/.coffer/skills/<name>/; verify the skill appears in coffer skill list.
Covering scenarios:
- import a valid skill folder
- reject import when SKILL.md is missing or has invalid frontmatter
- reject import on duplicate skill name (unless overwrite is requested)
- re-import with overwrite replaces the existing skill
- reject import containing path-escape symlinks
User Story 3 — Enable a skill for a specific agent (Priority: P1)
The developer wants this skill available in Claude Code but not in Codex. They enable per agent, and Coffer creates a directory symlink into the skills/ subfolder of that agent's config directory.
Why this priority: This is the core "unify management" value. Without per-agent enable, Coffer has no advantage over copying files manually.
Independent Test: Register a Claude Code agent (per spec 004); import a skill; enable for that agent; verify a directory symlink appears at <config_dir>/skills/<skill-name> pointing to ~/.coffer/skills/<skill-name>/.
Covering scenarios:
- enable a skill for a registered agent
- disable a skill for an agent (link removed, master untouched)
- enable for multiple agents (multiple links to one master)
- refuse to overwrite an existing non-Coffer file at the target without
--force
User Story 5 — Detect and report drift (Priority: P2)
Files in agents' config_dir/skills folders can be tampered with (deleted, replaced, edited). The developer needs to see what's out of sync and decide what to do.
Why this priority: Trust in the sync engine depends on transparency about disagreement.
Independent Test: Manually delete a symlink in an agent's config_dir/skills folder; run coffer skill verify; observe the drift report identifies the missing link with a suggested remedy.
Covering scenarios:
- detect missing link at target
- detect tampered link (regular file or symlink to a different target)
- detect missing master folder
- detect orphan master (folder on disk but no DB record)
- no automatic remediation without explicit user action
User Story 6 — Manage skills through the desktop app (Priority: P2)
The user opens Coffer, sees the Skills page rendered as a data table (search, filter, pagination, row multi-select for bulk actions), can import via file picker and browse the list. The Skills page manages the skill resource itself, not its per-agent bindings: clicking a skill opens a detail view with an Overview metadata tab and a Files tab (file tree + a read-only file viewer that renders Markdown and shows other text files raw). The viewer does not edit content; to change a file the user opens it (or its containing folder) in their own external editor or file manager — every file and folder offers "open in external editor" and "reveal in file manager" affordances (daemon-backed on the web, native on the desktop). Per-agent enable/disable lives on the agent detail page — the agent's "Skills" tab lists the skills bound to that agent with per-binding toggles.
Why this priority: Non-CLI users need a visual surface for daily management.
Independent Test: Open desktop app → Skills → import a folder via picker → see it listed in the table → open the agent detail page → its Skills tab → toggle the skill enabled for that agent → confirm the symlink exists on disk.
Covering scenarios:
- import a skill via desktop file picker
- toggle per-agent enable via desktop toggle
- surface drift count via a UI notification
User Story 7 — Same operations from the command line (Priority: P2)
The developer scripts skill setup across machines via coffer skill ... subcommands with --json output.
Independent Test: A bash script imports skills, enables both for two agents, lists state, verifies drift, all without GUI.
Covering scenarios:
- command line covers every visual operation
- machine-readable JSON output
User Story 8 — Remove a skill cleanly (Priority: P3)
When the developer removes a skill, every per-agent symlink is removed and the canonical folder is deleted.
Why this priority: A delete that leaves stale symlinks behind would silently confuse agents.
Independent Test: Enable a skill for two agents; remove the skill; verify both target symlinks are gone and the master folder is deleted.
Covering scenarios:
- remove a skill that has active per-agent bindings
- audit the removal with a snapshot
User Story 9 — Audit skill lifecycle (Priority: P3)
Every import, enable, disable, and remove is auditable.
Independent Test: Perform a representative sequence; view audit log; one row per change with actor, target, and event type.
Covering scenarios:
- audit import, enable, disable, remove
User Story 10 — Surface and adopt unmanaged skills (Priority: P2)
Agents accumulate skills Coffer never delivered — hand-copied folders, skills installed by other tools. Today these are invisible: the agent's Skills tab lists only Coffer-managed bindings. The user opens the tab and additionally sees the unmanaged skills found in the agent's skill locations — <config_dir>/skills for both types, plus ~/.agents/skills for Codex (the newer standard location Codex also reads). Coffer-managed links and Codex's .system internal entries are excluded. For each unmanaged skill the user can adopt it (move it into the master store, leave a managed link in its place so the agent keeps seeing it, and record a binding) or delete it.
Why this priority: The hub model only works if existing assets can flow into it. Adoption is User Story 1's import, made one-click and in-place.
Independent Test: Place a valid skill folder in a registered agent's skills/ directory; open the agent's Skills tab; observe it listed as unmanaged; adopt it; verify the master copy exists at ~/.coffer/skills/<name>/, the original path is now a managed symlink, and a binding row exists.
Covering scenarios:
- list unmanaged skills across an agent's skill locations
- adopt an unmanaged skill into the master store
- reject adopting an invalid or conflicting unmanaged skill
- delete an unmanaged skill
- exclude managed links and system entries from the unmanaged scan
User Story 11 — Follow the master library (Priority: P2)
Per-skill bindings are precise but chatty: every new skill must be enabled agent by agent. The user flips a per-agent "follow the master library" switch; from then on, every skill in the master store is delivered to that agent automatically — new skills appear on registration, removed skills disappear — with a per-agent exclusion list for the rare opt-outs. Per-skill bindings remain the mode for agents that don't follow. Turning follow off keeps the currently delivered set as explicit bindings, so nothing vanishes by surprise.
Why this priority: This is "configure once, share everything" for skills — the filesystem counterpart of the MCP gateway's one-entry-serves-all model.
Independent Test: Enable follow for an agent with three master skills; verify three links exist; register a fourth skill; verify its link appears without further action; exclude one skill; verify its link is removed while the rest stay.
Covering scenarios:
- enable follow-all and deliver every master skill
- auto-deliver new skills to following agents
- auto-remove deleted skills from following agents
- exclude a skill from a following agent
- disable follow-all preserving current bindings
Edge Cases
- Skill name collision on import: Rejected by default; the user either renames via SKILL.md frontmatter and retries, or re-imports with
overwrite(--force) to replace the existing skill in place — its per-agent bindings and delivered symlinks are preserved. - Master folder size exceeds limit (default 50 MB): Import rejected with the configured cap and a hint to adjust settings.
- Symlink/junction creation fails on Windows (FAT32 or network share): Falls back to copy mode for that target with an audit flag
degraded=true; UI shows a warning chip. - User edits
SKILL.mdin an external editor from inside an agent'sconfig_dir/skillsfolder: Coffer's UI never edits file content; the user makes the change in their own editor (reached via Coffer's "open in external editor" / "reveal in file manager" affordances or directly). Because the agent's path is a symlink to master, the external edit lands in master and is visible to all other agents on next read; no drift is detected. - User deletes a Coffer-managed file from inside an agent's
config_dir/skillsfolder: Master is affected (same reason); nextverifyflags any other agents whose links no longer resolve consistently. - Removing an agent (per spec 004) while it has skill bindings: Spec 004 defines the agent kind's
on_deleteseam; the 005-skill-manager spec supplies thecleanup_bindings_for_agentcallback at the composition root, so removing an agent first cleans up that agent's bindings and any associated symlinks before the agent row is deleted. - Agent's
config_diris moved or removed externally: The next sync operation surfaces the failure;verifyreports the affected bindings; user remediates by updating the agent'sconfig_diror removing the agent. ~/.agents/skillsis shared with other tools: The scan lists what it finds and classifies only Coffer's own links as managed; everything else is unmanaged. Deletion is always an explicit user action — Coffer never garbage-collects another tool's skills.- Unmanaged entry is a symlink pointing outside the master store: Listed as unmanaged-but-not-adoptable (adopting would move someone else's source of truth); the user can follow the link's target manually or delete the link.
- Unmanaged skill without a valid SKILL.md: Listed with
valid=falseand the reason; it can be deleted but not adopted until it validates. - Follow-all enabled while a target path holds a non-Coffer folder of the same name: That skill is reported as a conflict (same rule as FR-011) instead of being overwritten; the rest of the master store is delivered normally.
- Per-agent delivery target: Both current agent types (Claude Code, Codex) use the
folderdelivery mode and deliver to<config_dir>/skills/<name>. Each agent's mode and subpath come from the capability manifest, so adding a future agent's delivery target is data, not a new branch. - Non-folder delivery modes (reserved extension points):
rules_mdcandexternal_dirare recognizedSkillDeliveryModevalues with no current agent type assigned. Enabling a skill for a hypothetical agent using one of these modes is refused with HTTP 422 before any filesystem write; the follow / relink reconcilers skip such agents so registration and policy changes still succeed.
Skill delivery scope (Amendment 2026-07-10 — machine × agent scope, ADR-045)
A skill resource carries a framework-level scope (machine × agent axes, per ADR-045). Scope is a resource-side GRANT ("this skill may run here"); the existing per-agent follow policy (FR-025) is agent-side INTENT ("deliver skills to me"). Delivery is the INTERSECTION of both, minus manual exclusions:
- Per-skill bindings (User Story 3 / FR-009) deliver a skill to an agent only when the agent is ALSO within the skill's scope for the local machine (
machine_in_scopefor this machine ANDagent_in_scopefor the binding's agent). A binding request for an agent/machine combination the skill's scope excludes is rejected the same way an invalid delivery target is rejected today. - Follow-all delivery (FR-025) computes a following agent's effective set as the master store minus exclusions, further filtered to skills whose scope includes both the local machine and that agent — an out-of-scope skill is never auto-delivered to a following agent, exclusion list or not.
- Scope is a hard grant — it overrides manual bindings. Editing a skill's scope to exclude a machine or agent it was PREVIOUSLY delivered to reclaims that delivery on the next reconcile: the symlink is removed and the binding is marked disabled/removed, exactly like a follow-policy-driven removal (FR-010), even for a binding that was created manually (not through follow). A skill re-entering scope does not auto-redeliver on its own — a following agent picks it back up on the next follow reconcile; an explicit per-skill binding must be re-enabled by the user.
- Machine-local reconcile — the sync engine's per-import reconcile hook (spec 010) — is the enforcement seam: after every import (a scope edit propagating from another machine, a new/removed skill, or a follow-policy change), each affected agent's delivered set is recomputed as
scope ∩ follow-or-binding, and any now-out-of-scope delivered copy is reclaimed.
Acceptance Scenarios
Per agents/sdd.md, every scenario in this section is referenced by at least one test marked @pytest.mark.acceptance(spec="005-skill-manager", scenario="…") (Python) or acceptance("005-skill-manager", "…", …) (TypeScript).
Scenario: import a valid local skill folder
- Given the daemon is running and no skill named
my-skillexists, - When the user imports a folder containing a valid SKILL.md with frontmatter
name: my-skill, - Then Coffer copies the folder to
~/.coffer/skills/my-skill/, persists a Resource of kindskill, and records an audit entry.
Scenario: re-import a skill with overwrite replaces it
- Given a skill named
my-skillis already imported and enabled for an agent, - When the user imports a folder with frontmatter
name: my-skillagain withoverwrite(--force), - Then the master folder content is replaced atomically, the skill's
version_hashis refreshed, the existing per-agent binding and its delivered symlink are preserved, and a skill-update audit entry is recorded — whereas the same re-import withoutoverwriteis rejected withconflict(409).
Scenario: reject import of an invalid skill folder
- Given the daemon is running,
- When the user imports a folder that is missing
SKILL.mdor has emptyname/descriptionfrontmatter, - Then the request is rejected with a clear error, and nothing is written to
~/.coffer/skills/or the database.
Scenario: reject import containing path-escape symlinks
- Given the daemon is running,
- When the user imports a folder containing a symlink that resolves outside the folder,
- Then the request is rejected with the offending paths listed, and nothing is persisted.
Scenario: reject a skill with an over-long description
- Given the daemon is running,
- When the user imports a folder whose SKILL.md
descriptionexceeds 1024 characters, - Then the request is rejected as invalid frontmatter, and nothing is written to
~/.coffer/skills/or the database.
Scenario: recognize optional agentskills.io frontmatter fields
- Given a valid SKILL.md that also declares
licenseand the experimentalallowed-tools, - When the folder is validated,
- Then validation succeeds and the parsed frontmatter retains
licenseand a normalizedallowed-toolslist (rather than discarding them).
Scenario: enable a skill for a registered agent
- Given an agent
claude_codeis registered (per spec 004) and a skillmy-skillis imported, - When the user enables
my-skillforclaude_code, - Then a directory symlink (or junction on Windows) is created at
<config_dir>/skills/my-skillpointing to~/.coffer/skills/my-skill/, and askill_agent_bindingsrow records the link.
Scenario: disable a skill for an agent
- Given a skill is enabled for an agent and the target symlink exists,
- When the user disables it for that agent,
- Then the symlink is removed, the binding is marked disabled, and the master folder is unchanged.
Scenario: enable for multiple agents
- Given two agents are registered,
- When the user enables one skill for both,
- Then two symlinks (one per agent) exist, both pointing to the same master folder.
Scenario: refuse to overwrite a non-Coffer target
- Given the user has placed a regular file or directory at the would-be link path,
- When the user enables a skill for that agent,
- Then the operation is rejected; with
--force, the existing target is backed up to<path>.coffer-backup-<ts>and the link is created.
Scenario: detect drift in agent skill directories
- Given a binding exists but its target on disk has been deleted, replaced, or relinked,
- When the user runs
coffer skill verify, - Then the report lists each drift type with a suggested remedy and exits with a non-zero status; no automatic remediation occurs.
Scenario: remove a skill cleans up all bindings
- Given a skill is enabled for two agents,
- When the user removes the skill,
- Then both target symlinks are removed, the bindings are cascade-deleted, the master folder is deleted, and an audit entry records the removal with a config snapshot.
Scenario: removing an agent (per spec 004) cleans up its skill bindings
- Given an agent has one or more enabled skills,
- When the user removes the agent,
- Then spec 004's
on_deletehook for the agent kind invokes the skill module to remove each binding and its symlink before the agent row is deleted; master folders are unchanged.
Scenario: desktop and CLI cover every operation
- Given the daemon is running,
- When the user performs each operation via desktop and via
coffer skill ..., - Then the same effect is achieved in either surface and CLI provides
--jsonfor read operations.
Scenario: audit skill lifecycle
- Given the user has performed a representative sequence of operations,
- When they view the audit log,
- Then each event appears with timestamp, actor, target, event type, and any payload (e.g., before/after content hashes for updates).
Scenario: view a skill's files as a tree
- Given an imported skill whose master folder contains
SKILL.mdand a nested subdirectory with a file, - When the user requests the skill's file listing,
- Then Coffer returns a recursive read-only tree rooted at the master folder, each node carrying its name, folder-relative path, absolute on-disk path, type (
file/dir), file size, and children, sorted directories-first then by name, with no symlink target that escapes the folder included.
Scenario: view a single skill file's contents
- Given an imported skill that contains a readable text file,
- When the user requests that file's contents by its folder-relative path,
- Then Coffer returns the file's text, its true byte size, its absolute on-disk path and its containing folder's absolute path, and
binary=false/truncated=false; a non-existent file path returns a not-found error.
Scenario: reject reading a path outside the skill folder
- Given an imported skill,
- When the user requests file contents for a path that resolves outside the master folder (
..traversal, an absolute path, or an escaping symlink), - Then the request is rejected with a
400error before any file is read, and no content is returned.
Scenario: programmatically overwrite a skill file via the write API
- Given an imported skill that contains an existing text file,
- When a programmatic client (REST/CLI) saves new contents for that file by its folder-relative path,
- Then Coffer overwrites the file atomically and a subsequent read returns the new contents; writing a non-existent path, a path outside the master folder, an existing binary file, or content over the size cap is rejected (
404/400) and the file is left unchanged. (The in-app UI does not call this endpoint to edit content; it is a programmatic write surface only.)
Scenario: list unmanaged skills across an agent's skill locations
- Given a registered
codexagent with one Coffer-managed link in<config_dir>/skills, one hand-copied skill folder there, and another skill folder in~/.agents/skills, - When the user lists the agent's unmanaged skills,
- Then Coffer returns exactly the two hand-placed skills — each with name, path, location, and a
validflag from SKILL.md validation — and excludes the managed link.
Scenario: adopt an unmanaged skill into the master store
- Given an unmanaged skill folder with a valid SKILL.md whose name collides with no master skill,
- When the user adopts it,
- Then Coffer validates it per FR-004, moves the folder to
~/.coffer/skills/<name>/, registers theskillresource, replaces the original path with the managed link, records a binding for that agent, and audits the adoption — and on any failure the original folder is left exactly where and as it was.
Scenario: reject adopting an invalid or conflicting unmanaged skill
- Given an unmanaged entry that lacks a valid SKILL.md, collides with an existing master skill's name, or is a symlink pointing outside the master store,
- When the user attempts to adopt it,
- Then the request is rejected with a reason-specific error (invalid:
unprocessable_entity422; name conflict:conflict409; foreign link:unprocessable_entity422), and nothing is moved, registered, or linked.
Scenario: delete an unmanaged skill
- Given an unmanaged skill folder in an agent's skill location,
- When the user deletes it (an explicit, confirmed action),
- Then the folder is removed from disk, an audit entry is recorded, and no master content or binding is touched.
Scenario: exclude managed links and system entries from the unmanaged scan
- Given an agent's skill directory containing Coffer-managed links and (for Codex) a
.systementry, - When the user lists unmanaged skills,
- Then neither the managed links nor the
.systementry appear in the result.
Scenario: enable follow-all and deliver every master skill
- Given a registered agent not yet following, and three skills in the master store,
- When the user enables the agent's follow-master-library switch,
- Then the sync engine delivers all three skills to the agent (links + binding rows) and the agent's effective set equals the master store minus its (empty) exclusion list.
Scenario: auto-deliver new skills to following agents
- Given an agent with follow enabled,
- When a new skill is registered in the master store (import or adoption),
- Then the daemon delivers it to that agent without further user action.
Scenario: auto-remove deleted skills from following agents
- Given an agent with follow enabled and a delivered skill,
- When that skill is removed from the master store,
- Then the agent's link and binding are cleaned up as part of the removal.
Scenario: exclude a skill from a following agent
- Given an agent with follow enabled and a delivered skill,
- When the user excludes that skill for this agent,
- Then its link and binding are removed, the skill joins the agent's exclusion list, and later master changes never re-deliver it until the exclusion is lifted.
Scenario: disable follow-all preserving current bindings
- Given an agent with follow enabled and several delivered skills,
- When the user disables the follow switch,
- Then every currently delivered skill remains as an explicit per-skill binding with its link intact, and subsequent master-store additions are no longer auto-delivered.
Scenario: delivery is the intersection of scope and follow policy; an out-of-scope copy is reclaimed
- Given an agent following the master library with a skill currently delivered to it, and that skill's scope currently includes this machine and this agent,
- When the user edits the skill's scope to exclude this agent (or this machine) and the next reconcile runs,
- Then the delivered symlink is removed and the binding is reclaimed even though the agent still follows the master library and never excluded the skill itself — delivery equals scope ∩ follow policy, and scope's exclusion wins.
Scenario: opt-in repair re-delivers repairable drift from master
- Given an agent skill directory where one enabled binding has a missing Coffer link, another has a tampered Coffer link (a stale link pointing elsewhere), a third binding's path is occupied by a foreign regular directory the user owns, and a fourth binding's master folder no longer exists,
- When the user runs the opt-in repair (
coffer skill verify --fix/POST /skills/repair), - Then the missing link is re-created pointing to master, the tampered link is backed up to
<path>.coffer-backup-<ts>and then re-created pointing to master, the foreign regular directory is left completely untouched and still appears in the report as requiring manual action, the missing-master entry is left and reported as requiring manual action, and each re-delivery is recorded as a repair event in the audit log.
Requirements
Functional Requirements
Resource model
- FR-001: System MUST register each managed skill as a Resource of kind
skill, identified byskill:<name>where<name>comes from SKILL.md frontmatter. - FR-002: System MUST validate skill configuration against a kind-specific schema with fields
source(variant:local_importonly),skill_md_name,skill_md_description,version_hash, andlast_synced_from_source_at.
Canonical storage
- FR-003: System MUST store each managed skill's content under
~/.coffer/skills/<name>/, with that path as the single editable source of truth. - FR-004: System MUST validate every imported skill folder against the AgentSkills specification:
SKILL.mdpresent; frontmatternamepresent and non-empty (lowercase alphanumerics, hyphen, or underscore, ≤64 chars) anddescriptionpresent, non-empty, and ≤1024 chars; no path-escape symlinks; total size within a configurable limit (default 50 MB). A folder that violates any of these is rejected withunprocessable_entity(422) and nothing is persisted. - FR-027: System MUST recognize the optional agentskills.io frontmatter fields it understands —
licenseand the experimentalallowed-tools— parsing and retaining them rather than discarding them, while tolerating any other unrecognized frontmatter field so non-Coffer-authored skills validate cleanly.allowed-toolsaccepts either a list or a comma/whitespace-separated string and is normalized to a list of tool names; a malformed value is tolerated (treated as absent), never a validation failure. Likewise a non-stringlicensescalar (e.g. an unquoted year or version) is coerced to a string rather than rejected.
Sources
- FR-005: System MUST support importing a skill from a local filesystem path; the original source path is recorded for provenance but is not retained as a live dependency. Re-importing a name that already exists is rejected by default (
conflict, 409); with an explicitoverwriteflag (CLI--force) the existing skill is replaced in place — the master folder content is swapped atomically, itsversion_hashandlast_synced_from_source_atare refreshed, and its per-agent bindings and delivered symlinks are preserved (the master folder name is unchanged). Re-import overwrite is the only skill update mechanism (there is no live source to re-fetch); the replacement is audited as an update.
Per-agent delivery
- FR-008: Each
(skill, agent)binding is tracked in askill_agent_bindingstable recording whether the binding is enabled and the last successful link path. - FR-009: Enabling a binding MUST create a directory symlink (POSIX) or directory junction (Windows) at
<config_dir>/skills/<skill-name>pointing to~/.coffer/skills/<skill-name>/. - FR-010: Disabling a binding MUST remove the target link without touching the master folder.
- FR-011: Enabling MUST refuse to overwrite an existing non-Coffer target without
--force;--forcebacks up the existing target before creating the link. - FR-012: When symlinks/directory junctions are unavailable (e.g., FAT32, network share), System MAY fall back to copy mode for that target; the binding records
link_mode=copy_fallback(audited asmode: copy_fallbackon the enable event) and the UI MUST surface the degradation (the agent Skills tab shows a "Copied" warning chip on such bindings). - FR-012a (Amendment 2026-07-10 — machine × agent scope, ADR-045): Skill delivery to an agent MUST additionally require the skill to be in scope for the local machine AND that agent (
scope(M, agent)), whether delivery is a per-skill binding (FR-009) or follow-all (FR-025). A reconcile that finds a delivered binding now out of scope MUST reclaim it (remove the link, disable/remove the binding) exactly as FR-010 reclaims a disabled binding — scope is a hard grant that overrides a previously-manual binding.
Drift
- FR-015: System MUST provide a
verifyoperation that compares each enabled binding to its on-disk target and reports drift categories (missing link, tampered link, missing master, orphan master) with suggested remedies. - FR-016: System MUST NOT automatically remediate drift; remediation requires an explicit user action.
- FR-029: System MUST provide an explicit, opt-in drift repair (
coffer skill verify --fix,POST /skills/repair) that re-delivers repairable drift — missing link and tampered link — from the master library, and MUST NOT modify foreign/user content (replaced-with-regular), a missing master, or an orphan master; those are left intact and reported as requiring manual action. Each repair is audited.
Unmanaged skills (workspace amendment)
- FR-022: System MUST scan a registered agent's skill locations —
<config_dir>/skillsfor both types, plus~/.agents/skillsforcodex— and list unmanaged entries: everything that is not a Coffer-managed link (a link whose target resolves inside~/.coffer/skills/) and not Codex's.systementry. Each result carries name, path, location, and avalidflag (FR-004 validation) with the failure reason when invalid. The scan is read-only and derived at request time. - FR-023: Users MUST be able to adopt a valid unmanaged skill. Adoption validates the folder per FR-004, moves it to
~/.coffer/skills/<name>/, registers theskillresource, delivers the managed link (FR-009), and records an enabled binding for that agent — in that order, with any failure before registration leaving the original folder unmoved and unchanged (after registration the master copy is authoritative; a delivery failure is surfaced and retried via the binding, never rolled back). The managed link is always delivered to the agent's canonical delivery location<config_dir>/skills/<name>: adopting from<config_dir>/skillsreplaces the original path in place, while adopting from~/.agents/skillsconsolidates — the original folder there is removed and the link lands in<config_dir>/skills(Codex reads both locations, so the agent keeps seeing the skill). Name collisions are rejected withconflict(409); invalid folders and symlinks pointing outside the master store are rejected withunprocessable_entity(422). Audited as an adoption event. - FR-024: Users MUST be able to delete an unmanaged entry as an explicit, confirmed action. Deletion removes only that entry from disk, never master content or bindings, and is audited.
Follow the master library (workspace amendment)
- FR-025: Each agent MUST carry a follow-master-library flag and a per-agent skill exclusion list (stored on the agent resource's config, spec 004). While following, the agent's effective skill set is the entire master store minus its exclusions; the sync engine MUST reconcile deliveries when the flag changes, when a skill is registered or removed, and when the exclusion list changes. Conflicts at target paths follow FR-011 (report, never overwrite). Disabling the flag MUST preserve the currently delivered skills as explicit per-skill bindings. The flag defaults to enabled for newly registered agents, matching the pre-amendment auto-bind behavior.
- FR-026: Unmanaged-skill and follow operations MUST be available through the REST API, the
coffer agent skill …/coffer skill …CLI (with--jsonon reads), and the agent's Skills tab in the desktop app.
Lifecycle
- FR-017: Removing a skill MUST remove every enabled per-agent symlink, cascade-delete bindings, delete the master folder, and audit the removal with a snapshot.
- FR-018: Removing an agent (via spec 004) MUST trigger an
on_deletehook in the skill module that removes that agent's bindings and symlinks before the agent row is deleted.
Surfaces
- FR-019: Every management operation MUST be available through (a) the REST API, (b) the
coffer skill ...CLI with--json, and (c) the desktop Skills page. - FR-021: System MUST expose a read-only view of a skill's master folder: a recursive file tree (name, folder-relative path, absolute on-disk path, type, size, children) and the contents of an individual file (with its absolute on-disk path and containing folder's absolute path). Markdown files render as formatted Markdown; other text files show raw. The in-app UI viewer is read-only and never edits file content. Reads MUST be contained to the master folder — any path that resolves outside it (
..traversal, absolute path, or escaping symlink) MUST be rejected. File reads MUST be size-capped (truncating with atruncatedflag) and MUST flag non-UTF-8 / NUL-containing files as binary with empty content. No symlink-following out of the folder. - FR-027: The in-app file viewer MUST offer, at both file and containing-folder granularity, affordances to (a) open the target in the user's preferred external editor (the global preference is specced in 002-ui-shell; default = the OS default application) and (b) reveal the target in the OS file manager (Finder / Explorer). Open and reveal perform the real OS action on both surfaces — desktop (Tauri) via the OS opener, web via the daemon filesystem-action endpoints (spec 004 FR-039), since the loopback daemon is on the user's own machine (ADR-033). There is no copy-path fallback. These affordances replace in-app content editing: the user edits in their own external editor.
- FR-028: System MUST provide a programmatic (REST/CLI) write that overwrites an existing text file in the master folder, under the same containment guard and size cap as FR-021; it MUST refuse to create new files/directories here, to write outside the folder, or to overwrite a binary file with text. The write MUST be atomic with no symlink-following out of the folder. This write surface is for programmatic clients only; the in-app UI does not use it to edit content (see FR-027).
- FR-030: The desktop "Add skill" import dialog MUST offer a folder picker (reusing the shared component from spec 004 FR-023/FR-024 — the OS-native directory dialog in the packaged app, the daemon-backed folder browser on the web) so the user picks the skill folder instead of typing its absolute path. The picked absolute path feeds the existing import operation (FR-005); typing a path by hand stays supported.
Observability
- FR-020: System MUST record an audit entry for every import, enable, disable, remove, and drift remediation event.
Key Entities
- Skill: A Resource of kind
skill, identified byskill:<name>(name from SKILL.md frontmatter). Holds source provenance, content hash, and metadata; the content folder lives on disk at~/.coffer/skills/<name>/. Carries a framework-levelscope(machine × agent axes) whose intersection with the per-agent follow policy determines delivery (Amendment 2026-07-10 — machine × agent scope, ADR-045; see "Skill delivery scope"). - Skill Source: A record capturing where the skill came from. For local imports, it includes the original path for informational purposes only.
- Skill–Agent Binding: A row joining one skill Resource and one agent Resource (kind
agent, per spec 004), with anenabledflag and last-link-path metadata. Symlink existence on disk is the live representation; binding state is the persistent representation. - Drift Report: An ephemeral structure produced by
verifylisting each binding whose on-disk target disagrees with the binding state, categorized by drift type with a suggested remedy. - Unmanaged Skill: A derived (never stored) view of a skill-shaped entry found in an agent's skill locations that Coffer does not manage — name, path, location,
validflag. The filesystem is the source of truth; adoption or deletion are the only mutations. - Follow Policy: Per-agent state (flag + exclusion list, stored on the agent resource's config per spec 004) declaring that the agent receives the entire master store. Bindings remain the persistent delivery record; the policy drives the sync engine's reconciliation.
Success Criteria
Measurable Outcomes
- SC-001: From a fresh install, a user can import their existing
~/.claude/skills/<one-skill>/folder, enable it for the auto-detected Claude Code agent, and reach the "ready" state within 60 seconds. - SC-003: Enabling a skill for two agents creates two valid directory symlinks (or junctions on Windows), and each agent's reading process sees identical SKILL.md content.
- SC-004: After manually deleting an agent-side symlink,
coffer skill verifyidentifies it as drift within 5 seconds and exits with a non-zero status. - SC-005: Removing a skill that is enabled for two agents leaves no residual symlinks, no residual master folder, and no orphan binding rows in the database.
- SC-006: Every Acceptance Scenario in this spec is covered by at least one test marked
acceptance(spec="005-skill-manager", scenario="…"), andmake verify-acceptancereports zero uncovered scenarios. - SC-007: The full
make verifysuite passes locally and in CI;make verify-all(adding e2e) passes on macOS and Linux; Windows tests pass for both junction mode and copy-fallback mode. - SC-008: No SKILL.md content ever leaves the user's machine; verified by an automated network-egress scan during integration tests.
- SC-009: With follow enabled, a newly registered skill is delivered to the following agent within 5 seconds, with no user action beyond the registration itself.
- SC-010: On a machine with a mix of managed links and hand-placed skills, the unmanaged scan lists exactly the hand-placed entries — zero managed links, zero
.systementries — verified by integration tests over a constructed fixture tree.
Assumptions
- Spec 004-agent-registry has shipped (PR #25); the agent kind, its CRUD, audit, and
on_deletehook are available. - The kind-agnostic Resource framework, audit log, and
<kind>:<name>identity scheme defined by spec 001-mcp-gateway are in place. - The application shell from spec 002-ui-shell — sidebar IA, layout, routing skeleton, and design system — is in place; the desktop Skills page is a feature surface that renders within that shell and fills the
/skillsnav slot 002-ui-shell reserved as a placeholder. - Skills follow the open AgentSkills standard (
SKILL.mdwithname/descriptionfrontmatter at minimum) as published at agentskills.io, validated against the standard's exact constraints (name≤64 chars,description≤1024 chars) with the optionallicenseand experimentalallowed-toolsfields recognized; non-conforming folders are out of scope. - Local-imported skills are point-in-time copies; the source path is recorded for traceability, not for sync.
- Windows users have directory-junction support on their filesystem; FAT32 and network shares fall back to copy mode.
- Delivery stays at
<config_dir>/skillsfor both agent types. Codex additionally reads~/.agents/skills(its newer standard location) and treats<config_dir>/skillsas a backward-compatible legacy location — the unmanaged scan covers both; migrating Coffer's delivery target is a recorded decision deferred to a future change. - The follow-master-library flag and exclusion list live on the agent resource's config (spec 004's schema); this spec owns their delivery semantics.
- A browse-and-install skill catalog (discovery) was prototyped then withdrawn (simplification, 2026-06-20) for lack of a content ecosystem; an install catalog remains possible future work.
- v2 will explore: a remote catalog index, agent-to-agent skill recommendations, and project-local skills (
.claude/skills/in repo).