skillsync

package
v0.1.0-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package skillsync projects active registry skills into native client skill directories (Claude Code's ~/.claude/skills, the vendor-neutral ~/.agents/skills interop dir, Antigravity's ~/.gemini/config/skills) so gridctl-managed skills are usable in clients that never fetch MCP prompts and auto-trigger in clients that read skills from disk. It is the directory-projection sibling of pkg/contexts: a per-client target table, a machine-global lockfile with ownership tracking, and sync/status/unsync operations. Every operation is a pure file operation; no running gateway is required. The MCP prompt channel is untouched: projection and prompts are complementary per-client delivery channels.

Index

Constants

View Source
const (
	StateInSync        = project.StateInSync
	StateStale         = project.StateStale
	StateDrifted       = project.StateDrifted
	StateTargetMissing = project.StateTargetMissing
)

Projection states, from the engine's shared vocabulary. Symlink projections of active skills are never content-stale (the link references the registry directly), but any projection goes stale when its skill leaves the active set: the pending action is removal.

View Source
const (
	ActionLinked             = "linked"
	ActionCopied             = "copied"
	ActionUpdated            = project.ActionUpdated
	ActionUnchanged          = project.ActionUnchanged
	ActionRemoved            = "removed"
	ActionSkippedDrift       = project.ActionSkippedDrift
	ActionSkippedUnmanaged   = "skipped-unmanaged"
	ActionSkippedUnavailable = project.ActionSkippedUnavailable
	ActionSkippedEmptyStore  = "skipped-empty-store"
	ActionSkippedPolicy      = "skipped-policy"
	ActionWouldLink          = "would-link"
	ActionWouldCopy          = "would-copy"
	ActionWouldUpdate        = project.ActionWouldUpdate
	ActionWouldRemove        = "would-remove"
	ActionAlreadyGone        = "already-gone"
	ActionError              = project.ActionError
)

Sync result actions. Shared ones come from the engine; the rest are skill-kind extensions.

View Source
const ChannelReasonModelPolicy = project.ChannelReasonModelPolicy

ChannelReasonModelPolicy marks a projection whose copied bytes carry a stack model preference rewrite (which is also what forced it off symlink channel). Aliased from the engine so the skill and agent kinds can never drift apart on the string.

Variables

View Source
var (
	ErrUnknownClient = errors.New("unknown client")
	ErrNotAvailable  = errors.New("client not initialized on this machine")
	ErrNotProjected  = errors.New("skill is not projected")
)

Sentinel errors callers branch on.

View Source
var ErrNewerLockVersion = project.ErrNewerLockVersion

ErrNewerLockVersion signals projection state written by a newer gridctl. Aliased from the engine so callers' errors.Is checks keep working across the pkg/project extraction.

Functions

func HasFailures

func HasFailures(results []SyncResult) bool

HasFailures reports whether any result needs the caller's attention.

func NeedsAttention

func NeedsAttention(statuses []ProjectionStatus) bool

NeedsAttention reports whether any projection requires action: drifted, stale, or a missing target. Backs the status exit code.

func SupportedSlugs

func SupportedSlugs() []string

SupportedSlugs lists the target slugs, derived from the table so error messages never go stale.

Types

type AdoptRefusal

type AdoptRefusal struct {
	// contains filtered or unexported fields
}

AdoptRefusal is a user-actionable "nothing to adopt" outcome (the projection is symlinked, empty, or otherwise not adoptable), distinct from infrastructure errors. The CLI maps it to exit 1.

func (*AdoptRefusal) Error

func (e *AdoptRefusal) Error() string

type AdoptResult

