Package the portable Skill as a plugin - #16
Conversation
Expose the existing portable Skill through a source-only plugin marketplace without copying its instructions. Keep installation narrow and isolated from operator state while preserving local preview and unreleased journey gates.
ReviewThe packaging split holds up and the verification claims reproduce. Two notes below, one a design question about the version pin and one about the precision of the audit claim. Neither blocks. Verification
My first test run failed with On the install smoke, the live observation agreed with the committed evidence on all of it: 8 files, the per-file byte sizes and SHA-256 digests, the installed tree digest, Mutation checkI replaced The distinction it protects is worth stating: The version pin is redundant with the fields around it
That fired for me one day after the evidence was recorded, on a patch bump, with zero behavioral difference. The evidence note gives the rationale at So the version equality adds churn without adding protection against the stated risk. What it could still catch is a version change that alters behavior the observation does not record, but the observation covers the file inventory, the digests, the tree digest, and the component counts, so that residue is small. Worth considering recording The audit claim needs a qualifier"npm audit 0" holds for
What holds upThe two-manifest split does what the description says. Deriving Commands absence rather than reading it is the right call, and the evidence note is explicit that the CLI folds Skills and Commands into one group, so Real-state presence staying run-local rather than compared across machines is the right boundary, and requiring a core registry target on every run keeps that from silently degrading into no check at all. |
Testing against a tool you do not controlMost tests you write check your own code. You call a method, you assert on what comes back, and if it changes you changed it. This PR is doing something different, and the difference is what makes it interesting. The question it has to answer is: when someone installs this plugin through Claude Code, what actually lands on their disk? That answer is not produced by any code in this repository. It is produced by the Claude Code CLI, which is somebody else's program, on a release cadence nobody here controls. You cannot unit test that. What you can do is three things, and this PR does all three. One: write down what you sawThe pattern is a characterization test, sometimes called a golden file or an approval test. You do not assert that the output equals something you reasoned out in advance. You run the real thing once, record exactly what happened, commit that recording, and from then on assert that it still happens. Here the recording is The distinction between the two npm scripts is the whole idea: "check:claude-plugin-install": "node script/check-claude-plugin-install.mjs",
"record:claude-plugin-install": "node script/check-claude-plugin-install.mjs --observation-output evidence/..."Same script. One asserts against the recording, the other replaces it. Re-recording is a deliberate act with a visible diff, not something a test run does quietly on your behalf. The reason this earns its keep: nobody had to predict that installing this plugin yields exactly eight files. They installed it, looked, and wrote down the eight. Now a future Claude Code release that starts pulling in a ninth file shows up as a failing check rather than as a surprise in somebody's profile. Two: keep the test out of your real configA test that installs a plugin has an obvious hazard. Installing normally writes to your actual Claude Code configuration and plugin cache. A test that does that has stopped being a test and started being a change to your machine. So the run installs into an isolated cache, and then it verifies that your real state did not move: function assertRealStateUnchanged(stage) {
const changedRealState = changedRealStateEntries(realStateBefore);
if (changedRealState.length === 0) return;
...
throw new Error(
`real Claude configuration or plugin cache changed ${stage}: ` + ...
"Inspect the named paths, then remove any escaped user-scoped state with:\n" +
" claude plugin uninstall firstdraft@firstdraft-skills --scope user\n" + ...
);
}Two details worth stealing. It snapshots before and compares after, rather than trusting that isolation worked. And when isolation fails it hands you the exact cleanup commands, because the person reading that error is in the middle of a mess and does not want to go read documentation. There is a sharp edge hiding in "did this path stay absent," and the PR handles it: export function pathEntryExists(target) {
try {
lstatSync(target);
return true;
} catch (error) {
if (error.code === "ENOENT") return false;
throw error;
}
}
I confirmed this is load bearing by swapping in Three: derive what the tool will not tell youThis is the subtlest part, and it is the one most worth internalizing. The plugin needs to prove it ships no Commands. The natural move is to ask the CLI for a Commands count. You cannot, because the CLI reports Skills and Commands together in one group. What comes back is That number is compatible with one Skill and no Commands, which is the truth here. It is equally compatible with zero Skills and one Command. Reading it as a Commands count would be reading a fact that is not in the data. So the evidence note spells out what the derivation actually rests on: the manifest declares no Commands, and the installed file set is exactly the eight known files. Together those support the conclusion. The live count does not, on its own, and the note says so rather than letting the number imply more than it carries. That habit generalizes well past this repository. When a tool gives you an aggregate and you want a component of it, the aggregate is not the answer. Either find independent evidence for the piece you want, or say plainly that you do not have it. The part where this gets awkwardRecording what a third-party tool does means your test now has an opinion about that tool's version. This PR puts the version inside the comparison, so a Claude Code release makes the check fail. There is a real reason for that. Whether The cost is that it fires constantly. I hit it on a patch bump one day after the evidence was recorded, with every behavioral field identical. Worth sitting with, because the tension is general. Pin loosely and you miss the drift you built the check for. Pin tightly and the check goes red so often that re-recording becomes muscle memory, which is the same as not having the check. The way out is usually to ask which field would actually move if the thing you fear happened, and assert hard on that one. Here, a version that started discovering that YAML file would change the Agents count from 0 to 1, and the Agents count is already compared strictly. |
Summary
Verification
7944bf3cBoundaries
Source packaging only. No real profile install, plugin/Skill/CLI publication, GitHub clone, model-backed session, or live service journey.