Skip to content

fix(install): don't destroy local changes on sync - #168

Draft
MajorLift wants to merge 3 commits into
mainfrom
jongsun/fix/install-refuse-clobber
Draft

MajorLift wants to merge 3 commits into
mainfrom
jongsun/fix/install-refuse-clobber

Conversation

@MajorLift

Copy link
Copy Markdown
Contributor

Overview

tools/install now records a content hash of what it wrote to .mms-install-manifest; a later run refuses to overwrite an install that no longer matches that record. --force overrides. Carries an unrelated domains/performance frontmatter fix.

Matching is against that record, not source, so an update installs however far source has moved; only a local edit refuses. An install predating the manifest compares against source instead, refusing one legitimate update until a manifest exists.

Motivation

tools/install copies over the installed tree with rm -rf then cp -R, so an edit in the installed copy dies on the next sync, silently.

Showcase

Against a temporary install target and HOME:

case exit
clean install; re-install; source moves ahead 0
local edit to the install 1, REFUSED
same, with --force 0, edit discarded

Replaces #111, which was closed when its fork was deleted. GitHub cannot reopen a pull request whose head repository is gone, so this is a new pull request from a branch in this repository carrying the same commits.

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.
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