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

8.9 KiB
Raw Blame History

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) 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. A repo can contribute multiple skills. Installer detects the layout:
    • skills/*/SKILL.md (plugin bundle) → import each subskill flat. This is ponytail: it ships 6 skills (ponytail, -help, -review, -audit, -debt, -gain) under skills/<name>/SKILL.md plus .claude-plugin/, hooks, commands, an MCP — none of which we consume; we take only the skills/<name>/ dirs.
    • root SKILL.md → single skill.
    • neither → reject. The CLI expects .claude/skills/<name>/SKILL.md flat, so subskills are flattened on install. Selection is per individual skill name (enable just ponytail + ponytail-help if you want, not all six).
  4. Public repos only (plain git clone over HTTPS, no auth) for MVP.

Correction (2026-07-03, from the smoke test): the original "one repo = one skill at root" MVP was wrong for the very target repo — ponytail is a multi-skill plugin. Decision 3 above replaces it.

Architecture

Storage & registry

  • Each discovered skill is copied flat to ~/.todo-app/session-skills/<name>/ (its own self-contained dir with SKILL.md at the root of that dir), so the seeder just copies <name>/ → cwd.
  • New DB table session_skills, one row per skill (a multi-skill repo writes N rows sharing source_url + pinned_ref): name (PK), source_url, pinned_ref (commit SHA), subpath (dir within the repo the skill came from, e.g. skills/ponytail or . for root), description, added_at. Repo-level ops act on all rows with the same source_url (no separate sources table — keep it flat).
  • New worker service SessionSkillRegistry (in a new Skills/ area under the Worker):
    • InstallAsync(url) — clone to temp → detect layout (skills/*/SKILL.md bundle vs root SKILL.md) → for each discovered skill parse frontmatter (name, description), resolve HEAD SHA as pinned_ref, copy its dir flat into place, upsert a row. Returns the list of installed skill names. Name collision (a skill name from a different source) → error surfaced to UI; reinstalling the same source updates. Clone is injected (IRepoCloner) so tests use a local source dir — no real network, no real CLI.
    • UpdateAsync(sourceUrl) — re-clone, re-detect, refresh that source's skills + pinned_ref.
    • RemoveAsync(sourceUrl) — delete all its skill dirs + rows.
    • ListAsync() — registry entries for the UI (grouped by source for display).

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. Add (URL text box → install a repo, which may yield several skills); lists installed skills grouped by source (name, description, source, short ref); Update / Remove act per source (repo). 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(sourceUrl), RemoveSessionSkill(sourceUrl). 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/? CONFIRMED 2026-07-03. Mika ran, in a stable terminal, a claude -p invocation in a scratch cwd holding .claude/skills/ponytail*/SKILL.md; the model invoked the ponytail-help skill and returned its exact Lite/Full/Ultra table. Headless mode does surface cwd skills → the whole approach holds; no CLAUDE_CONFIG_DIR fallback needed.
  • 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; consuming a plugin's non-skill parts (hooks, commands, MCP — we take only skills/<name>/); auto-update & update notifications; per-task disabling of an inherited skill; surfacing skill invocation in the run log.