skillsync

package
v0.1.0-beta.15 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 15 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        = "in-sync"
	StateStale         = "stale"
	StateDrifted       = "drifted"
	StateTargetMissing = "target-missing"
)

Projection states, sharing the pkg/contexts 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            = "updated"
	ActionUnchanged          = "unchanged"
	ActionRemoved            = "removed"
	ActionSkippedDrift       = "skipped-drift"
	ActionSkippedUnmanaged   = "skipped-unmanaged"
	ActionSkippedUnavailable = "skipped-unavailable"
	ActionWouldLink          = "would-link"
	ActionWouldCopy          = "would-copy"
	ActionWouldUpdate        = "would-update"
	ActionWouldRemove        = "would-remove"
	ActionAlreadyGone        = "already-gone"
	ActionError              = "error"
)

Sync result actions.

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 = errors.New("skill projection lockfile was written by a newer gridctl version")

ErrNewerLockVersion signals a lockfile written by a newer gridctl.

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 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 `yaml:"channel"`
	// Target is the absolute path gridctl created (the symlink itself or
	// the copied directory).
	Target string `yaml:"target"`
	// CreatedByGridctl marks the path as gridctl-owned. Always true for
	// recorded entries; present in the schema so a future adopt flow can
	// track foreign paths without a format break.
	CreatedByGridctl bool `yaml:"created_by_gridctl"`
	// TreeHash is the copied directory's tree hash at sync time (empty
	// for symlinks, whose content lives in the registry).
	TreeHash string    `yaml:"tree_hash,omitempty"`
	SyncedAt time.Time `yaml:"synced_at"`
}

Entry is one (skill, client) projection record.

type LockFile

type LockFile struct {
	Version int `yaml:"version"`
	// Projections maps skill name → client slug → entry.
	Projections map[string]map[string]*Entry `yaml:"projections"`
}

LockFile records, per skill and client, what gridctl last projected. Ownership (CreatedByGridctl) is what lets sync refuse to clobber foreign paths and lets unsync remove only gridctl's own artifacts.

type Manager

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

Manager owns the projection lockfile under <home>/.gridctl 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 a flock file across processes (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.

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

func (*Manager) LockPath

func (m *Manager) LockPath() string

LockPath returns the projection lockfile path (<home>/.gridctl/skillsync.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.

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"`
	State        string     `json:"state"`
	Detail       string     `json:"detail,omitempty"`
	Experimental bool       `json:"experimental,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
}

SyncOptions configure a sync pass.

type SyncResult

type SyncResult struct {
	Skill      string `json:"skill"`
	Client     string `json:"client"`
	Channel    string `json:"channel,omitempty"`
	Target     string `json:"target,omitempty"`
	Action     string `json:"action"`
	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
	// Experimental marks targets whose documented path rests on
	// unofficial sourcing; surfaced in status output.
	Experimental 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