|
| 1 | +# Phase 5.3 — Install script (skills and subagents deployment) |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Build a unified Python tool `tools/install.py` that deploys skills and |
| 6 | +subagents from this curated library to their runtime location. |
| 7 | +Without this tool, the library is purely catalog: skills sit in |
| 8 | +`skills/<name>/` but Claude Code does not see them. The tool bridges |
| 9 | +that gap by creating symlinks or copies in the appropriate |
| 10 | +`.claude/skills/` or `.claude/agents/` location. |
| 11 | + |
| 12 | +## Design choices (decided with user) |
| 13 | + |
| 14 | +- **Deployment modes**: symlink (default) and copy. Symlink keeps the |
| 15 | + library as single source of truth; copy is for sharing across |
| 16 | + systems where the source path won't exist. |
| 17 | +- **Install tracking**: a log file at `~/.aithos-install-log.yaml` |
| 18 | + records every install (what, where, mode, date). Enables `--list` |
| 19 | + and `--uninstall`. |
| 20 | +- **Default targets**: |
| 21 | + - Skills install to `~/.claude/skills/<name>/` by default (global). |
| 22 | + - Subagents require an explicit `--target` (they live in a project's |
| 23 | + `.claude/agents/`, not globally). |
| 24 | + |
| 25 | +## Prerequisites |
| 26 | + |
| 27 | +- Phase 5.2-bis complete and pushed. |
| 28 | +- The current branch should be a new feature branch off `main`: |
| 29 | + |
| 30 | + ```bash |
| 31 | + git checkout main |
| 32 | + git pull # main is currently behind, PR #3 is open |
| 33 | + ``` |
| 34 | + |
| 35 | + However, PR #3 (containing 5.2 + 5.2-bis) is still open. The user |
| 36 | + prefers to merge it at end of day along with 5.4 and 5.5. |
| 37 | + Therefore Phase 5.3 must be developed on a new branch that |
| 38 | + **branches off `feat/ingest-assets-batch-1`** (the current state), |
| 39 | + not off `main`: |
| 40 | + |
| 41 | + ```bash |
| 42 | + git checkout feat/ingest-assets-batch-1 |
| 43 | + git pull # ensure local is up to date |
| 44 | + git checkout -b feat/install-script |
| 45 | + ``` |
| 46 | + |
| 47 | + This way Phase 5.3 builds on the work just done. After all phases |
| 48 | + are complete, the user will merge them in order (5.2-bundle, 5.3, |
| 49 | + 5.4, 5.5) into `main`. |
| 50 | + |
| 51 | +- `git status` clean, current branch `feat/install-script`. |
| 52 | + |
| 53 | +## High-level flow |
| 54 | + |
| 55 | +1. **Design** the CLI (`install`, `uninstall`, `list`, `info`). |
| 56 | +2. **Implement** `tools/install.py` with the Typer framework |
| 57 | + (consistent with existing tooling). |
| 58 | +3. **Test** locally by installing one skill, listing, then |
| 59 | + uninstalling. |
| 60 | +4. **Update CONTRIBUTING.md** with a deployment section explaining |
| 61 | + how to use the script. |
| 62 | +5. **Update README.md** briefly to mention the install tool. |
| 63 | +6. **Commit and push**. |
| 64 | + |
| 65 | +## Deliverables |
| 66 | + |
| 67 | +### 1. `tools/install.py` — main script |
| 68 | + |
| 69 | +Implement using Typer (already in the project's dependencies). The CLI |
| 70 | +must expose four subcommands: |
| 71 | + |
| 72 | +#### `install <item-path> [--target <path>] [--mode symlink|copy] [--force]` |
| 73 | + |
| 74 | +Install a single skill or subagent. |
| 75 | + |
| 76 | +Arguments: |
| 77 | + |
| 78 | +- `item-path` (positional, required): relative path from repo root |
| 79 | + to the item to install. Must be either `skills/<name>` or |
| 80 | + `subagents/<name>`. Path validation is strict — the directory must |
| 81 | + exist, must contain the expected entrypoint (`SKILL.md` for skills, |
| 82 | + the manifest-declared `entrypoint` for subagents). |
| 83 | + |
| 84 | +Options: |
| 85 | + |
| 86 | +- `--target <path>` (optional): destination directory. Defaults: |
| 87 | + - For skills: `~/.claude/skills/<name>` |
| 88 | + - For subagents: **required** (no default; if missing, error and |
| 89 | + exit with code 2 and a message explaining that subagents need a |
| 90 | + project-specific target). |
| 91 | +- `--mode symlink|copy` (default: `symlink`): how to deploy. |
| 92 | +- `--force` (flag, default off): if the target already exists, remove |
| 93 | + it (recursively if it's a directory, unlink if it's a symlink) and |
| 94 | + proceed. Without `--force`, exit with code 3 and an error message. |
| 95 | + |
| 96 | +Behavior: |
| 97 | + |
| 98 | +1. Resolve the absolute path of the source (canonical, follows the |
| 99 | + current working dir's repo root). |
| 100 | +2. Validate the source structure (skill or subagent). |
| 101 | +3. Resolve the target path (expand `~`, normalize, make absolute). |
| 102 | +4. Check for target collision; honor `--force` or abort. |
| 103 | +5. Ensure the parent directory of the target exists; create it |
| 104 | + recursively if needed. |
| 105 | +6. Apply the deployment: |
| 106 | + - `symlink`: `os.symlink(source_abs, target_abs)` |
| 107 | + - `copy`: `shutil.copytree(source_abs, target_abs, |
| 108 | + symlinks=False)` (recursive copy, dereference any internal |
| 109 | + symlinks) |
| 110 | +7. Record the install in the log (see §2 below). |
| 111 | +8. Print a confirmation message: `✓ Installed <name> to <target> |
| 112 | + (mode=<mode>)`. |
| 113 | + |
| 114 | +#### `uninstall <name-or-target>` |
| 115 | + |
| 116 | +Uninstall a previously installed item. |
| 117 | + |
| 118 | +Argument: |
| 119 | + |
| 120 | +- `name-or-target` (positional, required): either the item name |
| 121 | + (kebab-case, matches a logged install) or the full path of an |
| 122 | + installed item. |
| 123 | + |
| 124 | +Behavior: |
| 125 | + |
| 126 | +1. Look up in the log: find one or more matching entries. |
| 127 | +2. If multiple matches (e.g. the same skill installed to multiple |
| 128 | + targets), list them and ask the user to disambiguate. Print exit |
| 129 | + code 4 if non-interactive. |
| 130 | +3. For each match: remove the target (`os.unlink` if symlink, |
| 131 | + `shutil.rmtree` if directory). Remove the entry from the log. |
| 132 | +4. Print: `✓ Uninstalled <name> from <target>`. |
| 133 | + |
| 134 | +#### `list` |
| 135 | + |
| 136 | +Show all currently installed items. |
| 137 | + |
| 138 | +Output as a `rich` table with columns: `Name`, `Type` (skill/subagent), |
| 139 | +`Target`, `Mode`, `Installed`. |
| 140 | + |
| 141 | +If the log is empty: print `No items installed.`. |
| 142 | + |
| 143 | +If a target listed in the log no longer exists on disk (user |
| 144 | +manually deleted it): mark the row with a warning icon `⚠️` and |
| 145 | +include the column `Status` with values `ok` or `missing`. |
| 146 | + |
| 147 | +#### `info <item-path>` |
| 148 | + |
| 149 | +Show metadata about a library item without installing. |
| 150 | + |
| 151 | +Output: name, type, version, status, description, tags, and (for |
| 152 | +subagents) declared tools/mcp_servers/skills_dependencies. |
| 153 | + |
| 154 | +### 2. Install log — `~/.aithos-install-log.yaml` |
| 155 | + |
| 156 | +Format: |
| 157 | + |
| 158 | +```yaml |
| 159 | +installs: |
| 160 | + - name: librarian |
| 161 | + type: skill |
| 162 | + source: /home/riccardo/projects/aithos-selection/skills/librarian |
| 163 | + target: /home/riccardo/.claude/skills/librarian |
| 164 | + mode: symlink |
| 165 | + installed: 2026-05-16T01:15:00Z |
| 166 | + |
| 167 | + - name: automation-architect |
| 168 | + type: subagent |
| 169 | + source: /home/riccardo/projects/aithos-selection/subagents/automation-architect |
| 170 | + target: /home/riccardo/projects/martina-os/.claude/agents/automation-architect |
| 171 | + mode: copy |
| 172 | + installed: 2026-05-16T02:30:00Z |
| 173 | +``` |
| 174 | +
|
| 175 | +The file is created on first install if missing. Operations on it |
| 176 | +must be **atomic** (write to temp file, then rename) to avoid |
| 177 | +corruption on partial writes. |
| 178 | +
|
| 179 | +The log file is NOT tracked in git (it's a user-local artifact). |
| 180 | +
|
| 181 | +### 3. Error handling and exit codes |
| 182 | +
|
| 183 | +Use distinct exit codes for scriptability: |
| 184 | +
|
| 185 | +- `0`: success |
| 186 | +- `1`: generic error / unexpected exception |
| 187 | +- `2`: invalid arguments (e.g. subagent without `--target`) |
| 188 | +- `3`: target collision without `--force` |
| 189 | +- `4`: ambiguous uninstall (multiple matches, non-interactive) |
| 190 | +- `5`: source not found / not a valid skill or subagent |
| 191 | +- `6`: log file corrupt or unreadable |
| 192 | + |
| 193 | +All error messages should be printed via `rich` to stderr, with a |
| 194 | +prefix `Error:` in red. |
| 195 | + |
| 196 | +### 4. Idempotency |
| 197 | + |
| 198 | +Re-installing the same item to the same target is a no-op (the log |
| 199 | +already has the entry, the target already exists and points to the |
| 200 | +same source). Print a message: `Already installed at <target> |
| 201 | +(mode=<mode>). Use --force to reinstall.` and exit code 0. |
| 202 | + |
| 203 | +This is important because the `nightly-sync` phase (5.4) may |
| 204 | +re-install skills periodically; it must not error on each run. |
| 205 | + |
| 206 | +### 5. Edge cases |
| 207 | + |
| 208 | +- **Source moved/renamed**: if a previously-installed item's source |
| 209 | + no longer exists, `list` shows `Status: missing` for that entry. |
| 210 | + `uninstall` still works (removes target and log entry). |
| 211 | +- **Target is a regular file (not directory or symlink)**: error, |
| 212 | + refuse to clobber. |
| 213 | +- **Cross-device symlink**: if target is on a different filesystem |
| 214 | + and symlinks aren't supported, error explicitly and suggest |
| 215 | + `--mode copy`. |
| 216 | +- **Permissions**: if target requires elevated permissions, error |
| 217 | + explicitly (don't try to sudo). |
| 218 | +- **Relative source paths**: must be relative to the current working |
| 219 | + directory. If the user runs from elsewhere, the script should |
| 220 | + resolve correctly via `Path.cwd()` and `Path.resolve()`. |
| 221 | + |
| 222 | +### 6. CONTRIBUTING.md update |
| 223 | + |
| 224 | +Add a new section `## Deploying skills and subagents` after the |
| 225 | +existing `## Adding a subagent` section. Cover: |
| 226 | + |
| 227 | +- Why deployment is needed (catalog vs runtime location). |
| 228 | +- Symlink vs copy mode — when to choose each. |
| 229 | +- Default targets and how to override. |
| 230 | +- Example commands for each subcommand. |
| 231 | +- Mention of the install log location. |
| 232 | + |
| 233 | +Length: ~30-40 lines. |
| 234 | + |
| 235 | +### 7. README.md update |
| 236 | + |
| 237 | +In the existing folder map, augment the `tools/` entry to mention |
| 238 | +`install.py` briefly. One line is enough: |
| 239 | + |
| 240 | +> `tools/` — Python utilities (index generator, validator, install |
| 241 | +> script for deploying skills/subagents). |
| 242 | + |
| 243 | +### 8. Test the script |
| 244 | + |
| 245 | +Before committing, run a self-test inside the phase: |
| 246 | + |
| 247 | +```bash |
| 248 | +# Install the canonical example skill (a no-op skill, safe to test) |
| 249 | +uv run python tools/install.py install skills/librarian |
| 250 | +
|
| 251 | +# Verify the symlink was created and resolves correctly |
| 252 | +ls -la ~/.claude/skills/librarian |
| 253 | +test -L ~/.claude/skills/librarian |
| 254 | +readlink ~/.claude/skills/librarian |
| 255 | +test -f ~/.claude/skills/librarian/SKILL.md |
| 256 | +
|
| 257 | +# List installs |
| 258 | +uv run python tools/install.py list |
| 259 | +
|
| 260 | +# Uninstall |
| 261 | +uv run python tools/install.py uninstall librarian |
| 262 | +
|
| 263 | +# Verify it's gone |
| 264 | +test ! -e ~/.claude/skills/librarian |
| 265 | +``` |
| 266 | + |
| 267 | +All eight commands must succeed (exit 0 where expected). If any |
| 268 | +fails, fix the script before committing. |
| 269 | + |
| 270 | +After the test, clean up: the log file should be back to its initial |
| 271 | +state (no installs). |
| 272 | + |
| 273 | +## Done criteria |
| 274 | + |
| 275 | +```bash |
| 276 | +# Script exists |
| 277 | +test -f tools/install.py |
| 278 | +
|
| 279 | +# Script is executable in the project venv |
| 280 | +uv run python tools/install.py --help |
| 281 | +
|
| 282 | +# All four subcommands work (smoke test) |
| 283 | +uv run python tools/install.py install skills/librarian |
| 284 | +test -L ~/.claude/skills/librarian |
| 285 | +uv run python tools/install.py list |
| 286 | +uv run python tools/install.py info skills/librarian |
| 287 | +uv run python tools/install.py uninstall librarian |
| 288 | +test ! -e ~/.claude/skills/librarian |
| 289 | +
|
| 290 | +# CONTRIBUTING.md extended |
| 291 | +grep -q "Deploying skills and subagents" CONTRIBUTING.md |
| 292 | +
|
| 293 | +# README.md extended |
| 294 | +grep -q "install" README.md |
| 295 | +
|
| 296 | +# Validation still passes (no regressions) |
| 297 | +uv run python tools/generate_index.py --check |
| 298 | +uv run python tools/check.py |
| 299 | +
|
| 300 | +git status --short |
| 301 | +``` |
| 302 | + |
| 303 | +## Commit |
| 304 | + |
| 305 | +```bash |
| 306 | +git add . |
| 307 | +git commit -m "feat(phase-5.3): unified install.py for deploying skills/subagents |
| 308 | +
|
| 309 | +Adds tools/install.py with four subcommands (install, uninstall, list, |
| 310 | +info) supporting symlink and copy modes. Install log at |
| 311 | +~/.aithos-install-log.yaml. Default target for skills is |
| 312 | +~/.claude/skills/; subagents require an explicit --target. |
| 313 | +CONTRIBUTING.md extended with a deployment section." |
| 314 | +git push -u origin feat/install-script |
| 315 | +``` |
| 316 | + |
| 317 | +## Stop here |
| 318 | + |
| 319 | +After commit and push, stop. Do not start Phase 5.4. |
| 320 | + |
| 321 | +Do not auto-create a PR. The user prefers to merge all remaining |
| 322 | +phases at end of day in a single coherent batch. |
| 323 | + |
| 324 | +## Safety brakes |
| 325 | + |
| 326 | +- No network calls in this phase. |
| 327 | +- Do not modify the install log if testing fails partway — clean up |
| 328 | + on error. |
| 329 | +- Do not modify any imported assets from Phase 5.2 or 5.2-bis. |
| 330 | +- Test installation must be done on a real path |
| 331 | + (`~/.claude/skills/librarian`) but cleaned up before commit, so the |
| 332 | + user's `~/.claude/skills/` is untouched after the phase completes. |
0 commit comments