Skip to content

Commit 164e1ca

Browse files
Rowan TrollopeRowan Trollope
authored andcommitted
Fix LiveSkills interactive installs
1 parent 1a972ca commit 164e1ca

9 files changed

Lines changed: 829 additions & 175 deletions

File tree

examples/liveskills/README.md

Lines changed: 120 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# LiveSkills
22

3-
LiveSkills is an AFS-backed CLI for installing agent skills into the folders
4-
used by Codex, Claude Code, and other local agents.
3+
LiveSkills is a Go CLI example for installing agent skills into Codex, Claude
4+
Code, and other local agent skill folders through an AFS-backed registry model.
55

6-
The product direction is intentionally simple:
6+
The normal workflow is intentionally small:
77

88
```bash
99
liveskills add <source-or-ref>
@@ -12,38 +12,36 @@ liveskills add -g <source-or-ref>
1212
liveskills list -g
1313
```
1414

15-
`add` is the happy path. It should register or version a skill in AFS when
16-
needed, then install it. Users should not need a separate `publish` step for
17-
normal installs.
18-
19-
LiveSkills installs through the workspace-plus-symlink architecture: one
20-
mounted LiveSkills workspace per scope, with agent folders linked to the
21-
canonical skill copy by default.
15+
`add` is the happy path. It can take a local skill source and register it before
16+
installing, or it can install an existing registry reference such as
17+
`local/react-best-practices`. `publish`, `show`, and `download` remain available
18+
for explicit registry, debug, and export workflows.
2219

2320
## Quick Start
2421

22+
Run these commands from this directory:
23+
2524
```bash
2625
make
27-
make install
28-
liveskills help
29-
go test ./...
30-
liveskills add ./examples/react-best-practices
31-
liveskills list
32-
liveskills add -g ./examples/react-best-practices
33-
liveskills list -g
26+
LIVESKILLS_AFS_MODE=local ./bin/liveskills help
27+
LIVESKILLS_AFS_MODE=local ./bin/liveskills add ./examples/react-best-practices --yes
28+
LIVESKILLS_AFS_MODE=local ./bin/liveskills list
29+
make test
3430
make surface-test
3531
```
3632

37-
LiveSkills uses the `afs` CLI when it is available. Set
38-
`LIVESKILLS_AFS_MODE=local` to use the local development stand-in instead.
33+
Use `LIVESKILLS_AFS_MODE=local` for isolated development. Without it,
34+
LiveSkills uses the `afs` CLI when it is available on `PATH`; if `afs` is not
35+
available, it falls back to the local adapter.
3936

40-
Registry data and the local AFS staging area live under `~/.liveskills` by default. Set `LIVESKILLS_HOME` to isolate a run:
37+
Registry data and local AFS staging data live under `~/.liveskills` by default.
38+
Set `LIVESKILLS_HOME` to isolate a demo or test run:
4139

4240
```bash
43-
LIVESKILLS_HOME=/tmp/liveskills-demo go run . find
41+
LIVESKILLS_HOME=/tmp/liveskills-demo LIVESKILLS_AFS_MODE=local ./bin/liveskills list
4442
```
4543

46-
## Install
44+
## Build And Install
4745

4846
Build the local binary:
4947

