Skip to content

Add project-level skills lock file (POC)#5715

Closed
samuv wants to merge 20 commits into
mainfrom
poc-thv-skills-lock
Closed

Add project-level skills lock file (POC)#5715
samuv wants to merge 20 commits into
mainfrom
poc-thv-skills-lock

Conversation

@samuv

@samuv samuv commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Why: Project-scoped skill installs had no reproducible pin, no integrity verification beyond content hashing, and no safe upgrade path when publisher identity changes. Teams could not commit a lock file and trust that sync/upgrade would restore or advance skills safely.
  • What: Introduce toolhive.lock.yaml for project scope with thv skill sync and thv skill upgrade, then fold RFC THV-0080 v1 Sigstore signing/verification into the same POC: verify on consume, sign on push, pin publisher identity (provenance) or record unsigned: true, and re-verify stored bundles offline in sync --check.

POC scope (RFC THV-0080)

This PR is a feasibility vertical slice, not production-hardening split across many small PRs yet. It proves:

Question How this PR answers it
Reproducible project installs? Lock pins source, resolvedReference, digest, contentDigest
On-disk tampering? contentDigest dirhash + sync --check
Adopt out-of-band installs? sync --adopt writes lock entries (with provenance or unsigned)
Transitive deps? toolhive.requires materialized; requiredBy + cascade uninstall
Safe upgrades? Digest re-resolve; immutable pins; --allow-ref-change; --allow-signer-change
Supply-chain trust? Sigstore verify on install/sync/upgrade; TOFU identity in lock; offline bundle re-check
Publisher signing? thv skill push signs with --key (cosign); --no-sign escape hatch
CI/scripting? Typed failure reasons, --yes, exit codes 2/3/4

Trust model (v1)

Three layers (see docs/arch/12-skills-system.md):

  1. Cryptographic — Fulcio + Rekor for OCI/gitsign; offline bundle re-verification in sync --check
  2. Publisher identity (TOFU)provenance.signerIdentity + provenance.certIssuer pinned at first verified install; signer-mismatch on divergence
  3. Human review — PR review of toolhive.lock.yaml; unsigned: true is an explicit reviewed exception

Deferred: keyless OIDC push (E2E against Sigstore staging); catalog-supplied expected identity (needs toolhive-core Provenance on Skill types).

Lock entry schema (v1)

version: 1
skills:
  - name: code-review
    version: "1.0.0"
    source: ghcr.io/org/code-review
    resolvedReference: ghcr.io/org/code-review:v1.2.0
    digest: sha256:abc...
    contentDigest: sha256:def...
    provenance:
      signerIdentity: https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main
      certIssuer: https://token.actions.githubusercontent.com
      repositoryURI: https://github.com/org/repo
      sigstoreURL: https://rekor.sigstore.dev
    requiredBy: [parent-skill]
    explicit: true
  - name: local-build
    source: local-build
    digest: sha256:...
    contentDigest: sha256:...
    unsigned: true

CLI surface

thv skill sync--check, --adopt, --prune, --yes, --clients, --project-root

thv skill upgrade [name...]--preview, --fail-on-changes, --allow-ref-change, --allow-signer-change, --yes

thv skill install--allow-unsigned (project scope)

thv skill push--key, --no-sign

Exit codes: 0 success · 2 check/freshness failure · 3 partial failure · 4 policy rejection

Commit structure (19 commits, atomic by layer)

Lock file core

  1. Add project-level skills lock file with sync and upgrade commands
  2. Extend lockfile schema with contentDigest and validation
  3. Split SkillLockService from SkillService
  4. Add managed flag for lock-managed skill installs
  5. Harden install hooks and materialize toolhive.requires
  6. Implement RFC sync with check, adopt, and prune
  7. Implement RFC upgrade with preview and ref-change guard
  8. Wire lock service through API, client, and CLI
  9. Rename SyncResult UpToDate field to fix codespell
  10. Document skills lock file sync and upgrade behavior

