Documentation
¶
Overview ¶
Package agentsync projects imported agent definitions into native client agent directories so agents distributed through git repos work in clients that read subagent definitions from disk. It is the agent-kind tenant of the pkg/project engine: single-file targets with the dedicated-file ownership model from pkg/contexts (content hash, backup before replacing anything unmanaged, adopt).
Two render classes exist. The claude-code target is identity: the canonical AGENT.md bytes are copied verbatim. (Cursor reads ~/.claude/agents too, so it needs no target of its own — see Targets for how that was verified.) The opencode, copilot, and gemini targets render the canonical definition into each client's dialect; those renders are lossy (unmappable frontmatter keys are dropped and reported), deterministic, and one-way — adopt is refused on rendered targets.
Index ¶
- Constants
- Variables
- func HasFailures(results []SyncResult) bool
- func NeedsAttention(statuses []ProjectionStatus) bool
- func SupportedSlugs() []string
- type AdoptRefusal
- type AdoptResult
- type Entry
- type LockFile
- type Manager
- func (m *Manager) Adopt(ctx context.Context, agent, client string) (*AdoptResult, error)
- func (m *Manager) HasProjections(ctx context.Context) (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) 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 RenderFunc
- type Rendered
- 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.
const ( ActionCopied = "copied" ActionUpdated = project.ActionUpdated ActionUnchanged = project.ActionUnchanged ActionRemoved = "removed" ActionSkippedDrift = project.ActionSkippedDrift ActionSkippedUnmanaged = "skipped-unmanaged" ActionSkippedEmptyStore = "skipped-empty-store" 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 mirror the skill-kind extensions so the CLI vocabulary stays one language.
const ChannelCopy = "copy"
ChannelCopy is the only agent channel: agents are always copied, the single-file dedicated ownership model has no symlink variant. A plain string (unlike skillsync's two-valued Channel type) because nothing ever branches on it.
Variables ¶
var ( ErrUnknownClient = errors.New("unknown client") ErrUnknownAgent = errors.New("unknown agent") ErrNotAvailable = errors.New("client not initialized on this machine") ErrNotProjected = errors.New("agent 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 work.
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.
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, distinct from infrastructure errors. The CLI maps it to exit 1.
func (*AdoptRefusal) Error ¶
func (e *AdoptRefusal) Error() string
type AdoptResult ¶
type AdoptResult struct {
Agent string `json:"agent"`
Client string `json:"client"`
// Target is the projected file the content came from.
Target string `json:"target"`
// CanonicalFile is the canonical AGENT.md written into.
CanonicalFile string `json:"canonical_file"`
// BackupFile is the AGENT.md.pre-<sha> backup written store-side
// before the overwrite (empty when the content did not change).
BackupFile string `json:"backup_file,omitempty"`
// Changed reports whether the projected content differed from canon.
Changed bool `json:"changed"`
// PolicyKeysRestored reports that the projected file 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 canonical store.
PolicyKeysRestored bool `json:"policy_keys_restored,omitempty"`
}
AdoptResult describes what adopt pulled back into the canonical store.
type Entry ¶
type Entry struct {
// Target is the absolute file path gridctl wrote.
Target string
// InstalledHash is the projected file's hash exactly as written.
InstalledHash string
// CanonicalHash is the canonical AGENT.md hash at sync time. The
// render is identity, so the two coincide at write time; they diverge
// when either side changes.
CanonicalHash string
// CreatedByGridctl marks the path as gridctl-owned. Always true for
// recorded entries.
CreatedByGridctl bool
// ModelValue is the model preference a policy rewrite wrote into the
// installed bytes; non-empty marks the projection 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 (agent, client) projection record. Drift is judged against InstalledHash; staleness against CanonicalHash. The channel is always copy.
type LockFile ¶
type LockFile struct {
// Projections maps agent name → client slug → entry.
Projections map[string]map[string]*Entry
}
LockFile is the agent-kind view over the unified project lockfile, keyed agent name → client slug. The engine owns the on-disk schema, versioning, migration, and locking; this view exists so the ops code keeps the same shape as the other kinds.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns agent projections and every write into client agent directories. All target paths resolve against home; the canonical agent store lives under registryDir/agents. Mutating operations serialize on mu in-process and on the engine's cross-process lock.
func NewManager ¶
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 agent directories.
func NewManagerWithHome ¶
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 projected agent file back into the canonical store (the agent-kind sibling of `gridctl ctx adopt`). The prior AGENT.md is backed up as AGENT.md.pre-<sha> and the import origin is left untouched, so the next `gridctl skill update` sees the adopted content as a local edit and refuses to clobber it without --force. The (agent, client) pair is then force-resynced so its hashes return to in-sync.
func (*Manager) HasProjections ¶
HasProjections reports whether any agent is currently projected.
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. An empty agent store while projections are recorded is refused rather than mass-removed, mirroring the skill-kind guard: a missing or unreadable store directory reads as empty.
func (*Manager) SetModelPolicy ¶
func (m *Manager) SetModelPolicy(p *registry.ModelPolicy)
SetModelPolicy installs the compiled model preference policy for the agents scope (the stack.yaml `model_preferences.agents` block). Passing nil removes it: CLI call sites without stack context run pass-through for new work and preserve previously rewritten projections (see materialize).
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 agent 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 agents into client agent directories. With names, the named imported agents are projected to the resolved targets. With no names, every imported agent is projected and recorded projections whose agent left the store are removed (agents are single small files, so the all-by-default divergence from skill sync's named-only contract does not carry its context-flooding cost).
func (*Manager) Unsync ¶
func (m *Manager) Unsync(ctx context.Context, names []string, opts UnsyncOptions) ([]UnsyncResult, error)
Unsync removes projections: named agents, or the whole set with All. Only gridctl-owned files (those with a lock entry) are touched; unmanaged files are left alone. Removed files are backed up first.
type ProjectionStatus ¶
type ProjectionStatus struct {
Agent string `json:"agent"`
Client string `json:"client"`
Channel string `json:"channel"`
Target string `json:"target"`
// Render is the target's render class: "identity" (canonical bytes
// copied verbatim) or "lossy" (client dialect, some keys dropped).
Render string `json:"render"`
// ModelValue is the model preference a policy rewrite wrote into the
// installed bytes; empty for pass-through projections.
ModelValue string `json:"model_value,omitempty"`
State string `json:"state"`
Detail string `json:"detail,omitempty"`
// Pack names the pack that applied this projection; empty for
// projections made outside a pack. Additive provenance for UI chips.
Pack string `json:"pack,omitempty"`
SyncedAt *time.Time `json:"synced_at,omitempty"`
}
ProjectionStatus is one (agent, client) row in status output.
type RenderFunc ¶
type RenderFunc func(def *skills.AgentDefinition) (Rendered, error)
RenderFunc converts a parsed canonical agent definition into one client's native dialect. Renders must be pure and deterministic: the same definition always yields the same bytes, or drift detection manufactures false positives on every sync.
type Rendered ¶
Rendered is one render's output: the client-native bytes plus the canonical frontmatter keys the dialect could not express. Dropped is surfaced in status detail and dry-run output so lossy conversions are never silent.
type SyncOptions ¶
type SyncOptions struct {
// Clients restricts the pass to these target slugs. Empty means every
// available target.
Clients []string
// Force overwrites drifted copies and unmanaged destination files
// (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 {
Agent string `json:"agent"`
Client string `json:"client"`
Channel string `json:"channel,omitempty"`
Target string `json:"target,omitempty"`
Action string `json:"action"`
// Detail carries the lossy-render report (which canonical keys the
// target dialect dropped); empty for identity targets.
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 (agent, client) projection.
type Target ¶
type Target struct {
Slug string
Name string
// AgentsPath is a ~-template expanded against the Manager's home.
AgentsPath string
// DetectDirs mark the client as initialized on this machine. The
// agents directory itself is created on first sync, but only inside
// a detected client tree.
DetectDirs []string
// Render converts the canonical definition into the client dialect.
// Nil means identity: the canonical bytes are copied verbatim.
Render RenderFunc
// FileName maps an agent name to the target's file name. Nil means
// "<name>.md" (Copilot requires "<name>.agent.md").
FileName func(name string) string
}
Target describes one client's native agents directory. Each agent becomes a single file AgentsPath/<file name>, always copied (no symlink channel: a symlinked file would expose registry sidecar paths to client tooling and cannot express the adopt flow).
func FindTarget ¶
FindTarget returns the projection target for slug.
func Targets ¶
func Targets() []Target
Targets returns the supported projection targets in display order. claude-code is the identity target, and Cursor rides along on it: no Cursor target exists because Cursor reads ~/.claude/agents directly. Verified against the shipped Cursor bundle rather than its docs — a path predicate matching ".claude/agents/" feeds Cursor's subagent descriptors and gates their deletable flag off, so Cursor lists the agents and treats the files as read-only. Client formats churn, so re-check that predicate before treating the free ride as permanent. The rendered targets convert into each client's own dialect. VS Code Copilot does NOT read ~/.claude/agents — its global agents live under ~/.copilot/agents, which is why it needs a render target.
type UnsyncOptions ¶
type UnsyncOptions struct {
// All removes every projection instead of named agents.
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.