@@ -57,121 +55,137 @@ Install it onto your command line:
5755
make install
5856
```
5957

60-
By default this installs `liveskills` to `~/.local/bin/liveskills`. Override the target with `BINDIR=/path/to/bin` or `PREFIX=/usr/local`:
58+
By default this installs `liveskills` to `~/.local/bin/liveskills`. Override the
59+
target with `BINDIR=/path/to/bin` or `PREFIX=/usr/local`:
6160

6261
```bash
6362
make install PREFIX=/usr/local
6463
```
6564

66-
## Full Surface Test
67-
68-
Run the isolated CLI harness when changing command behavior:
65+
Other useful targets:
6966

7067
```bash
71-
make surface-test
68+
make test # go test ./...
69+
make surface-test # isolated end-to-end CLI harness
70+
make fmt # gofmt -w *.go
71+
make clean # remove bin/
7272
```
7373

74-
The harness builds a temporary `liveskills` binary, runs `go test ./...`, then
75-
uses isolated `HOME`, `LIVESKILLS_HOME`, and `LIVESKILLS_AFS_MODE=local` values
76-
to exercise the current Go command surface: auth, publish, find, show,
77-
download, add, list, update, remove, scan, project/global installs,
78-
agent-specific paths, validation errors, and deterministic stress coverage.
74+
## Current Install Model
7975

80-
Use `--keep-temp` to inspect the generated project, home, store, and source
81-
fixtures after a run:
76+
Project installs are the default. Global installs use `-g` / `--global`.
8277

83-
```bash
84-
python3 scripts/liveskills_surface_test.py --keep-temp --verbose
78+
For each scope, LiveSkills keeps a canonical skills workspace and installs one
79+
skill folder at a time from that canonical copy. Agent-facing skill folders are
80+
relative symlinks by default; `--copy` writes a standalone copy instead.
81+
82+
```text
83+
<project>/.liveskills/mount/skills/<skill> # project canonical copy
84+
<project>/.agents/skills/<skill> # Codex/project universal target
85+
<project>/.claude/skills/<skill> # Claude Code project target
86+
87+
~/.liveskills/mount/skills/<skill> # global canonical copy
88+
~/.codex/skills/<skill> # Codex global target
89+
~/.claude/skills/<skill> # Claude Code global target
8590
```
8691

87-
## Install Model
92+
LiveSkills owns only the specific installed skill folder. Neighboring manual
93+
skills under `.agents/skills`, `.claude/skills`, `~/.codex/skills`, or other
94+
agent folders must remain untouched.
8895

89-
Project installs are the default. Global installs use `-g` / `--global`,
90-
matching the `skills` CLI scope model.
96+
## AFS Boundary
9197

92-
LiveSkills should keep one hidden AFS-backed skills workspace per scope and put
93-
registered skill content under `skills/<skill-slug>` inside that workspace.
94-
Agent-facing skill folders then point at that canonical copy:
98+
`afs.go` and `workspace_mount.go` contain the AFS adapter boundary. In local
99+
mode, the adapter materializes checkpoint files and writes mount metadata under
100+
`$LIVESKILLS_HOME`. In CLI mode, it calls `afs` to create/import checkpoints and
101+
mount skill volumes into the canonical skills workspace.
95102

96-
```text
97-
.liveskills/mount/skills/<skill> # project canonical source
98-
.agents/skills/<skill> # relative symlink by default
99-
.claude/skills/<skill> # relative symlink by default
103+
The current shape is:
100104

101-
~/.liveskills/mount/skills/<skill> # global canonical source
102-
~/.codex/skills/<skill> # relative symlink by default
103-
~/.claude/skills/<skill> # relative symlink by default
104-
```
105+
- one skills workspace per scope
106+
- registered skill content under `skills/<skill-slug>` in that workspace
107+
- direct per-skill volume attachment at the canonical skill path
108+
- symlinked agent folders by default
109+
- copy fallback with `--copy`
105110

106-
LiveSkills owns one skill folder at a time, not the whole skills parent
107-
directory. Neighboring manually downloaded skills under `.agents/skills`,
108-
`.claude/skills`, or global agent folders must remain untouched.
111+
## Commands
109112