Sigstore v1

  1. Add provenance and unsigned fields to skills lockfile
  2. Add Sigstore verifier and signer packages for skills
  3. Store verified Sigstore bundles on installed skills
  4. Verify Sigstore signatures during project-scope installs
  5. Re-verify stored bundles offline during skill sync
  6. Block skill upgrades on signer identity changes
  7. Sign OCI artifacts after skill push
  8. Expose Sigstore install and push flags in API and CLI
  9. Document skills lock Sigstore trust model and update tests

Type of change

  • New feature

Test plan

  • Unit tests (task testpkg/skills/... passes)
  • Linting (task lint-fix)
  • E2E against Sigstore staging (deferred)
  • Manual: signed OCI push + project install + sync --check

API compatibility

Additive: POST /skills/sync, POST /skills/upgrade; new request fields (allowUnsigned, allowSignerChange, key, skipSigning). SkillLockService is separate from SkillService. No operator CRD changes.

Large PR justification

~6.6k lines / 74 files. Kept unified for POC feasibility: lock integrity, sync/upgrade guardrails, and Sigstore trust are coupled — splitting mid-stack leaves non-functional intermediate states. If validated, split into the stack below for production merge.

Review focus

  1. Is contentDigest the right integrity primitive?
  2. Are cascade uninstall semantics (requiredBy / explicit) correct?
  3. Is TOFU provenance the right default until catalog trust exists?
  4. Is offline bundle re-verification in --check sufficient?
  5. Is SkillLockService the right API boundary?

Does this introduce a user-facing change?

Yes — new sync/upgrade commands, lock file on project install, Sigstore flags, push signing.

@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Jul 3, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Large PR Detected

This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.

How to unblock this PR:

Add a section to your PR description with the following format:

## Large PR Justification

