Add thv skill sync: restore project skills from the lock file#5895
Open
samuv wants to merge 4 commits into
Open
Add thv skill sync: restore project skills from the lock file#5895samuv wants to merge 4 commits into
samuv wants to merge 4 commits into
Conversation
samuv
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
aponcedeleonch,
jhrozek,
rdimitrov and
reyortiz3
as code owners
July 21, 2026 14:48
This was referenced Jul 21, 2026
samuv
force-pushed
the
skills-lock/04-sync
branch
from
July 21, 2026 15:31
784a7ce to
9c2d5f5
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## skills-lock/03-install-hooks #5895 +/- ##
================================================================
- Coverage 71.87% 71.85% -0.02%
================================================================
Files 708 710 +2
Lines 72375 72574 +199
================================================================
+ Hits 52017 52147 +130
- Misses 16623 16676 +53
- Partials 3735 3751 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
samuv
force-pushed
the
skills-lock/03-install-hooks
branch
from
July 21, 2026 16:58
7824f3f to
1514a9d
Compare
samuv
force-pushed
the
skills-lock/04-sync
branch
from
July 21, 2026 16:58
9c2d5f5 to
06a66f8
Compare
samuv
force-pushed
the
skills-lock/03-install-hooks
branch
from
July 21, 2026 17:08
1514a9d to
3dd34f4
Compare
samuv
force-pushed
the
skills-lock/04-sync
branch
from
July 21, 2026 17:08
06a66f8 to
42a03de
Compare
samuv
force-pushed
the
skills-lock/04-sync
branch
from
July 21, 2026 17:14
42a03de to
4604083
Compare
JAORMX
reviewed
Jul 23, 2026
JAORMX
left a comment
Collaborator
There was a problem hiding this comment.
🤖 Automated panel review — PR 4/6 (thv skill sync)
Multi-agent review, each finding adversarially verified against the diff. Advisory. Whole stack gated behind TOOLHIVE_SKILLS_LOCK_ENABLED (off by default).
🔴 Blocker
sync --checksilently passes when a locked skill is entirely missing (sync.go:~92). Drift is only recorded insideif dbOK { result.Drifted = … }, then the code returns early onif opts.Check. A lock entry with no install record (dbOK=false) lands in no result bucket → the CLI prints "Nothing to sync" and the drift gate exits 0. SQLite state is per-machine and uncommitted, so on a fresh clone / fresh CI runner every entry isdbOK=false— meaning the RFC's canonicalnpm ci-style gate green-lights a checkout that is missing every locked skill. Fix: recorddbOK==falseas drift/missing before theCheckreturn. No test covers missing-entry +--check(TestSync_MissingInstallIsRestoredonly covers the non-check path).
🟠 Major
reinstallPinnedre-resolves dependencies from mutable source during a restore (sync.go:~1681). It calls the fullInstall(SyncRestore=true) with no signal suppressingtoolhive.requiresrecursion. Under the base recursion (#5894), restoring a parent reads itsSKILL.mdand re-installs each dep by its source string (a mutable tag/branch) with noSyncRestore/Digest. If the tag has moved, the dep is re-extracted to the new digest and its lock entry is rewritten — and if that dep sorted before its parent in the sync loop, nothing restores it. So a "deterministic restore" silently upgrades and re-pins a dependency against a mutable source, defeating the RFC's "restore at the pinned reference without re-resolving source." Suppressrequiresrecursion on restore (each dep already has its own entry the loop restores directly). Parent+dependency restore is untested.
🟡 Minor
classifySyncFailuremaps every HTTP 500 →FailureReasonLockWriteFailed(sync.go:~214). Several install-path 500s aren't lock-write failures (e.g.git resolver is not configured,path resolver is required), so automation keying onreason == lock-write-failedgets a misleading typed reason.- E2E decodes
AlreadyCurrentfrom the wrong JSON key (test/e2e/api_skills_test.go:~2074):up_to_datevs the actualalready_current— a latent silently-passing assertion. - PR scope: ~12 non-test/non-generated source files (guideline is 10), and it folds the
SyncRestoreinstall fast-path fix (a behavior change to the sharedInstallpath) into the sync feature PR. Per.claude/rules/pr-creation.mdthat incidental fix ideally lands as its own PR so it can be reviewed/reverted independently — worth at least an explicit size-exception note.
🟢 Verified praise
- The
SyncRestorefast-path bypass (your most-wanted-scrutiny item) is correctly scoped: it forces full re-extraction only for sync-restore and does not regress normal installs. Verified clean in bothinstall_extraction.goandinstall_git.go.
samuv
force-pushed
the
skills-lock/04-sync
branch
from
July 23, 2026 09:30
e4b12ca to
a954095
Compare
samuv
force-pushed
the
skills-lock/03-install-hooks
branch
from
July 23, 2026 09:30
176f367 to
1c30244
Compare
Contributor
Author
|
The blocker was real and is fixed, along with the restore-path major — plus a latent bug the panel's trace led us to. In the latest push:
|
This was referenced Jul 23, 2026
JAORMX
previously approved these changes
Jul 23, 2026
JAORMX
left a comment
Collaborator
There was a problem hiding this comment.
🤖 Automated panel re-review on a954095 → approving
The blocker and the major are both fixed and adversarially confirmed against the actual code:
- ✅ 🔴 Blocker (
sync --checkfalse-pass on fresh clone): a newSyncResult.Missingbucket is recorded before theif opts.Checkreturn, surfaced in the CLI ("Missing (not installed)") and counted byisSyncResultEmpty. A fresh clone no longer reports "Nothing to sync". The recording defect this PR owns is gone. - ✅ Major (restore re-resolves deps from mutable source):
InstallOptions.SyncRestoreis set byreinstallPinnedand gatesmaterializeDependenciesinrecordLockState, so a restore never re-resolves a dependency's source — each dep is restored from its own pinned entry. Verified end-to-end byTestSync_RestoreDoesNotReResolveDependencies(dep ordered before parent, upstream moved, digest asserted unchanged). - ✅ Minors fixed:
errLockWritesentinel +errors.Is(no more blanket 500→lock-write-failed); corrected E2Ealready_currentJSON key. Two bonus latent bugs (explicit-flag promotion on restore, single-client verification) also fixed.
Two notes (non-blocking):
- The end-to-end exit-2 CI gate for
--checklands in stacked PR #5897 (syncExitError), not here — this PR correctly fixes the recording defect it owns, and I've verified #5897 delivers the exit code. Sosync --checkin this PR alone still exits 0; the gate is closed once #5897 is in the stack. - New minor: a fresh-clone (
Missing) restore without--clientsforwards an emptyClientstoInstall, whose "empty = first client" semantics contradictSyncOptions.Clients' documented "every detected client". Since the lock records no per-entry clients, this silently narrows targets — worth expanding to all detected clients or tightening the doc. LGTM.
samuv
force-pushed
the
skills-lock/04-sync
branch
from
July 23, 2026 11:28
a954095 to
3b4e60f
Compare
samuv
force-pushed
the
skills-lock/03-install-hooks
branch
from
July 23, 2026 11:28
1c30244 to
773d3ff
Compare
RFC THV-0080 needs a way to restore a project's pinned skill set on any machine (a fresh clone, CI, a teammate's laptop) and to verify on-disk content still matches the lock file without installing anything. This adds the full sync vertical slice: service logic, HTTP endpoint, client, and CLI command. - Sync reconciles installed skills against toolhive.lock.yaml: missing/drifted entries reinstall at the pinned reference (never re-resolving source, see buildPinnedReference), --check reports drift read-only, --adopt/--prune handle unlocked installs - Fixes a real bug this surfaced: reinstalling at an unchanged pinned digest hit both the OCI and git install paths' "same digest means content is already correct" fast path, silently skipping re-extraction — exactly the drift sync exists to repair. A new internal SyncRestore option bypasses that fast path. - POST /api/v1beta/skills/sync via a narrow skillSyncer interface (pkg/api/v1), so this PR doesn't need to ship an Upgrade stub - `thv skill sync` CLI (--check/--adopt/--prune/--clients/ --project-root, auto-detected via .git when omitted) Part of the production stack for #5715 (RFC THV-0080). Stack: 4/6.
reinstallPinned reinstalls at a pinned reference derived from the lock entry, and that pinned string was flowing straight into ResolvedReference via the install path — a drift repair overwrote it with an internal restore form instead of preserving what Source had actually resolved to, which later corrupts upgrade's ref-change detection. Add a LockResolvedReference override on InstallOptions, mirroring the existing LockSource one.
Picks up the AlreadyCurrent JSON tag rename and Drifted doc comment fix from the PR2 review pass.
The panel review's blocker: drift was only recorded for entries with an install record, so a lock entry with none — every entry on a fresh clone or CI runner, since SQLite state is per-machine — landed in no result bucket and sync --check green-lit a checkout missing all of its locked skills. Add a Missing bucket, recorded before the check return. Restores had three related correctness gaps: restoring a parent re-materialized its toolhive.requires from mutable source strings (silently upgrading and re-pinning a dependency whose upstream tag moved), a restore promoted non-explicit dependencies to explicit (permanently exempting them from cascade removal), and verification hashed only the first client's directory (tampering with any other client's copy was invisible, install-order-dependently). Skip requires recursion on SyncRestore, preserve Explicit verbatim, and verify every client directory. Also classify lock-write failures via a sentinel instead of mapping every HTTP 500 to lock-write-failed, and fix the E2E decode struct reading AlreadyCurrent from a stale JSON key — a silently-passing assertion.
samuv
force-pushed
the
skills-lock/04-sync
branch
from
July 23, 2026 11:54
3b4e60f to
f8fe482
Compare
samuv
force-pushed
the
skills-lock/03-install-hooks
branch
from
July 23, 2026 11:54
773d3ff to
78b4c7d
Compare
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.
Summary
syncvertical slice in one PR since the CLI command can't exist without its API endpoint.Syncreconciles installed skills againsttoolhive.lock.yaml: missing/drifted entries reinstall at the pinned reference (never re-resolvingsource— seebuildPinnedReference),--checkreports drift read-only,--adoptrecords lock entries for unmanaged installs,--pruneremoves installs no longer in the lock.syncexists to repair (a tampered file doesn't change the pinned commit/digest). A new internalSyncRestoreoption onInstallOptionsbypasses that fast path in bothinstall_extraction.goandinstall_git.go.POST /api/v1beta/skills/sync, wired through a narrowskillSyncerinterface inpkg/api/v1so this PR doesn't need to ship anUpgradestub (widens toskills.SkillLockServicein PR5).thv skill syncCLI:--check,--adopt,--prune,--clients,--project-root(auto-detected by walking up for.gitwhen omitted).Part of the production stack for #5715 (RFC THV-0080). Stack: 4/6 — lockfile → lock-service → install-hooks → sync → upgrade → cli-exitcodes. Based on #5894 (PR3).
Type of change
Test plan
task test)task test-e2e, scoped:ginkgo --label-filter=skills-lock)task lint-fix)Table-driven drift-matrix tests cover up-to-date / drifted-and-reinstalled / missing-and-restored / check-mode-no-write / adopt / prune / feature-disabled-403; golden tests for
buildPinnedReference(OCI digest-pin, git commit-pin with and without a subdir path) andisImmutableSource; API handler tests including the 501 "not supported" path when the underlying service lacksSync. New E2E spec: install → delete files on disk →sync --checkreports drift without touching anything →syncrestores the files.Does this introduce a user-facing change?
Not yet — same
TOOLHIVE_SKILLS_LOCK_ENABLEDgate as the rest of the stack (see PR3). Once the full stack lands:thv skill syncrestores a project's skills to match its committed lock file.Special notes for reviewers
SyncRestorefix (install_extraction.go,install_git.go) is the part I'd like the most scrutiny on — it's a small diff but changes behavior on the existing install fast paths, gated entirely behind an internal-only option onlySyncsets.pkg/skills/client.ClientgainsSyncbut does not yet assertskills.SkillLockServiceconformance — that lands in PR5 onceUpgradeexists too.🤖 Generated with Claude Code