110-
Use `--copy` when symlinks are not wanted or not supported. Copy installs are
111-
not live-linked to the AFS workspace and should be reported that way by `list`.
113+
| Command | Description |
114+
| --- | --- |
115+
| `liveskills help` | Show the compact command screen. |
116+
| `liveskills add <source-or-ref>` | Register/version a local source if needed, then install it into the current project. |
117+
| `liveskills add -g <source-or-ref>` | Install into global agent folders. |
118+
| `liveskills add <source-or-ref> --agent <agent>` | Install for one or more selected agents. Repeat `--agent` for multiple targets. |
119+
| `liveskills add <source> --skill <skill>` | Select one or more skills from a multi-skill source. Repeat `--skill` for multiple selections. |
120+
| `liveskills add <source> --all` | Install every skill found in a multi-skill source. |
121+
| `liveskills add <source> --list` | List skills available in a source without installing. |
122+
| `liveskills add <source-or-ref> --copy` | Copy instead of symlinking from the LiveSkills canonical workspace. |
123+
| `liveskills add <source-or-ref> --yes` | Skip the interactive install confirmation. |
124+
| `liveskills list` / `liveskills ls` | Show current-project installed skills. |
125+
| `liveskills list -g` | Show global installed skills, plus current-project LiveSkills mounts. |
126+
| `liveskills find [query]` | Open the interactive skill finder in a terminal; with a query or non-TTY output, print copyable install refs. |
127+
| `liveskills find --interactive [query]` | Force the terminal skill finder, optionally seeded with a query. |
128+
| `liveskills publish <source> [--skill <name>]` | Advanced: register a source without installing it. |
129+
| `liveskills show <owner>/<skill>` | Show registry details and versions for a skill. |
130+
| `liveskills download <owner>/<skill> --output <dir>` | Export a registry snapshot to a local directory. |
131+
| `liveskills update <owner>/<skill>` | Move an existing install to the selected/latest version. |
132+
| `liveskills remove <owner>/<skill>` / `liveskills rm <owner>/<skill>` | Remove a managed install. |
133+
| `liveskills scan [-g|-p] [--agent <agent>]` | Scan local skill folders without using the registry. |
134+
| `liveskills auth login` | Store registry auth settings in the local registry config. |
135+
136+
Most commands support `--json` for machine-readable output.
112137

113138
## Add Behavior
114139

115-
Target command shape:
140+
`<source-or-ref>` may be:
116141

117-
```bash
118-
liveskills add <source-or-ref>
119-
liveskills add <source-or-ref> --agent codex --agent claude
120-
liveskills add <source-or-ref> --skill react-best-practices --skill docs-review
121-
liveskills add <source-or-ref> --copy
122-
liveskills add -g <source-or-ref>
123-
```
142+
- a local skill folder containing `SKILL.md`
143+
- a local folder containing multiple skill folders
144+
- a remote Git/GitHub source
145+
- a registry reference in `<owner>/<skill>` form
146+
- a source with an inline `@skill` selector
124147

125-
`<source-or-ref>` may be a local skill folder, a source containing multiple
126-
skills, or a registry reference once registry lookup is implemented for that
127-
shape.
148+
When adding a local source, LiveSkills publishes it with owner `local` by
149+
default, then installs the selected version. If a source contains multiple
150+
skills, pass repeated `--skill <name>` values or `--all`; otherwise the command
151+
fails rather than guessing.
128152

129-
Repeated `--agent` selects multiple target agents. Repeated `--skill` selects
130-
multiple skills from a multi-skill source. Without explicit targets, `add`
131-
should prompt for any ambiguous agent, skill, scope, or install-method choice.
153+
The install output reports the selected skill, version, scope, workspace,
154+
canonical path, agent targets, and the matching `list` command. Non-JSON `add`
155+
also prints a local security assessment and a final reminder that installed
156+
skills run with full agent permissions.
132157

133-
The install flow should show:
158+
## Lists And Scans
134159

135-
- selected skills, agents, scope, and install method
136-
- security risk assessment before making filesystem changes
137-
- confirmation prompt unless a future noninteractive yes flag is used
138-
- installed paths grouped by universal, symlinked, and copied targets
139-
- final full-permissions warning for installed agent skills
160+
`liveskills list` is an installed inventory, not a registry browser. It shows
161+
LiveSkills-managed rows separately from local unmanaged skill folders discovered
162+
on disk. If no project skills are found, it hints to try `liveskills list -g`.
140163

141-
Do not add the upstream `find-skills` upsell prompt.
164+
Use `liveskills find` for the interactive registry picker, or
165+
`liveskills find [query]` for copyable search output. Use `liveskills scan` for
166+
a read-only scan of project and global skill folders across supported agents.
167+
When the picker selection is confirmed with Enter, LiveSkills shows the skill
168+
summary, asks which agents to install to, asks for project or global scope,
169+
asks for symlink or copy when multiple agent folders are targeted, shows an
170+
installation summary and local security assessment, and asks before installing.
142171

143-
## Commands
172+
## Full Surface Test
144173

145-
| Command | Description |
146-
| --- | --- |
147-
| `liveskills help` | Show available commands. |
148-
| `liveskills add <source-or-ref>` | Register/version if needed, then install skills into the project. |
149-
| `liveskills add -g <source-or-ref>` | Install skills into global agent folders. |
150-
| `liveskills add <source-or-ref> --agent <agent>` | Install for one or more selected agents. |
151-
| `liveskills add <source-or-ref> --skill <skill>` | Install one or more selected skills from a source. |
152-
| `liveskills add <source-or-ref> --copy` | Copy instead of symlinking from the LiveSkills workspace. |
153-
| `liveskills list [-g]` | Show skills installed on this computer. |
154-
| `liveskills remove <skill>` | Remove a skill from this computer. |
155-
| `liveskills update <skill>` | Update an installed skill. |
156-
| `liveskills scan [-g|-p] [--agent codex]` | Scan local skill folders on this computer. |
157-
| `liveskills find [query]` | Show published registry skills when registry discovery is needed. |
158-
| `liveskills find --interactive [query]` | Search published registry skills interactively. |
159-
| `liveskills publish <source> [--skill <name>]` | Advanced: publish/register without installing. |
160-
| `liveskills show <owner>/<skill>` | Show registry details for a skill. |
161-
| `liveskills download <owner>/<skill> --output <dir>` | Download a registry skill snapshot. |
162-
| `liveskills add <source> --list` | Show skills available in a source. |
163-
| `liveskills auth login` | Configure registry authentication. |
174+
Run the isolated CLI harness when changing command behavior:
164175

165-
## AFS Boundary
176+
```bash
177+
make surface-test
178+
```
166179

167-
`afs.go` contains the AFS adapter. In normal use it calls the `afs` CLI; in
168-
`LIVESKILLS_AFS_MODE=local` it uses the local development stand-in.
180+
The harness builds a temporary `liveskills` binary, runs `go test ./...`, then
181+
uses isolated `HOME`, `LIVESKILLS_HOME`, and `LIVESKILLS_AFS_MODE=local` values
182+
to exercise auth, publish, find, show, download, add, list, update, remove,
183+
scan, project/global installs, agent-specific paths, validation errors, and
184+
deterministic stress coverage.
169185

170-
Production behavior should preserve the same CLI contract while moving installs
171-
to the workspace-plus-symlink model:
186+
Use `--keep-temp` to inspect the generated project, home, store, and source
187+
fixtures after a run:
172188

173-
- one hidden skills workspace per scope
174-
- skill content under `skills/<skill-slug>` in that workspace
175-
- symlinked agent folders by default
176-
- copy fallback with `--copy`
177-
- no ownership of the whole agent skills parent directory
189+
```bash
190+
python3 scripts/liveskills_surface_test.py --keep-temp --verbose
191+
```