[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformation

Alternative:

Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.

See our Contributing Guidelines for more details.


This review will be automatically dismissed once you add the justification section.

@JAORMX

JAORMX commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Love it! Let's talk on Monday about this. Shall we have an RFC too? Just for docs

Comment thread pkg/skills/lockfile/lockfile.go Dismissed
Comment thread pkg/skills/lockfile/lockfile.go Dismissed
Comment thread pkg/skills/lockfile/lockfile.go Dismissed
Comment thread pkg/skills/lockfile/lockfile.go Dismissed
Comment thread pkg/skills/lockfile/lockfile.go Dismissed
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 29.44855% with 1241 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.75%. Comparing base (9662681) to head (18d7dd1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/skills/verifier/oci_bundle.go 0.00% 181 Missing ⚠️
pkg/skills/skillsvc/sync.go 25.32% 160 Missing and 11 partials ⚠️
pkg/skills/skillsvc/upgrade.go 35.09% 116 Missing and 19 partials ⚠️
pkg/skills/skillsvc/verify.go 23.80% 104 Missing and 8 partials ⚠️
pkg/skills/skillsvc/lock.go 13.08% 91 Missing and 2 partials ⚠️
pkg/skills/signer/cosign_attach.go 0.00% 50 Missing ⚠️
pkg/skills/signer/key.go 0.00% 45 Missing ⚠️
pkg/skills/signer/signer.go 0.00% 43 Missing ⚠️
pkg/skills/verifier/git.go 0.00% 36 Missing ⚠️
pkg/skills/verifier/oci.go 0.00% 32 Missing ⚠️
... and 21 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5715      +/-   ##
==========================================
- Coverage   70.74%   69.75%   -1.00%     
==========================================
  Files         683      704      +21     
  Lines       69194    70884    +1690     
==========================================
+ Hits        48950    49443     +493     
- Misses      16663    17741    +1078     
- Partials     3581     3700     +119     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@samuv

samuv commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Love it! Let's talk on Monday about this. Shall we have an RFC too? Just for docs

Yeah 🚀 , I opened this RFC: stacklok/toolhive-rfcs#80. I just refined it a bit.

@samuv samuv self-assigned this Jul 7, 2026
@github-actions
github-actions Bot dismissed their stale review July 7, 2026 10:03

Large PR justification has been provided. Thank you!

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review.

@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jul 7, 2026
@samuv
samuv force-pushed the poc-thv-skills-lock branch from dafb744 to ae2f41a Compare July 7, 2026 10:16
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jul 7, 2026
Comment thread pkg/skills/signer/key.go Dismissed
Comment thread pkg/skills/signer/signer.go Dismissed
Introduce toolhive.lock.yaml, committed at the project root, that pins the
name, version, source, resolved reference, and digest of every project-scoped
skill install. This gives teams reproducible skill installs (restorable via
"thv skill sync") and a path to refresh pinned content when the catalog moves
("thv skill upgrade"), mirroring the role package-lock.json plays for npm.

- New pkg/skills/lockfile package: schema, load/save, and file-locked
  upsert/remove using pkg/fileutils.WithFileLock; entries sorted by name for
  stable diffs.
- skillsvc install/uninstall hooks: project-scope installs upsert a lock
  entry (recording the original source string for later re-resolution);
  uninstalls remove it. User-scope installs never touch the lock.
- Sync: installs each entry strictly at its pinned resolvedReference@digest,
  reports unmanaged project-scope skills, and prunes them with --prune.
- Upgrade: re-resolves each entry's original source, compares digests, and
  installs + rewrites the entry on change; immutable sources (OCI digest,
  git commit) reported as not-upgradable; --dry-run supported.
- API: POST /skills/sync and POST /skills/upgrade plus skills client methods.
- CLI: thv skill sync and thv skill upgrade with git-root auto-detection and
  --project-root override; JSON/text output.
- Docs: regenerated CLI/swagger docs; updated docs/arch/12-skills-system.md
  with a Project Lock File section, diagram, and endpoint/CLI tables.

Co-authored-by: Cursor <cursoragent@cursor.com>
samuv added a commit that referenced this pull request Jul 21, 2026
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.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 needs a controlled way to pull newer content for
skills pinned by source, without silently drifting the lock file
on every install. This adds the full upgrade vertical slice:
service logic, HTTP endpoint, client, and CLI command.

- Upgrade re-resolves each targeted entry's Source (via the same
  git/OCI/registry dispatch order Install uses, stopping short of
  extraction — resolveLatestState) and installs newer content when
  the digest changed; Source itself is never rewritten
- Entries pinned to an immutable reference (an OCI digest or full
  git commit hash) report not-upgradable — there's nothing newer to
  resolve to
- --preview reports what would change without persisting it;
  --allow-ref-change permits the resolved reference itself
  changing; --fail-on-changes gives CI a freshness gate
- var _ skills.SkillLockService = (*service)(nil) lands here now
  that both Sync and Upgrade exist; SkillsRouter widens from the
  narrow skillSyncer interface (PR4) to the full interface
- `thv skill upgrade [name...]` CLI

Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 requires every project-scoped skill install to be
pinned in toolhive.lock.yaml, including transitively materialized
toolhive.requires dependencies, with a fail-hard guarantee so a
lock-write failure never leaves an install silently unpinned.

The whole feature stays inert on main until the full RFC v1 (this
lock-file stack plus the later Sigstore stack) has landed — gated
behind TOOLHIVE_SKILLS_LOCK_ENABLED, following the existing
TOOLHIVE_DEV precedent for staged rollouts. Each PR in the stack is
independently mergeable without exposing partial behavior.

- Install hooks into the single choke point (installAndRegister):
  compute contentDigest, upsert the lock entry, roll back the DB
  record if the lock write fails
- toolhive.requires dependencies materialize recursively (visited
  set guards cycles, skills.MaxDependencies bounds the whole tree),
  with RequiredBy merged across shared dependencies
- Uninstall cascades to orphaned, non-explicit dependencies via
  Lockfile.RemoveParentFromRequiredBy, itself cycle-safe
- E2E coverage for the real HTTP API + thv serve subprocess path

Part of the production stack for #5715 (RFC THV-0080). Stack: 3/6.
samuv added a commit that referenced this pull request Jul 21, 2026
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.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 needs a controlled way to pull newer content for
skills pinned by source, without silently drifting the lock file
on every install. This adds the full upgrade vertical slice:
service logic, HTTP endpoint, client, and CLI command.

- Upgrade re-resolves each targeted entry's Source (via the same
  git/OCI/registry dispatch order Install uses, stopping short of
  extraction — resolveLatestState) and installs newer content when
  the digest changed; Source itself is never rewritten
