Skip to content

fix(install): don't destroy local changes on sync, and manage hooks/ - #111

Open
MajorLift wants to merge 3 commits into
MetaMask:mainfrom
MajorLift:fix/install-refuse-clobber
Open

fix(install): don't destroy local changes on sync, and manage hooks/#111
MajorLift wants to merge 3 commits into
MetaMask:mainfrom
MajorLift:fix/install-refuse-clobber

Conversation

@MajorLift

Copy link
Copy Markdown
Contributor

What

tools/install copies source over the installed tree with rm -rf then cp -R. An edit made in the installed copy is destroyed on the next sync, with no warning and no record.

Why the obvious guard doesn't work

Comparing source against the install cannot tell two cases apart, because both make them differ:

  • the install was edited locally → overwriting destroys the only copy
  • the source moved ahead → overwriting is the whole point

A guard that refuses on any difference breaks every routine update, which is a good way to get itself disabled.

What this does instead

Each install records a content hash of what it wrote, to .mms-install-manifest in the installed skill dir. A later run compares the install against that record. Matching means nothing local happened, so the copy proceeds no matter how far source has moved.

Bundles with no record predate the manifest and fall back to comparing against source — conservative, refusing a legitimate update once until a manifest exists, on the grounds that a one-time re-run beats a silent delete. --force restores the previous behaviour.

Also: hooks/ was unmanaged

hooks/ was in neither BUNDLE_DIRS nor the directories tools/install copies. A skill shipping a hook had it installed by hand and tracked nowhere — which is how the evidence skill's wired PreToolUse gate, the one control that fires on every publish, came to have no source of record.

The existing lint-skill-entry test asserts those two lists agree, and caught the omission when only one was updated.

Verification

Against a temp target and temp HOME:

case exit result
clean install 0
re-install, nothing touched 0
source moves ahead 0 change propagates
local edit to the install 1 REFUSED, sha unchanged
same, with --force 0 edit discarded

node --test test/*.test.mjs — 61/61.

Unrelated fix carried along

domains/performance/skills/performance/skill.md had a description that was a plain YAML scalar containing ": ", so the frontmatter did not parse. tools/install masked it by folding the value to a block scalar on the way out, so the installed copy parsed and the source did not. Folded at rest; text unchanged.

tools/install copies source over the installed tree with `rm -rf` followed by
`cp -R`, so an edit made in the installed copy is destroyed on the next sync
with no warning and no record.

Comparing source against the install cannot tell the two cases apart, because
both make them differ: the install was edited locally, or the source moved
ahead. A guard that refuses on any difference breaks every routine update,
which is a good way to get itself disabled. So each install now records a
content hash of what it wrote, and a later run compares the install against
that record. Matching means nothing local happened, and the copy proceeds no
matter how far source has moved.

Bundles with no record predate the manifest and fall back to comparing against
source. That is conservative — it refuses a legitimate update once, until a
manifest exists — on the grounds that a one-time re-run beats a silent delete.

Also adds hooks/ to BUNDLE_DIRS and to the directories tools/install copies.
It was in neither, so a skill shipping a hook had it installed by hand and
tracked nowhere. That is how the evidence skill's wired PreToolUse gate — the
one control that fires on every publish — came to have no source of record.
The existing lint-skill-entry test asserts these two lists agree, and caught
the omission when only one was updated.

Controls, against a temp target and temp HOME:

  clean install                exit 0
  re-install, nothing touched  exit 0
  source moves ahead           exit 0, change propagates
  local edit to the install    exit 1, REFUSED, sha unchanged
  same with --force            exit 0, edit discarded

`node --test test/*.test.mjs` 61/61.
`description` was a plain scalar containing ": ", which YAML reads as a nested
mapping, so the frontmatter did not parse. Any tool reading source frontmatter
with a YAML parser fails on it; tools/install masked the defect by folding the
value to a block scalar on the way out, so the installed copy parsed and the
source did not.

Folded to a block scalar at rest. The description text is unchanged.
`hooks/` was added here to `BUNDLE_DIRS` and to the installer's copy list, but
`jongsun/add/pr-validate-falsifying-test` already does both in `edf65a3`, along
with `tools/check-bundle-contract.sh` to assert the two lists agree. Two
branches making the same change is how one of them ends up reverted by a merge
resolution nobody reads.

So this branch keeps only the part that is its own: refusing to overwrite an
installed copy that has local changes. The guard iterates whatever
`BUNDLE_DIRS` holds, so it covers `hooks/` for free once that branch lands.

Dropping it also removes a defect this branch would otherwise carry into main.
`check-bundle-contract.sh` reads the array with a single-line `sed`, and the
multi-line form here made it fail to PARSE rather than fail a comparison — it
exited 2 reporting that it could not read the declaration, which is a check
that has stopped checking rather than one that found something.

Controls re-run after narrowing, since the guard now iterates a different set:
clean install 0, no-op re-install 0, source moves ahead 0 and propagates,
local edit 1 with the sha unchanged, `--force` 0 and discards. 61/61.
@MajorLift

Copy link
Copy Markdown
Contributor Author

Narrowed in 7069c46. This branch no longer touches hooks/.

Why

hooks/ was added here to BUNDLE_DIRS and to the installer's copy list. #84 already does both in edf65a3, along with tools/check-bundle-contract.sh asserting the two lists agree. Neither exists on main, so this branch wasn't redundant against its own base — but two branches making the same change is how one of them gets reverted by a merge resolution nobody reads.

So this keeps only what is its own: refusing to overwrite an installed copy that has local changes. The guard iterates whatever BUNDLE_DIRS holds, so it covers hooks/ for free once #84 lands.

It was also carrying a defect into main

check-bundle-contract.sh reads the array with a single-line sed. The multi-line form here made it fail to parse rather than fail a comparison — exit 2, "could not parse BUNDLE_DIRS". A check that has stopped checking, not one that found something. That would have landed the moment these two branches met.

Found by merging both into the trial branch and running the contract check there, which is the only place they currently coexist.

The merge confirmed the concern was real

Re-merging the narrowed branch into trial silently removed hooks from the installer's bundle loop — no conflict on that line, because only one side had touched it. The schema kept hooks, the installer lost it, and check-bundle-contract.sh caught the disagreement:

1 declared bundle director(y/ies) never reach a consuming repo.
A skill can pass lint carrying one of these and it will install, load, and answer
without it.

That is exactly the failure the contract check was written for, and exactly the failure the "two branches, one change" note above is about. Resolved on trial by keeping hooks in both.

Controls re-run after narrowing

The guard now iterates a different set of directories, so the previous run doesn't transfer:

case exit result
clean install 0
re-install, nothing touched 0
source moves ahead 0 change propagates
local edit to the install 1 REFUSED, sha unchanged
same, with --force 0 edit discarded

node --test 61/61 here; 69/69 on the trial branch with #84 and #107 merged in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant