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
- Variables
- func HasFailures(results []SyncResult) bool
- func NeedsAttention(statuses []ProjectionStatus) bool
- func SupportedSlugs() []string
- type AdoptRefusal
- type AdoptResult
- type Channel
- type Entry
- type LockFile
- type Manager
- func (m *Manager) Adopt(ctx context.Context, skill, client string) (*AdoptResult, error)
- func (m *Manager) HasProjections() (bool, error)
- func (m *Manager) LockPath() string
- func (m *Manager) Reconcile(ctx context.Context) ([]SyncResult, error)
- func (m *Manager) SetModelPolicy(p *registry.ModelPolicy)
- func (m *Manager) SetPolicy(policy func(name string) (allowed bool, rule string))
- func (m *Manager) Statuses(ctx context.Context) ([]ProjectionStatus, error)
- func (m *Manager) Sync(ctx context.Context, names []string, opts SyncOptions) ([]SyncResult, error)
- func (m *Manager) Unsync(ctx context.Context, names []string, opts UnsyncOptions) ([]UnsyncResult, error)
- type ProjectionStatus
- type SkillSource
- type SyncOptions
- type SyncResult
- type Target
- type UnsyncOptions
- type UnsyncResult
Constants ¶
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.
const ( ActionLinked = "linked" ActionCopied = "copied" ActionUpdated = project.ActionUpdated ActionUnchanged = project.ActionUnchanged ActionRemoved = "removed" ActionSkippedDrift = project.ActionSkippedDrift ActionSkippedUnmanaged = "skipped-unmanaged" 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.
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 ¶
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.
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).
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 ¶
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 ¶
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 ¶
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) 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 ¶
FindTarget returns the projection target for slug.
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.