Files
ClaudeDo/docs/superpowers/specs/2026-07-03-session-skills-design.md
T

147 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session Skills — Design
**Date:** 2026-07-03
**Status:** Approved (design), implementation not started
## Problem
Mika wants to give headless task agents a specific Claude skill (e.g.
[ponytail](https://github.com/DietrichGebert/ponytail)) **without** installing it
globally in `~/.claude/skills/`, where it would leak into every interactive session.
Skills should be a first-class, per-level session setting alongside `model`,
`max_turns`, and `system_prompt` — configurable **global / per-list / per-task** — and
sourced from a GitHub URL.
## Key facts that shape the design
- The Claude CLI discovers skills from two places: the **global** `~/.claude/skills/`
(every session — undesirable here) and the **working directory's** `.claude/skills/`
(plus plugins). ClaudeDo fully controls each spawned session's `WorkingDirectory`
(`ClaudeProcess.cs:30`), so a skill dropped into the session cwd is scoped to exactly
that headless run.
- **Auto-commit uses `git add -A`** (`WorktreeManager.CommitIfChangedAsync`
`_git.AddAllAsync`, `WorktreeManager.cs:142`). Anything seeded into a worktree's
`.claude/skills/` would be committed unless explicitly excluded → the seeder must add
the seeded paths to the worktree's `info/exclude`.
- A skill is not just prompt text: `SKILL.md` may reference scripts that run via Bash.
Headless agents run with `--permission-mode auto` (effectively unattended), so a skill
pulled from an arbitrary URL is **unattended third-party code execution**. This is why
install is a deliberate, pinned, reviewable step — not a live per-run URL fetch.
## Decisions (locked)
1. **Install-and-pin, not live fetch.** A dedicated registry screen installs a skill
once: clone the repo, pin to the current commit, store locally. Per-level config then
references installed skills **by name** (checkboxes), never a URL.
2. **Three levels, additive union.** Effective skill set = `global list task`.
(Unlike `model`/`prompt`, which override — skills add up. Trade-off accepted: an
inherited skill can't be switched off for a single task in the MVP.)
3. **One repo = one skill.** Installer accepts only a repo with `SKILL.md` at the root
(covers ponytail). Multi-skill repos / plugin-format repos are out of scope for MVP.
4. **Public repos only** (plain `git clone` over HTTPS, no auth) for MVP.
## Architecture
### Storage & registry
- Installed skills live at `~/.todo-app/session-skills/<name>/` (the cloned repo contents,
`SKILL.md` at root).
- New DB table `session_skills`: `name` (PK), `source_url`, `pinned_ref` (commit SHA),
`description`, `added_at`.
- New worker service `SessionSkillRegistry` (in a new `Skills/` area under the Worker):
- `InstallAsync(url)` — clone to temp → validate `SKILL.md` at root → parse frontmatter
(`name`, `description`) → resolve HEAD SHA as `pinned_ref` → move into place →
upsert DB row. Name collision → error surfaced to UI. The clone step is injected
(`IRepoCloner`) so tests use a local source dir — **no real network, no real CLI**.
- `UpdateAsync(name)` — re-fetch, checkout latest, refresh files + `pinned_ref`.
- `RemoveAsync(name)` — delete dir + row.
- `ListAsync()` — registry entries for the UI.
### Resolution
`TaskRunner.ResolveConfigAsync` (`TaskRunner.cs:488`) already merges
task → list → global for the other fields. Add:
```
SkillNames = Union(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills)
```
deduped, filtered to names that still exist in the registry (a removed skill is silently
dropped — logged). Add `IReadOnlyList<string> SkillNames` to `ClaudeRunConfig`
(`ClaudeArgsBuilder.cs:5`). **No CLI flag is emitted** — skills are seeded on disk, not
passed as args. `ClaudeArgsBuilder.Build` is unchanged for skills.
Per-level storage: a nullable TEXT column `session_skills` (JSON array of names) on
`tasks`, `list_config`, and `app_settings`.
### Seeding
New service `SessionSkillSeeder`, called by `TaskRunner` after the working dir is
resolved and before `ClaudeProcess.RunAsync`:
- For each resolved skill name, copy `~/.todo-app/session-skills/<name>/`
`<cwd>/.claude/skills/<name>/` (overwrite → idempotent for resume/re-run).
- If the cwd is a git worktree, append `/.claude/skills/<name>/` to the worktree's
`info/exclude` (path via `git rev-parse --git-path info/exclude`, so it targets the
per-worktree exclude), only if not already present. **Only the seeded subdirs are
excluded** — never blanket-exclude `/.claude/`, in case the target project commits its
own `.claude/`.
- Sandbox runs (not a repo) skip the exclude step.
- No separate cleanup: seeded dirs vanish with the worktree/sandbox.
### allowedTools caveat
`--allowedTools` is only emitted when set (`ClaudeArgsBuilder.cs:80`); normal task runs
leave it null → all tools allowed → the `Skill` tool is available. If a future per-task
allowedTools restriction is added, it must include `Skill`. Noted, not handled in MVP.
## UI
Mirror the existing agent-file pattern.
- **Registry screen ("extra mask"):** a new **Skills** tab in the Settings modal
(`SettingsModalView.axaml`) with `SessionSkillsSettingsTabViewModel`. Lists installed
skills (name, description, source, short ref); **Add** (URL text box → install),
**Update**, **Remove**. Status/error line like `FilesSettingsTabViewModel`.
- **Global selector:** multi-select (checkbox list) of installed skills in the General
settings tab → `AppSettings.SessionSkills`.
- **List + Task selectors:** add a skills multi-select to the shared
`AgentConfigEditor` control (`AgentConfigEditor.axaml` /
`AgentConfigEditorViewModel.cs`), which is already reused by both List settings and the
per-task flyout — one addition covers both levels, with the existing inheritance-badge
pattern.
### Hub / client surface
New `WorkerHub` methods + `IWorkerClient` entries (update hand-rolled fakes in both test
projects — see memory `iworkerclient_fakes_sync`):
`GetSessionSkills`, `InstallSessionSkill(url)`, `UpdateSessionSkill(name)`,
`RemoveSessionSkill(name)`. Extend `AppSettingsDto`, `ListConfigDto`,
`UpdateListConfigDto`, `UpdateTaskAgentSettingsDto` with the selected skill-name lists.
New `SessionSkillDto`.
## Edge cases
- **Removed skill still referenced** by a level → dropped at resolve time, logged, no
failure.
- **Name collision on install** → reject with a clear message; offer Update instead.
- **Repo without root `SKILL.md`** → reject at install.
- **Target project already has `.claude/skills/`** → additive copy; exclude only our
subdirs.
- **Resume / re-run** reuses the worktree → re-seed overwrites, exclude append is
idempotent.
## Verification (must-check, can't be unit-tested)
- **Does `claude -p` actually load and invoke a skill placed in cwd `.claude/skills/`?**
This is the load-bearing assumption. Manual smoke test with ponytail before calling the
feature done. If headless mode does *not* surface cwd skills, fall back to
`CLAUDE_CONFIG_DIR` isolation (heavier — needs credentials copied) and revisit.
- Seeded skill is **not** committed by the auto-commit step (worktree run).
- Skill does not appear in a normal interactive session (no global leak).
## Out of scope (MVP)
Private-repo auth; multi-skill / plugin-format repos; auto-update & update notifications;
per-task *disabling* of an inherited skill; surfacing skill invocation in the run log.