- Entries pinned to an immutable reference (an OCI digest or full
  git commit hash) report not-upgradable — there's nothing newer to
  resolve to
- --preview reports what would change without persisting it;
  --allow-ref-change permits the resolved reference itself
  changing; --fail-on-changes gives CI a freshness gate
- var _ skills.SkillLockService = (*service)(nil) lands here now
  that both Sync and Upgrade exist; SkillsRouter widens from the
  narrow skillSyncer interface (PR4) to the full interface
- `thv skill upgrade [name...]` CLI

Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 21, 2026
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.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 needs a controlled way to pull newer content for
skills pinned by source, without silently drifting the lock file
on every install. This adds the full upgrade vertical slice:
service logic, HTTP endpoint, client, and CLI command.

- Upgrade re-resolves each targeted entry's Source (via the same
  git/OCI/registry dispatch order Install uses, stopping short of
  extraction — resolveLatestState) and installs newer content when
  the digest changed; Source itself is never rewritten
- Entries pinned to an immutable reference (an OCI digest or full
  git commit hash) report not-upgradable — there's nothing newer to
  resolve to
- --preview reports what would change without persisting it;
  --allow-ref-change permits the resolved reference itself
  changing; --fail-on-changes gives CI a freshness gate
- var _ skills.SkillLockService = (*service)(nil) lands here now
  that both Sync and Upgrade exist; SkillsRouter widens from the
  narrow skillSyncer interface (PR4) to the full interface
- `thv skill upgrade [name...]` CLI

Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 needs a controlled way to pull newer content for
skills pinned by source, without silently drifting the lock file
on every install. This adds the full upgrade vertical slice:
service logic, HTTP endpoint, client, and CLI command.

- Upgrade re-resolves each targeted entry's Source (via the same
  git/OCI/registry dispatch order Install uses, stopping short of
  extraction — resolveLatestState) and installs newer content when
  the digest changed; Source itself is never rewritten
- Entries pinned to an immutable reference (an OCI digest or full
  git commit hash) report not-upgradable — there's nothing newer to
  resolve to
- --preview reports what would change without persisting it;
  --allow-ref-change permits the resolved reference itself
  changing; --fail-on-changes gives CI a freshness gate
- var _ skills.SkillLockService = (*service)(nil) lands here now
  that both Sync and Upgrade exist; SkillsRouter widens from the
  narrow skillSyncer interface (PR4) to the full interface
- `thv skill upgrade [name...]` CLI

Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 needs a controlled way to pull newer content for
skills pinned by source, without silently drifting the lock file
on every install. This adds the full upgrade vertical slice:
service logic, HTTP endpoint, client, and CLI command.

- Upgrade re-resolves each targeted entry's Source (via the same
  git/OCI/registry dispatch order Install uses, stopping short of
  extraction — resolveLatestState) and installs newer content when
  the digest changed; Source itself is never rewritten
- Entries pinned to an immutable reference (an OCI digest or full
  git commit hash) report not-upgradable — there's nothing newer to
  resolve to
- --preview reports what would change without persisting it;
  --allow-ref-change permits the resolved reference itself
  changing; --fail-on-changes gives CI a freshness gate