type AdoptResult struct {
	Skill  string `json:"skill"`
	Client string `json:"client"`
	// Target is the projected copy the files came from.
	Target string `json:"target"`
	// RegistryDir is the registry skill directory written into.
	RegistryDir string `json:"registry_dir"`
	// BackupFile is the SKILL.md.pre-<sha> backup written registry-side
	// before the overwrite (empty when SKILL.md did not change).
	BackupFile string `json:"backup_file,omitempty"`
	// ChangedFiles lists the relative paths written back.
	ChangedFiles []string `json:"changed_files"`
	// PolicyKeysRestored reports that the projected SKILL.md carried a
	// stack model preference rewrite whose keys were restored to the
	// author's declaration before write-back: policy-owned deltas are
	// never adopted into the registry canonical.
	PolicyKeysRestored bool `json:"policy_keys_restored,omitempty"`
}

AdoptResult describes what adopt pulled back into the registry.

type Channel

type Channel string

Channel is how one skill reaches one client: a symlink into the registry (edits propagate instantly, no drift class) or a full copy (needed where the client does not follow symlinked skill dirs).

const (
	ChannelSymlink Channel = "symlink"
	ChannelCopy    Channel = "copy"
)

type Entry

type Entry struct {
	// Channel is "symlink" or "copy".
	Channel Channel
	// Target is the absolute path gridctl created (the symlink itself or
	// the copied directory).
	Target string
	// CreatedByGridctl marks the path as gridctl-owned. Always true for
	// recorded entries; adopt reads it to tell managed copies apart.
	CreatedByGridctl bool
	// TreeHash is the registry source tree's hash at sync time (empty
	// for symlinks, whose content lives in the registry). Staleness is
	// judged against it: the registry moved when they disagree.
	TreeHash string
	// InstalledHash is the projected tree's hash exactly as written.
	// Drift (a hand edit of the copy) is judged against it. For plain
	// pass-through copies it equals TreeHash; a model policy rewrite
	// diverges them. Empty in pre-rewrite lockfiles, which migrate on
	// read as equal to TreeHash (exactly the old content-identical
	// contract).
	InstalledHash string
	// ChannelReason marks a channel diverging from what the user or
	// target table chose: ChannelReasonModelPolicy when the projection
	// was forced off symlink because its bytes carry a policy rewrite.
	// Empty for a copy the user requested themselves (--copy stays
	// sticky even when a policy rewrite touches it).
	ChannelReason string
	// ModelValue is the model preference a policy rewrite wrote into the
	// projected SKILL.md; non-empty marks the bytes as rewritten (the
	// preserve rule and adopt key on it), and the value lets adopt tell
	// the policy's write apart from a deliberate user edit. Empty for
	// pass-through projections.
	ModelValue string
	// Pack tags the projection with the pack that applied it (empty =
	// not pack-managed).
	Pack     string
	SyncedAt time.Time
}

Entry is one (skill, client) projection record.

type LockFile

type LockFile struct {
	Version int
	// Projections maps skill name → client slug → entry.
	Projections map[string]map[string]*Entry
}

LockFile is the skill-kind view over the unified project lockfile: what gridctl last projected, keyed skill name → client slug exactly as the legacy skillsync.lock.yaml was. Ownership (CreatedByGridctl) is what lets sync refuse to clobber foreign paths and lets unsync remove only gridctl's own artifacts. The engine owns the on-disk schema, versioning, migration, and locking.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns skill projections and every write into client skill directories. All target paths resolve against home, so tests point it at a temp dir. Mutating operations serialize on mu in-process and on the engine's cross-process lock (the CLI and the daemon reconcile can race).

func NewManager

func NewManager(store SkillSource) (*Manager, error)

NewManager builds a Manager rooted at the user's home directory. It is for end-of-the-line CLI call sites only: any caller in pkg/ or internal/ that tests can reach must use NewManagerWithHome so an injected home keeps the suite away from real client skill directories.

func NewManagerWithHome

func NewManagerWithHome(home string, store SkillSource) *Manager

NewManagerWithHome builds a Manager rooted at an explicit home directory. Tests use this to stay isolated from $HOME.

func (*Manager) Adopt

func (m *Manager) Adopt(ctx context.Context, skill, client string) (*AdoptResult, error)

