agentsync

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: 18 Imported by: 0

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

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

Projection states, from the engine's shared vocabulary.

View Source
const (
	ActionCopied             = "copied"
	ActionUpdated            = project.ActionUpdated
	ActionUnchanged          = project.ActionUnchanged
	ActionRemoved            = "removed"
	ActionSkippedDrift       = project.ActionSkippedDrift
	ActionSkippedUnmanaged   = "skipped-unmanaged"
	ActionSkippedUnavailable = project.ActionSkippedUnavailable
	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.

View Source
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

View Source
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.

View Source
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

func NewManager(registryDir string) (*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 agent directories.

func NewManagerWithHome

func NewManagerWithHome(home, registryDir string) *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, agent, client string) (*AdoptResult, error)

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

func (m *Manager) HasProjections(ctx context.Context) (bool, error)

HasProjections reports whether any agent is currently projected.

func (*Manager) LockPath

func (m *Manager) LockPath() string

LockPath returns the unified projection lockfile path.

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

type Rendered struct {
	Bytes   []byte
	Dropped []string
}

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

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

type UnsyncResult

type UnsyncResult struct {
	Agent      string `json:"agent"`
	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