- var _ skills.SkillLockService = (*service)(nil) lands here now
  that both Sync and Upgrade exist; SkillsRouter widens from the
  narrow skillSyncer interface (PR4) to the full interface
- `thv skill upgrade [name...]` CLI

Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.
samuv added a commit that referenced this pull request Jul 21, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 22, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 23, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 23, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 23, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 24, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 24, 2026
* Add SkillLockService interface and managed install flag

RFC THV-0080's sync and upgrade commands operate over a project's
whole lock file rather than a single named skill, so they need
their own service boundary (SkillLockService) instead of extending
SkillService. Installed skills also need a durable "is this
tracked in the lock file" marker so sync --prune can distinguish
lock-managed installs from ones a user manages by hand.

- SkillLockService{Sync,Upgrade} interface with generated mock
- Sync/Upgrade option and result types, RFC's typed failure reasons
- installed_skills.managed column (migration 003)

Part of the production stack for #5715 (RFC THV-0080). Stack: 2/6.
No consumers yet — skillsvc wiring lands in PR3.

* Enforce Managed only for project-scoped skills

Managed is documented as pinning a skill in a project's lock file,
but Create/Update wrote it unconditionally regardless of scope, and
the existing round-trip test created a user-scoped skill with
Managed: true. Reject the invalid combination in the store instead
of only in a doc comment. Also fix a self-contradictory SyncResult
doc comment and an inconsistent JSON tag on AlreadyCurrent.

* Trim unproduced FailureReason values and expose Managed=false

Four typed failure reasons shipped in the JSON API with no code able
to produce them: the three Sigstore verification reasons (deferred to
the signing stack) and ref-change-blocked (which surfaces as an
UpgradeStatus, not a failure). Pinning unproducible API constants now
would make refining them later a breaking change — they land with
their producers instead. Also drop omitempty from InstalledSkill's
Managed flag so list/get consumers can observe the false state, and
reword registry-unreachable's doc to cover git hosts, which map to
the same reason.
samuv added a commit that referenced this pull request Jul 24, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 24, 2026
* Record project-scope skill installs in the lock file

RFC THV-0080 requires every project-scoped skill install to be
pinned in toolhive.lock.yaml, including transitively materialized
toolhive.requires dependencies, with a fail-hard guarantee so a
lock-write failure never leaves an install silently unpinned.

The whole feature stays inert on main until the full RFC v1 (this
lock-file stack plus the later Sigstore stack) has landed — gated
behind TOOLHIVE_SKILLS_LOCK_ENABLED, following the existing
TOOLHIVE_DEV precedent for staged rollouts. Each PR in the stack is
independently mergeable without exposing partial behavior.

- Install hooks into the single choke point (installAndRegister):
  compute contentDigest, upsert the lock entry, roll back the DB
  record if the lock write fails
- toolhive.requires dependencies materialize recursively (visited
  set guards cycles, skills.MaxDependencies bounds the whole tree),
  with RequiredBy merged across shared dependencies
- Uninstall cascades to orphaned, non-explicit dependencies via
  Lockfile.RemoveParentFromRequiredBy, itself cycle-safe
- E2E coverage for the real HTTP API + thv serve subprocess path

Part of the production stack for #5715 (RFC THV-0080). Stack: 3/6.

* Fix CI failure: gitRef test fixture hardcoded http scheme