Adopt pulls a hand-edited copy projection back into the registry skill (chezmoi re-add semantics, the skill-kind sibling of `gridctl ctx adopt`). Changed files are written through the pkg/skills local-edit conventions: the prior SKILL.md is backed up as SKILL.md.pre-<sha> and the import origin is left untouched, so the next `gridctl skill update` sees the adopted content as local edits and refuses to clobber it without --force. The (skill, client) pair is then force-resynced so its hashes return to in-sync; other clients projecting the skill go stale, which is correct: the canon changed.

func (*Manager) HasProjections

func (m *Manager) HasProjections() (bool, error)

HasProjections reports whether any skill is currently projected. The daemon reconcile uses it as a cheap no-op guard (through the context-aware form; this signature stays context-free for existing callers).

func (*Manager) LockPath

func (m *Manager) LockPath() string

LockPath returns the projection lockfile path (<home>/.gridctl/project.lock.yaml, a sibling of the registry).

func (*Manager) Reconcile

func (m *Manager) Reconcile(ctx context.Context) ([]SyncResult, error)

Reconcile re-syncs the recorded projection set. The daemon calls it after every registry refresh; it is a fast no-op when nothing is projected.

A store reporting zero active skills while projections are recorded is refused rather than reconciled, surfaced as a single ActionSkippedEmptyStore result: the registry treats a missing or unreadable directory as empty, so an empty store here is far more likely a degraded registry than a deliberate deactivate-everything, and acting on it would mass-remove every projection. Explicit `gridctl skill project sync` and `unsync` keep full authority.

func (*Manager) SetModelPolicy

func (m *Manager) SetModelPolicy(p *registry.ModelPolicy)

SetModelPolicy installs the compiled model preference policy for the skills scope. Passing nil removes it (pass-through plus preserve).

func (*Manager) SetPolicy

func (m *Manager) SetPolicy(policy func(name string) (allowed bool, rule string))

SetPolicy installs the skill exposure check. Passing nil removes it.

func (*Manager) Statuses

func (m *Manager) Statuses(ctx context.Context) ([]ProjectionStatus, error)

Statuses computes the per-projection state for everything in the projection set, sorted by skill then client. Reads are lock-free: the lockfile is written atomically.

func (*Manager) Sync

func (m *Manager) Sync(ctx context.Context, names []string, opts SyncOptions) ([]SyncResult, error)