examples/liveskills/afs.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (a *LocalAFSAdapter) PublishVersion(volumeID, version string, files []Skill
100100
return "", "", err
101101
}
102102
if a.usesCLI() {
103-
if err := a.Runner.Run("vol", "import", "--force", volumeID, versionRoot); err != nil {
103+
if err := a.importVolumeSnapshot(volumeID, versionRoot); err != nil {
104104
return "", "", err
105105
}
106106
if err := a.Runner.Run("cp", "create", volumeID, checkpointID, "--description", "LiveSkills "+version); err != nil {
@@ -162,6 +162,9 @@ func (a *LocalAFSAdapter) MountSkillVolume(volumeID, checkpointID, mountPoint, s
162162
if isExistingAFSMount(err, volumeID, mountPoint) {
163163
return nil
164164
}
165+
if existingPath, ok := existingMountedVolumePath(err, volumeID); ok {
166+
return ensureCanonicalMountAlias(mountPoint, existingPath)
167+
}
165168
return err
166169
}
167170
return nil
@@ -184,12 +187,23 @@ func (a *LocalAFSAdapter) importStagedCheckpoint(volumeID, checkpointID string)
184187
if _, err := os.Stat(source); err != nil {
185188
return err
186189
}
187-
if err := a.Runner.Run("vol", "import", "--force", volumeID, source); err != nil {
190+
if err := a.importVolumeSnapshot(volumeID, source); err != nil {
188191
return err
189192
}
190193
return a.Runner.Run("cp", "create", volumeID, checkpointID, "--description", "LiveSkills "+checkpointID)
191194
}
192195

196+
func (a *LocalAFSAdapter) importVolumeSnapshot(volumeID, source string) error {
197+
err := a.Runner.Run("vol", "import", volumeID, source)
198+
if err == nil {
199+
return nil
200+
}
201+
if !isExistingVolumeImportError(err) {
202+
return err
203+
}
204+
return a.Runner.Run("vol", "import", "--force", volumeID, source)
205+
}
206+
193207
func needsStagedCheckpointImport(err error) bool {
194208
if err == nil {
195209
return false
@@ -198,6 +212,16 @@ func needsStagedCheckpointImport(err error) bool {
198212
return strings.Contains(message, "does not exist") || strings.Contains(message, "not found")
199213
}
200214

215+
func isExistingVolumeImportError(err error) bool {
216+
if err == nil {
217+
return false
218+
}
219+
message := strings.ToLower(err.Error())
220+
return strings.Contains(message, "already exists") ||
221+
strings.Contains(message, "rerun with --force") ||
222+
strings.Contains(message, "re-run with --force")
223+
}
224+
201225
func isExistingAFSMount(err error, volumeID, mountPoint string) bool {
202226
if err == nil {
203227
return false
@@ -208,6 +232,32 @@ func isExistingAFSMount(err error, volumeID, mountPoint string) bool {
208232
strings.Contains(message, strings.ToLower(filepath.Clean(mountPoint)))
209233
}
210234

235+
func isMountedVolumeAtDifferentPath(err error, volumeID string) bool {
236+
_, ok := existingMountedVolumePath(err, volumeID)
237+
return ok
238+
}
239+
240+
func existingMountedVolumePath(err error, volumeID string) (string, bool) {
241+
if err == nil {
242+
return "", false
243+
}
244+
message := err.Error()
245+
lower := strings.ToLower(message)
246+
if !strings.Contains(lower, strings.ToLower(volumeID)) || !strings.Contains(lower, "already mounted at ") {
247+
return "", false
248+
}
249+
index := strings.LastIndex(lower, "already mounted at ")
250+
if index < 0 {
251+
return "", false
252+
}
253+
path := strings.TrimSpace(message[index+len("already mounted at "):])
254+
path = strings.TrimRight(path, ". \t\r\n")
255+
if path == "" {
256+
return "", false
257+
}
258+
return filepath.Clean(path), true
259+
}
260+
211261
func afsMode(env map[string]string) string {
212262
mode := strings.ToLower(envValue(env, "LIVESKILLS_AFS_MODE"))
213263
if mode == "" {

0 commit comments

Comments
 (0)