gitresolver.ParseGitReference only uses "http://" when
TOOLHIVE_DEV=true; the correct default (and what GitHub Actions
CI runners use, since they don't set that var) is "https://".
The gitRef test helper hardcoded "http://" for the fixture
registration key, so every test using it failed in CI with
"no fixture registered for https://...". Compute the scheme the
same way the resolver does instead of hardcoding either one.

* Fix install rollback and dependency cycle detection gaps

installAndRegister's rollback only deleted the DB record, so a
dependency-materialization failure after the parent's own lock entry
had already been written left toolhive.lock.yaml claiming a pin that
Info() couldn't find. Also key materializeDependencies' cycle-check
visited set by the reference a skill was installed with instead of
its resolved name, matching how dependency edges reference it — the
mismatch let a cycle through unresolved names re-materialize a skill
as its own dependency and corrupt its RequiredBy list.

* Make install rollback restorative and uninstall resurrection-proof

Three panel-review findings on the install/uninstall lifecycle:
rollback deleted only the top-level skill, leaking earlier-succeeded
sibling dependencies as managed orphans whose RequiredBy pointed at a
rolled-back parent; rollback was also unconditionally destructive, so
a --force reinstall of a valid, lock-managed skill that failed on a
new dependency destroyed its pre-existing DB record and lock entry;
and uninstall removed the lock entry last and best-effort, so a
lock-write failure after files and DB were gone left a stale entry
the next sync silently reinstalled.

Rollback now snapshots pre-install state (DB record via
InstallResult.PreExisting, lock entry before any write) and restores
it when the record pre-existed; when this call created it, removal
runs the same dependency cascade as uninstall so fresh orphans are
cleaned while shared or explicit deps survive. Uninstall removes the
lock entry first and aborts intact on failure. Materialized
dependencies also now join the parent's --group instead of falling
back to the default group.
samuv added a commit that referenced this pull request Jul 24, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 24, 2026
* Add thv skill sync: restore project skills from the lock file

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.

* Preserve ResolvedReference across sync drift repair

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.

* Regenerate swagger docs for SyncResult field changes

Picks up the AlreadyCurrent JSON tag rename and Drifted doc comment
fix from the PR2 review pass.

* Report missing installs in sync --check and pin restores properly

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 added a commit that referenced this pull request Jul 24, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 24, 2026
* Add thv skill upgrade: re-resolve pinned skills to newer content

RFC THV-0080 needs a controlled way to pull newer content for
skills pinned by source, without silently drifting the lock file
on every install. This adds the full upgrade vertical slice:
service logic, HTTP endpoint, client, and CLI command.

- Upgrade re-resolves each targeted entry's Source (via the same
  git/OCI/registry dispatch order Install uses, stopping short of
  extraction — resolveLatestState) and installs newer content when
  the digest changed; Source itself is never rewritten
- Entries pinned to an immutable reference (an OCI digest or full
  git commit hash) report not-upgradable — there's nothing newer to
  resolve to
- --preview reports what would change without persisting it;
  --allow-ref-change permits the resolved reference itself
  changing; --fail-on-changes gives CI a freshness gate
- var _ skills.SkillLockService = (*service)(nil) lands here now
  that both Sync and Upgrade exist; SkillsRouter widens from the
  narrow skillSyncer interface (PR4) to the full interface
- `thv skill upgrade [name...]` CLI

Part of the production stack for #5715 (RFC THV-0080). Stack: 5/6.

* Make --fail-on-changes a true no-mutate CI gate

Upgrade installed each target in sequence and only checked
FailOnChanges after each one returned, so a multi-skill upgrade
would install the first changed skill for real before reporting the
conflict — a partial mutation a read-only CI gate must never
produce. Split into a resolve-only planning pass (also reused to
avoid re-resolving Source a second time when applying) and an apply
pass that only runs once every target has been checked. Upgrade also
now preserves a skill's existing clients instead of expanding to
every detected client, matching Sync's behavior.

* Fix broken OCI upgrade path and make fail-on-changes report-only

Two panel-review majors left the OCI upgrade path broken (and
invisible, since coverage was git-only): resolveOCILatest returned
the unqualified ref.String() while install records qualifiedOCIRef,
so every digest change on a tag-less source tripped the ref-change
guard instead of upgrading — the single most common upgrade case;
and resolveLatestState dropped Install's registry-catalogue fallback
for ambiguous names, so a skill installed through that fallback
could never be upgraded. Fix both and add an OCI test matrix
(tag-less upgrade, tagged up-to-date, ref-change blocked/allowed,
registry-fallback source).