Sync projects skills into client skill directories. With names, the named active skills are added to the projection set for the resolved targets and materialized. With no names, the recorded projection set is reconciled: dangling or missing artifacts are repaired, stale copies refreshed, and projections whose skill was deactivated or deleted are removed. Nothing is ever projected without an explicit prior request (the deliberate divergence from ctx sync's all-available default: ~90 active skills would bloat client context).

func (*Manager) Unsync

func (m *Manager) Unsync(ctx context.Context, names []string, opts UnsyncOptions) ([]UnsyncResult, error)

Unsync removes projections: named skills, or the whole set with All. Only gridctl-created artifacts are touched; copies are backed up before removal.

type ProjectionStatus

type ProjectionStatus struct {
	Skill   string `json:"skill"`
	Client  string `json:"client"`
	Channel string `json:"channel"`
	Target  string `json:"target"`
	// Render is always "identity" for skills (registry content is placed
	// as-is); present so JSON consumers see the same column the status
	// table renders across kinds.
	Render string `json:"render"`
	// ChannelReason marks a channel forced off the user's choice
	// ("model-policy" when the rewrite forced this projection from
	// symlink to copy); the status table renders it beside the channel.
	ChannelReason string `json:"channel_reason,omitempty"`
	// ModelValue is the model preference a policy rewrite wrote into
	// the projected bytes; empty for pass-through projections.
	ModelValue string     `json:"model_value,omitempty"`
	State      string     `json:"state"`
	Detail     string     `json:"detail,omitempty"`
	Unofficial bool       `json:"unofficial,omitempty"`
	SyncedAt   *time.Time `json:"synced_at,omitempty"`
}

ProjectionStatus is one (skill, client) row in `skill project status`.

type SkillSource

type SkillSource interface {
	// GetSkill returns a skill by name (a copy).
	GetSkill(name string) (*registry.AgentSkill, error)
	// ActiveSkills returns skills with state "active" (copies).
	ActiveSkills() []*registry.AgentSkill
	// Dir returns the registry base directory (skills live under
	// Dir()/skills).
	Dir() string
}

SkillSource is the slice of the registry store projection reads. The concrete *registry.Store satisfies it; tests can substitute a fake.

type SyncOptions

type SyncOptions struct {
	// Clients restricts the pass to these target slugs. Empty means every
	// available target.
	Clients []string
	// Copy projects copies instead of symlinks (copy-forced targets copy
	// regardless).
	Copy bool
	// Force overwrites drifted copies and unmanaged destination paths
	// (after a timestamped backup).
	Force bool
	// DryRun reports the plan without writing anything.
	DryRun bool
	// Pack tags recorded projections with the applying pack. Empty keeps
	// any existing tag (a plain re-sync never strips pack ownership).
	Pack string
}

SyncOptions configure a sync pass.

type SyncResult

type SyncResult struct {
	Skill   string `json:"skill"`
	Client  string `json:"client"`
	Channel string `json:"channel,omitempty"`
	// Reason names why the channel or bytes diverged from pass-through
	// ("model-policy"); the CLI renders it beside the channel so a
	// forced flip is never silent.
	Reason string `json:"channel_reason,omitempty"`
	Target string `json:"target,omitempty"`
	Action string `json:"action"`
	// Detail carries advisory notes (model policy application or
	// preservation); empty otherwise.
	Detail     string `json:"detail,omitempty"`
	BackupPath string `json:"backup_path,omitempty"`
	Error      string `json:"error,omitempty"`
}

SyncResult describes what happened (or would happen) for one (skill, client) projection.

type Target

type Target struct {
	Slug string
	Name string
	// SkillsPath is the directory skills are projected into; each skill
	// becomes SkillsPath/<name> (a symlink or a copied directory).
	SkillsPath string
	// DetectDirs mark the client as initialized on this machine when any
	// of them exists. Sync refuses to create client config trees
	// wholesale, so these gate every write unless AlwaysAvailable is set.
	DetectDirs []string
	// AlwaysAvailable targets skip detection: the vendor-neutral
	// ~/.agents/skills interop dir is created on first projection because
	// gating on its existence would silently skip clients (Grok Build)
	// that read it without ever creating it.
	AlwaysAvailable bool
	// DefaultChannel is used when the user does not pass --copy.
	DefaultChannel Channel
	// ForcedChannel, when set, overrides both the default and --copy.
	// Antigravity is copy-forced until symlink discovery is verified on
	// its exact path (symlinks went undiscovered under a related Gemini
	// skills path; vercel-labs/skills#633).
	ForcedChannel Channel
	// Unofficial marks targets whose path rests on unofficial sourcing
	// rather than published client documentation. The projection is
	// supported; the path may move without an upstream release note.
	// Surfaced in status output so the caveat is never silent.
	Unofficial bool
}

Target describes one client's native skills directory. SkillsPath is a ~-template expanded against the Manager's home directory.

func FindTarget

func FindTarget(slug string) (Target, bool)

FindTarget returns the projection target for slug.

func Targets

func Targets() []Target

Targets returns the supported projection targets in display order. Slugs match pkg/contexts and pkg/provisioner so every gridctl surface speaks one client-identifier language ("agents" names the shared interop dir, which is multi-client by design).

type UnsyncOptions

type UnsyncOptions struct {
	// All removes every projection instead of named skills.
	All bool
	// Clients restricts removal to these target slugs.
	Clients []string
	// DryRun reports what would be removed without writing.
	DryRun bool
}

UnsyncOptions configure an unsync pass.

type UnsyncResult

type UnsyncResult struct {
	Skill      string `json:"skill"`
	Client     string `json:"client"`
	Target     string `json:"target"`
	Action     string `json:"action"`
	BackupPath string `json:"backup_path,omitempty"`
}

UnsyncResult describes the removal of one projection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL