fix(skills): install where the runtime actually reads, and stop installed meaning is_file() - #74
Merged
Conversation
…say so on the response that carries the code A skill installed into a directory the connected runtime never opens reports success, sits on disk, and still never loads. That silent failure is what the reports from Codex were. Codex reads project skills from `.agents/skills` and does not look at `.claude/skills`. A fresh project has no skill root at all, so the choice fell through to the first known convention - `.claude/skills` - and the agent was told devup-ui was installed while it went on writing devup-ui from guesses. opencode never reported it because opencode scans `.claude/skills` for compatibility and so happened to work. The runtime is now read from the MCP `clientInfo` name sent at initialize and decides the directory: claude-code -> .claude/skills codex -> .agents/skills opencode -> .opencode/skill (+ the other two, read for compatibility) unnamed -> all three A client that does not name itself gets every convention written. With no name and no existing root there is nothing to choose on, and picking one of three blind is a two-in-three chance of writing where nothing looks, against a cost of a few kilobytes inside a directory the project already owns. Install state now also reads the machine-wide roots - `~/.claude/skills`, `~/.codex/skills`, `~/.config/opencode/skill`, `~/.agents/skills` - because a skill sitting there really is loaded. A workspace holding devup-ui in all four, every one of them live, was answering `installedCount: 0`; the README itself calls telling someone to install what they already have the noise that teaches them to ignore the field. Those roots are read and never written, so the allowed-write-root boundary is unchanged. The symmetric mistake is refused too, and it is the quieter one: a file in a directory this runtime never opens is not counted as installed. `devup_skills` takes `projectRoot` like every other tool. Without it the report described the server's own write root, which is not the project unless the two were configured alike - a host granting one shared parent, an Orca worktree pool or a monorepo checkout, had every call reporting on that parent and would have installed there. The inducement moved to where agents actually read. The strongest sentence used to live on `devup_skills`'s own description, which is the tool an agent has no reason to call. It is now in `devup_figma_export`'s description, the one channel that reaches an agent which never sees the server `instructions`: a subagent handed tool schemas alone, and Codex, where `instructions` becomes a single namespace description rather than prompt text. The same gap rides the export response as `skillGap`, deliberately not gated on `projectRoot`, because the caller who does not know devup-ui is precisely the caller who does not send it. `devup_ui_validate` keeps its own copy for the case where the code was already written and refused. Install state resolves through one `Lookup` carrying project, home and runtime rather than three threaded parameters. That also makes the home injectable: reading the real one had these assertions passing in CI and failing on the machine of anyone with devup-ui installed in their own home, which is everyone working on this repository. The integration harness points the spawned server's home at a scratch directory for the same reason. Verified by using it. The release binary was driven over stdio as four named clients: codex received `.agents/skills`, claude-code `.claude/skills`, opencode `.opencode/skill`, and an unnamed client all three. A skill placed only in `~/.codex/skills` came back installed with `action: null`. A real `devup_figma_export` against the local call bank - one Figma read, none spent beyond what was already banked - returned `status: complete` with real devup-ui TSX and a `skillGap` naming devup-ui and devfive-frontend, writing to `.agents/skills`; one `devup_skills install` closed it, and re-projecting the same artifact came back with no gap and zero further Figma calls.
…the update path it never was
`installed` was decided by whether a file existed, so a `SKILL.md` written by
a devup-mcp from six months ago was indistinguishable from the current
document, and so was a one-line placeholder someone dropped in that directory.
The sting was on the other side of the same check. `install` skipped anything
already present, which meant there was no way to update a skill at all: the
first install a machine ever did was the last one it would get, and every
later call reported that stale copy as installed and current.
Install state now compares the bytes on disk against exactly what this build
would write, and answers with `installState.revision`:
current identical, or fetched from upstream - which is at least as new
as the vendored copy and cannot be compared further offline
older carries devup-mcp provenance, but is not this build's
incomplete the entry document is this build's, but a file it links to is
missing or differs - the shape that looks installed and whose
links go nowhere
foreign no devup-mcp provenance note at all
An install rewrites `older` and `incomplete` in place, at the roots where the
stale copies actually are rather than where a fresh install would have chosen,
and never touches `foreign`. Replacing a document this server did not write
would destroy someone's work, and an install is not the moment to decide that
was a mistake; it is reported under `leftAlone` instead.
`report` counts `outdated` apart from `missing`, because the two need
different words from the caller: one is a skill the agent has never seen, the
other is one it is reading right now and being taught rules this binary no
longer emits.
The runtime table grew from three clients to twelve - Cursor, Zed, Windsurf,
VS Code/Copilot, Gemini CLI, Cline, Continue, Amp and Goose, each from its own
source or official documentation. Cline is the entry that justifies the table:
it reads `.agents/skills` only from the home directory, so the shared
project-local convention is one directory it never opens, and the previous
fallback wrote exactly there. JetBrains AI Assistant is deliberately absent -
it has no auto-discovered directory and a human must register one, so guessing
a path would be worse than the fallback.
An unidentified client now reads every convention devup-mcp knows while still
writing only the three broad ones. Reading narrowly was telling a Cursor user
who had installed once into `~/.cursor/skills` that the skill was missing,
with a second copy as the remedy.
`devup_project_context` carries the same gap, scoped to what each scope
actually found. `devup.json` is devup-ui's theme format, the `openapi.json`
under scope api is generated by vespera routes, and the `models/*.json` under
scope db are vespertide schemas - an agent about to edit one had no way to
learn it needed those rules. A project with no `openapi.json` is told nothing
about vespera.
Three guide rules moved into `devup_figma_export`'s own description:
screenshots verify rather than author, do not hand-interpret the node tree,
never guess a UI value. Those three produce wrong code rather than a worse
response when violated, and they lived only in a resource that Codex collapses
into a single namespace description.
`report` also hands over this runtime's machine-wide skill directory under
`machineWide`, as a path to run yourself. The home directory stays outside the
allowed write root; widening that so a design-to-code server can write to
`$HOME` is not a trade worth making for convenience.
`autoUpdate.installedPathNote` explains that a version-named install directory
belongs to the host and is fixed at install time, so a directory reading
0.9.0-dev around a binary reporting 0.10.1 is self-update working rather than
a mismatch.
Verified by using it. Against the release binary: a stale copy came back
`revision: older` with `outdated: ["devfive-frontend"]`, and one install
restored this build's bytes and reported `current`; a hand-written document
came back `foreign`, was listed under `leftAlone`, and was still byte-identical
afterwards; deleting one reference produced `incomplete` and the next install
repaired it. `devup_project_context` named devup-ui for a project with a
devup.json, vespera for one with an openapi.json, and nothing for db with no
models. `cursor-vscode` resolved to Cursor rather than VS Code and chose
`.cursor/skills`, Zed chose `.agents/skills`, Gemini `.gemini/skills`,
`@cline/core` `.cline/skills`, `Visual Studio Code` `.github/skills`, and
`JetBrains-IU-copilot-intellij` correctly fell through to all three
conventions.
A unit test caught the hazard the short names carry: `analyzed-client`
contains `zed-`, and matched Zed until the check was anchored.
Half the runtime table is matched against client names observed in the wild rather than read out of source, because those clients are closed. A wrong guess there does not fail: the match never fires, the runtime reads `unknown`, every convention is written, and the result is safe and completely silent - indistinguishable from a client devup-mcp has genuinely never heard of. The report now echoes the name verbatim as `runtime.clientName` beside the verdict drawn from it. When nothing matched it also says what `unknown` means: that it is safe rather than wrong, and that a client with its own skill directory whose name is missing from the table is exactly what should be reported. That is the only path by which the table gets corrected, and the gap it closes is the same class of silent failure as the rest of this branch. `Lookup::new` takes the client name and derives the runtime from it rather than being handed a verdict, so the name and the conclusion cannot disagree. Verified against the release binary: `codex` and `cursor-vscode` come back recognised with their names echoed and no note; an unrecognised name comes back `unknown` with the name intact and the note explaining what to do.
installed meaning is_file()
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A skill installed into a directory the connected runtime never opens reports success, sits on disk, and still never loads. That silent failure is what the reports from Codex were.
The cause
Codex reads project skills from
.agents/skillsand does not look at.claude/skills(ext/skills/src/host_roots.rs). A fresh project has no skill root at all, so the choice fell through to the first known convention ??.claude/skills??and the agent was told devup-ui was installed while it went on writing devup-ui from guesses.opencode never reported it because opencode scans
.claude/skillsfor compatibility and so happened to work.What changes
The runtime is read from the MCP
clientInfoname sent at initialize, and decides the directory..claude/skills~/.claude/skills.agents/skills~/.agents/skills,~/.codex/skills.opencode/skill(+ the other two, for compatibility)~/.config/opencode/skill(+ compat)A client that does not name itself gets every convention written. With no name and no existing root there is nothing to choose on, and picking one of three blind is a two-in-three chance of writing where nothing looks, against a cost of a few kilobytes inside a directory the project already owns.
Install state now also reads the machine-wide roots, because a skill sitting there really is loaded. A workspace holding devup-ui in
~/.claude/skills,~/.codex/skills,~/.config/opencode/skilland~/.agents/skills??every one of them live ??was answeringinstalledCount: 0. The README itself calls telling someone to install what they already have the noise that teaches them to ignore the field. Those roots are read and never written, so the allowed-write-root boundary is unchanged.The symmetric mistake is refused too, and it is the quieter one: a file in a directory this runtime never opens is not counted as installed.
devup_skillstakesprojectRootlike every other tool. Without it the report described the server's own write root, which is not the project unless the two were configured alike ??a host granting one shared parent (an Orca worktree pool, a monorepo checkout) had every call reporting on that parent and would have installed there.Where the nudge lives now
The strongest sentence used to live on
devup_skills's own description, which is the tool an agent has no reason to call.devup_figma_export's description. The one channel that reaches an agent which never sees the serverinstructions??a subagent handed tool schemas alone, and Codex, whereinstructionsbecomes a single namespace description rather than prompt text.skillGapon the export response. Deliberately not gated onprojectRoot, because the caller who does not know devup-ui is precisely the caller who does not send it.skillGapondevup_ui_validate. Unchanged; it covers the case where the code was already written and refused.Verified by using it
The release binary driven over stdio as four named clients:
status : complete
tsx first line : import { Grid, Image } from "@devup-ui/react";
skillGap present : true
runtime : codex
missing : devup-ui, devfive-frontend
writesTo[0] : .agents\skills\devup-ui\SKILL.md
--- one devup_skills install later ---
re-export : complete, figma calls 0
skillGap present : false
--- a stale skill ---
revision : older report.outdated : ["devfive-frontend"]
refreshed : devfive-frontend
bytes restored : true revision after : current
--- a document devup-mcp did not write ---
revision : foreign leftAlone : devup-ui
still mine : true
--- a reference deleted ---
revision : incomplete -> install -> current
--- devup_project_context ---
theme -> devup-ui api -> vespera db -> (none, no models/*.json)
--- runtimes ---
cursor-vscode -> cursor .cursor/skills
Zed -> zed .agents/skills
gemini-cli-mcp-client -> gemini-cli .gemini/skills
@cline/core -> cline .cline/skills
Visual Studio Code -> vscode .github/skills
JetBrains-IU-copilot-intellij -> unknown all three conventions