fail_on_changes previously returned a 409 that discarded the partial
result — hiding which skills were stale and conflating a genuine
resolution failure with "would change". It now evaluates the plan
and returns the full outcomes with a 200, mirroring sync's check
mode; exit-code mapping moves entirely to the CLI. Also accept
uppercase hex in isFullCommitHash, state honestly in the help text
that OCI previews still fetch artifacts, and return a non-nil
Outcomes slice.

* Fix forwarding notification context cancellation race

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
samuv added a commit that referenced this pull request Jul 24, 2026
RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.
samuv added a commit that referenced this pull request Jul 24, 2026
* Add typed exit codes and confirmation gate to skill sync/upgrade

RFC THV-0080 gives CI a scriptable contract for sync/upgrade: exit
2 for check/freshness failures, 3 for partial failures, 4 for
policy rejections, distinct from the generic 1 cobra already uses.
It also requires a pre-install confirmation gate — skill content
is a set of AI-followed instructions, so a non-interactive run
must refuse outright without --yes rather than silently proceeding.

- cmd/thv/app/exitcode.go: exitCodeError carries a specific exit
  code through a RunE return; main.go maps it via
  ExitCodeFromError instead of a hardcoded os.Exit(1)
- cmd/thv/app/skill_confirm.go: requireConfirmation prompts on a
  TTY, refuses with ExitCodePolicyRejection off one
- sync/upgrade gain --yes, skipped automatically by --check/
  --preview (which never install anything); their exit-code
  mapping prioritizes partial failure over drift/ref-change-blocked
- docs/arch/12-skills-system.md: new Project Lock File section
  covering rollout gating, schema, install/uninstall hooks, sync,
  upgrade, and the exit-code contract
- CLI E2E coverage for all four exit codes (0/2/4, plus --yes)

This is the last PR of Stack 1 (lock file + sync + upgrade) for
RFC THV-0080. Stack 2 (Sigstore signing/verification) builds on
top once this stack is fully merged; TOOLHIVE_SKILLS_LOCK_ENABLED
stays off by default until that full v1 lands.

Part of the production stack for #5715 (RFC THV-0080). Stack: 6/6.

* Gate upgrade's ref-change exit code behind --preview

upgradeExitError mapped a ref-change-blocked outcome to exit 4
unconditionally, even during --preview — where nothing was actually
blocked, only reported. Thread preview through, mirroring how
syncExitError already gates Drifted behind --check. Also reconcile a
docs contradiction about --preview's side effects, drop a stale
signer-change reference in the exit-code doc comment (that guard
doesn't exist until Stack 2), and add upgrade exit-code E2E coverage.

* Honor the exit-code contract under fail-on-changes and harden prompts

The panel review's major: the CLI mapped any 409 unconditionally to
exit 2, silently downgrading a genuine resolution failure during the
CI gate from "something broke" (exit 3) to "lock is stale" (exit 2)
and discarding the partial result. With the server now returning
outcomes instead of a 409, exit codes derive entirely from outcomes
with the correct precedence (failed > would-change > ref-blocked),
sync --check counts missing installs toward exit 2, and plan-only
runs print "would upgrade" instead of claiming an install happened.

The confirmation prompt also gets teeth and armor: it now prints the
lock entries being acted on (a bare "proceed?" gave the human gate
nothing to judge), goes to stderr so --format json stays parseable,
and strips non-graphic runes so a hostile directory or entry name
cannot repaint the prompt with terminal escapes. Both commands are
marked experimental in their help text, the architecture doc states
the pre-Sigstore trust model plainly (drift detection over a
repository-editable file, not verified integrity), and E2E coverage
adds the missing exit-3 path and the fresh-clone exit-2 gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants