Documentation
¶
Overview ¶
Package contexts manages the canonical global agent context and projects it into each linked client's global context mechanism. The store is one canonical file (AGENTS.md per the agents.md spec) by default, or an opt-in directory of rule fragments (~/.gridctl/context/fragments/*.md; see fragments.go). Single-file strategies are a dedicated file in a rules directory, an @-import shim line, or a marker-delimited managed block; in fragments mode, clients with real rules directories receive one file per fragment (multi-file) and everything else receives a compiled document. Per-project AGENTS.md files stay version-controlled in their repos and are out of scope. Every operation is a pure file operation; no running gateway is required.
Index ¶
- Constants
- Variables
- func FragmentContentHash(raw []byte) string
- func HasFailures(results []SyncResult) bool
- func NeedsSync(statuses []ClientStatus) bool
- func RenderFragmentMD(f *Fragment) ([]byte, error)
- func SupportedSlugs() []string
- func ValidateFragmentName(name string) error
- type ClientEntry
- type ClientStatus
- type Fragment
- type FragmentAddResult
- type FragmentEntry
- type FragmentExtraField
- type FragmentLockFile
- type FragmentStatus
- type LockFile
- type Manager
- func (m *Manager) AddFragment(name, content string) (FragmentAddResult, error)
- func (m *Manager) Adopt(ctx context.Context, slug string) error
- func (m *Manager) AdoptFragment(ctx context.Context, slug, name string) error
- func (m *Manager) AdoptInto(ctx context.Context, slug, fragmentName string) error
- func (m *Manager) CanonicalContent() (string, error)
- func (m *Manager) CanonicalPath() string
- func (m *Manager) Diff(ctx context.Context, slug string, fragmentName ...string) (string, error)
- func (m *Manager) Dir() string
- func (m *Manager) EnsureFragmentsActive() (FragmentAddResult, error)
- func (m *Manager) FragmentsActive() bool
- func (m *Manager) FragmentsDir() string
- func (m *Manager) HasCanonical() bool
- func (m *Manager) InitFromClient(slug string, force bool) error
- func (m *Manager) InitFromFile(path string, force bool) error
- func (m *Manager) InitFromTemplate(force bool) error
- func (m *Manager) InstallFragmentBytes(name string, data []byte) (FragmentAddResult, error)
- func (m *Manager) ListFragments() ([]*Fragment, error)
- func (m *Manager) ReadFragment(name string) (*Fragment, error)
- func (m *Manager) RemoveFragment(name string) (backupPath string, err error)
- func (m *Manager) SaveCanonical(content string) error
- func (m *Manager) SaveFragment(f *Fragment) error
- func (m *Manager) Scan() []ScanEntry
- func (m *Manager) Statuses(ctx context.Context) ([]ClientStatus, error)
- func (m *Manager) SyncAll(ctx context.Context, opts SyncOptions) ([]SyncResult, error)
- func (m *Manager) SyncClient(ctx context.Context, slug string, opts SyncOptions) (SyncResult, error)
- func (m *Manager) SyncClientDetailed(ctx context.Context, slug string, opts SyncOptions) ([]SyncResult, error)
- func (m *Manager) Unsync(ctx context.Context, slug string) ([]UnsyncResult, error)
- func (m *Manager) UnsyncAll(ctx context.Context) ([]UnsyncResult, error)
- func (m *Manager) UnsyncPackFragments(ctx context.Context, packName string) ([]UnsyncResult, []string, error)
- type ScanEntry
- type Strategy
- type SyncOptions
- type SyncResult
- type Target
- type UnsupportedClient
- type UnsyncResult
Constants ¶
const ( ModeSingleFile = "single-file" ModeCompiled = "compiled" ModeMultiFile = "multi-file" )
Projection modes reported per client. single-file is the pre-fragments behavior and stays the default; compiled and multi-file only appear once fragments mode is active.
const ( ActionRemoved = "removed" ActionWouldRemove = "would-remove" )
Fragment-specific sync actions, extending the shared vocabulary.
const ( StateUnsupported = "unsupported" StateNeverSynced = "never-synced" StateInSync = project.StateInSync StateStale = project.StateStale StateDrifted = project.StateDrifted StateTargetMissing = project.StateTargetMissing )
Client sync states. "stale" means the target still matches what gridctl wrote but the canonical file has changed since (a sync is pending). The shared vocabulary comes from the engine; "unsupported" and "never-synced" are context-kind extensions.
const ( ActionCreated = "created" ActionUpdated = project.ActionUpdated ActionUnchanged = project.ActionUnchanged ActionSkippedDrift = project.ActionSkippedDrift ActionWouldCreate = "would-create" ActionWouldUpdate = project.ActionWouldUpdate ActionError = project.ActionError )
Sync result actions. Shared ones come from the engine; "created" and "would-create" are context-kind extensions.
Variables ¶
var ( ErrNoCanonical = errors.New("no canonical context file; run 'gridctl ctx init' first") ErrCanonicalExists = errors.New("canonical context file already exists (use --force to overwrite)") ErrUnknownClient = errors.New("unknown client") ErrUnsupported = errors.New("client has no global context file mechanism") ErrNotAvailable = errors.New("client not initialized on this machine") ErrNotSynced = errors.New("client has never been synced") ErrOverCap = errors.New("rendered content exceeds the client's size limit") )
Sentinel errors callers branch on.
var ( ErrFragmentsInactive = errors.New("fragments mode is not active; create one with 'gridctl ctx add <name>'") ErrNoFragment = errors.New("no such fragment") ErrFragmentExists = errors.New("fragment already exists") ErrBadFragmentName = errors.New("fragment name must be lowercase letters, digits, and hyphens") )
Fragment sentinel errors.
var ( // ErrAdoptRequiresFragment marks a whole-client adopt on a multi-file // target: each fragment is its own file, so adopt needs a name. ErrAdoptRequiresFragment = errors.New("adopt requires a fragment name on a multi-file target") // ErrAdoptRefusesCompiled marks an adopt on a compiled target, where // wholesale adoption would collapse every fragment into one. ErrAdoptRefusesCompiled = errors.New("adopt refuses compiled targets without a capture fragment") // ErrAdoptLossyRender marks a per-fragment adopt on a lossy dialect, // which cannot flow back into the canonical store. ErrAdoptLossyRender = errors.New("fragment render is lossy and cannot be adopted") // ErrAdoptImportShim marks an adopt on an import-shim target, which // references the canonical file directly: no copied content exists. ErrAdoptImportShim = errors.New("import shim targets have no copied content to adopt") )
Adopt refusal sentinels. Callers (the REST layer, the UI) branch on the reason with errors.Is; the user-facing prose the CLI has always printed rides on the concrete error unchanged.
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 FragmentContentHash ¶
FragmentContentHash returns the hash of raw fragment bytes under the same scheme the projection lockfile uses, so provenance recorded at install time and drift measured later are directly comparable. Exported for the pack installer, which records what it wrote.
func HasFailures ¶
func HasFailures(results []SyncResult) bool
HasFailures reports whether any result needs the caller's attention: a write error or a drifted target that was skipped.
func NeedsSync ¶
func NeedsSync(statuses []ClientStatus) bool
NeedsSync reports whether any client requires attention: drifted, stale, or a recorded sync whose target file has gone missing. Backs `ctx sync --check` and the status exit code.
func RenderFragmentMD ¶
RenderFragmentMD serializes a fragment back to its file form deterministically: modeled keys first in fixed order, then extras in their original document order, then the body. Used by write-backs (adopt); imported fragments keep their Raw bytes verbatim.
func SupportedSlugs ¶
func SupportedSlugs() []string
SupportedSlugs lists the supported client slugs, derived from the strategy table so error messages and help text never go stale.
func ValidateFragmentName ¶
ValidateFragmentName rejects names that cannot become safe filenames.
Types ¶
type ClientEntry ¶
type ClientEntry struct {
Strategy string
// Target is the absolute path gridctl wrote to.
Target string
// InstalledHash is the managed-region hash exactly as written.
InstalledHash string
// CanonicalHash is the canonical file's hash at sync time.
CanonicalHash string
// CreatedFile records whether gridctl created the target file itself
// (unsync then removes the whole file, not just the managed region).
CreatedFile bool
// InputHashes records, for a compiled fragments-mode target, each
// input fragment's hash at sync time so staleness can name which
// fragment moved. Absent outside fragments mode.
InputHashes map[string]string
SyncedAt time.Time
}
ClientEntry is one client's sync record. Drift is judged against InstalledHash; staleness against CanonicalHash.
type ClientStatus ¶
type ClientStatus struct {
Slug string `json:"slug"`
Name string `json:"name"`
Supported bool `json:"supported"`
Available bool `json:"available"`
Unofficial bool `json:"unofficial,omitempty"`
Strategy string `json:"strategy,omitempty"`
// Mode is how this client receives the context: single-file (the
// pre-fragments default), compiled, or multi-file. Omitted while
// fragments mode is off so pre-fragments consumers see no new field.
Mode string `json:"mode,omitempty"`
TargetPath string `json:"target_path,omitempty"`
State string `json:"state"`
Detail string `json:"detail,omitempty"`
SyncedAt *time.Time `json:"synced_at,omitempty"`
// Fragments lists every non-synced fragment with its own state, for
// multi-file targets only. The aggregate State/Detail keep their
// worst-state-wins prose (a drifted fragment would otherwise hide a
// stale one from any structured consumer).
Fragments []FragmentStatus `json:"fragments,omitempty"`
}
ClientStatus is one client's row in `ctx status` and GET /api/context.
type Fragment ¶
type Fragment struct {
// Name is the file base without .md; FileName includes it.
Name string
FileName string
// Description and Paths are the only modeled frontmatter keys. Paths
// are glob strings passed to clients as metadata; gridctl never
// evaluates them (the Copilot applyTo transform is the one rewrite).
Description string
Paths []string
Extra []FragmentExtraField
Body string
Raw []byte
}
Fragment is one rule fragment: optional frontmatter plus a markdown body. Raw is the file exactly as stored and is the identity-render payload.
type FragmentAddResult ¶
type FragmentAddResult struct {
// Migrated is true when this call activated fragments mode by moving
// the canonical AGENTS.md to fragments/00-default.md.
Migrated bool
// MigratedBackup is the canonical file's backup path when Migrated.
MigratedBackup string
// CreatedPath is the new fragment's path.
CreatedPath string
}
FragmentAddResult reports what AddFragment did, so the CLI can print the migration explicitly (the manager itself stays silent, matching InitFromClient).
type FragmentEntry ¶
type FragmentEntry struct {
// Target is the absolute path gridctl wrote.
Target string
// InstalledHash is the hash of the bytes exactly as written (drift).
InstalledHash string
// CanonicalHash is the source fragment's hash at sync time (staleness).
CanonicalHash string
// Pack tags the projection with the pack that applied it.
Pack string
SyncedAt time.Time
}
FragmentEntry is one projected fragment file's ownership record.
type FragmentExtraField ¶
FragmentExtraField is one frontmatter key this package does not model, preserved in document order (the pkg/skills agent parser precedent) so write-backs never strip client extensions.
type FragmentLockFile ¶
type FragmentLockFile struct {
// Projections maps fragment name -> client slug -> record.
Projections map[string]map[string]*FragmentEntry
}
FragmentLockFile is the context-fragment view over the unified project lockfile: one record per (client, fragment) projected file. It is a separate kind from the per-client context entries on purpose — contexts flushes those with ReplaceKind, and an older gridctl that cannot represent fragments must never drop or clobber their records.
type FragmentStatus ¶
type FragmentStatus struct {
Name string `json:"name"`
State string `json:"state"`
// Pack names the pack that applied this fragment projection; empty
// for projections made outside a pack. Additive provenance for UI
// chips. Whole-document (single-file and compiled) context entries
// record no pack tag, so provenance is per-fragment only.
Pack string `json:"pack,omitempty"`
}
FragmentStatus is one fragment's projection state on one multi-file client.
type LockFile ¶
type LockFile struct {
Version int
// Scope is "global" today, recorded as the engine entry's source.
Scope string
Clients map[string]*ClientEntry
}
LockFile is the context-kind view over the unified project lockfile: the per-client sync records, keyed by client slug exactly as the legacy context.lock.yaml was. The engine owns the on-disk schema, versioning, migration, and locking; this view exists so the ops code keeps its shape.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the canonical global context store under <home>/.gridctl/context and every projection into client files. 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 (shared with skill projections, so a slow context sync can briefly block a skill sync and vice versa).
func NewManager ¶
NewManager builds a Manager rooted at the user's home directory.
func NewManagerWithHome ¶
NewManagerWithHome builds a Manager rooted at an explicit home directory. Tests use this to stay isolated from $HOME.
func (*Manager) AddFragment ¶
func (m *Manager) AddFragment(name, content string) (FragmentAddResult, error)
AddFragment creates a fragment, activating fragments mode on first use: the existing canonical AGENTS.md (when present) is backed up and becomes fragments/00-default.md — an explicit, never-automatic migration.
func (*Manager) Adopt ¶
Adopt pulls a target's managed content back into the canonical file (chezmoi re-add semantics), then re-syncs that client so its hashes return to in-sync. Other clients become stale, which is correct: the canon changed.
In fragments mode: multi-file identity targets require a fragment name (use AdoptFragment); compiled targets refuse (use AdoptInto).
func (*Manager) AdoptFragment ¶
AdoptFragment pulls one hand-edited projected fragment file back into the canonical fragment, then re-syncs that client so its hashes return to in-sync (the whole-client Adopt contract). Only the identity render round-trips: a lossy dialect cannot reconstruct what it dropped, so those targets refuse.
func (*Manager) AdoptInto ¶
AdoptInto captures a compiled target's whole managed body into one designated fragment (the deliberate escape hatch for the refusal above), then force re-syncs that client so the freshly assembled document replaces the hand edit it just captured.
func (*Manager) CanonicalContent ¶
CanonicalContent reads the canonical file.
func (*Manager) CanonicalPath ¶
CanonicalPath returns the canonical global context file path.
func (*Manager) Diff ¶
Diff renders a unified diff between the canonical content and one client's current managed content. fragmentName scopes a multi-file client to one fragment; bare multi-file diff returns a per-fragment summary.
func (*Manager) EnsureFragmentsActive ¶
func (m *Manager) EnsureFragmentsActive() (FragmentAddResult, error)
EnsureFragmentsActive activates fragments mode without creating a fragment (the pack import path: fragments arrive from a repo, but the migration of an existing canonical file must still happen first).
func (*Manager) FragmentsActive ¶
FragmentsActive reports whether fragments mode is on: the directory exists. Read-only callers must treat false as "single-file store" and never create the directory themselves.
func (*Manager) FragmentsDir ¶
FragmentsDir returns the fragment store directory.
func (*Manager) HasCanonical ¶
HasCanonical reports whether the canonical file exists.
func (*Manager) InitFromClient ¶
InitFromClient adopts a client's existing global context file as the canonical context. Content gridctl previously managed there (block, shim line, header chrome) is stripped so adoption never round-trips gridctl's own markers into the canon.
func (*Manager) InitFromFile ¶
InitFromFile adopts an arbitrary file as the canonical context.
func (*Manager) InitFromTemplate ¶
InitFromTemplate scaffolds the starter canonical file.
func (*Manager) InstallFragmentBytes ¶
func (m *Manager) InstallFragmentBytes(name string, data []byte) (FragmentAddResult, error)
InstallFragmentBytes writes raw fragment file content into the store (pack import path). Activates fragments mode if needed and reports the activation so callers can surface a migration explicitly — installing must never migrate the user's AGENTS.md silently. Existing files are backed up out of tree before overwrite.
func (*Manager) ListFragments ¶
ListFragments returns every fragment in filename-lexicographic order — the composition order. Dotfiles (origin sidecars) and non-.md files are not fragments. Returns ErrFragmentsInactive when the mode is off so callers cannot accidentally treat "no directory" as "no fragments".
func (*Manager) ReadFragment ¶
ReadFragment returns one fragment by name.
func (*Manager) RemoveFragment ¶
RemoveFragment deletes a fragment, backing it up first (out of tree, so nothing lingers in a directory that is now load-bearing configuration).
func (*Manager) SaveCanonical ¶
SaveCanonical writes the canonical file with a backup of the previous revision. Content is normalized to end with exactly one newline.
func (*Manager) SaveFragment ¶
SaveFragment writes a fragment back to the store (the adopt path), backing up any existing file out of tree.
func (*Manager) Scan ¶
Scan inspects every supported client's import location so init can offer adoption before scaffolding.
func (*Manager) Statuses ¶
func (m *Manager) Statuses(ctx context.Context) ([]ClientStatus, error)
Statuses computes the per-client sync state for every known client, supported and unsupported, in display order.
func (*Manager) SyncAll ¶
func (m *Manager) SyncAll(ctx context.Context, opts SyncOptions) ([]SyncResult, error)
SyncAll projects the canonical context to every supported, available client. Unavailable clients are reported as skipped, never errors.
func (*Manager) SyncClient ¶
func (m *Manager) SyncClient(ctx context.Context, slug string, opts SyncOptions) (SyncResult, error)
SyncClient projects the canonical context to one explicitly named client. Unlike SyncAll, an unavailable client is an error here: the user asked for it by name and should hear why nothing happened. Multi-file targets may write several files; the returned result is a per-client summary (SyncAll retains the per-fragment rows).
func (*Manager) SyncClientDetailed ¶
func (m *Manager) SyncClientDetailed(ctx context.Context, slug string, opts SyncOptions) ([]SyncResult, error)
SyncClientDetailed is SyncClient with per-fragment rows for multi-file targets (pack apply and CLI named-client sync with full honesty).
func (*Manager) Unsync ¶
Unsync removes one client's managed artifact and clears its lock entry. Files gridctl created are deleted outright; files the user owned lose only the managed region or shim line. Multi-file fragment projections are removed individually; unrecorded sibling files are never touched.
func (*Manager) UnsyncAll ¶
func (m *Manager) UnsyncAll(ctx context.Context) ([]UnsyncResult, error)
UnsyncAll removes every synced client's managed artifact. The loop keeps its historical fail-fast semantics: the first removal error aborts the pass without persisting the deletes made so far.
func (*Manager) UnsyncPackFragments ¶
func (m *Manager) UnsyncPackFragments(ctx context.Context, packName string) ([]UnsyncResult, []string, error)
UnsyncPackFragments removes every projected fragment file tagged with packName and returns the fragment names that lost their last projection of that pack (so the caller can decide whether to drop the store file).
type ScanEntry ¶
type ScanEntry struct {
Slug string `json:"slug"`
Name string `json:"name"`
Path string `json:"path"`
Exists bool `json:"exists"`
Size int64 `json:"size"`
}
ScanEntry reports what already exists at one client's likely global context location. Scanning never writes anything.
type Strategy ¶
type Strategy string
Strategy is how gridctl projects the canonical global context into one client's global context mechanism. Strategies are ordered by safety: a dedicated file gridctl fully owns, a single import line in a user-owned file, and a marker-delimited block in a shared file.
const ( // StrategyDedicatedFile writes a whole file gridctl owns inside a // rules directory the client reads (zero merge risk). StrategyDedicatedFile Strategy = "dedicated-file" // StrategyImportShim inserts one @-import line referencing the // canonical file; everything else in the target stays untouched. StrategyImportShim Strategy = "import-shim" // StrategyBlock writes the full file when the target is absent, or a // marker-delimited managed block when user content exists. StrategyBlock Strategy = "block" )
type SyncOptions ¶
type SyncOptions struct {
// Force overwrites drifted targets and repairs corrupt blocks.
Force bool
// DryRun renders and diffs without writing anything.
DryRun bool
// Pack tags recorded fragment projections with the applying pack.
// Empty keeps any existing tag.
Pack string
// PackRules limits the Pack tag to these fragment names. A pack apply
// projects the whole fragment set (composition is global), but must
// only claim ownership of the fragments it shipped; tagging a user
// fragment would let pack remove retract it.
PackRules []string
}
SyncOptions configure a sync pass.
type SyncResult ¶
type SyncResult struct {
Slug string `json:"slug"`
Name string `json:"name"`
Strategy string `json:"strategy"`
Mode string `json:"mode,omitempty"`
Fragment string `json:"fragment,omitempty"`
TargetPath string `json:"target_path"`
Action string `json:"action"`
// Detail carries honest render loss: frontmatter a client's dialect
// cannot express, named rather than silently dropped.
Detail string `json:"detail,omitempty"`
BackupPath string `json:"backup_path,omitempty"`
Diff string `json:"diff,omitempty"`
Error string `json:"error,omitempty"`
}
SyncResult describes what happened (or would happen) for one client. In multi-file mode there is one result per (client, fragment).
type Target ¶
type Target struct {
Slug string
Name string
Strategy Strategy
// Paths is the write target per OS. A missing key means the client
// has no known global context path on that platform.
Paths map[string]string
// ImportPaths is where a user's pre-existing hand-written global
// context most likely lives, for `ctx init --import`. Defaults to
// Paths when empty.
ImportPaths map[string]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.
DetectDirs []string
// Frontmatter is prepended verbatim to dedicated files for clients
// whose rules format requires it (e.g. VS Code *.instructions.md).
Frontmatter string
// MaxChars caps the rendered target file size; 0 means unlimited.
// Windsurf enforces a 6,000-character limit on global_rules.md.
MaxChars int
// 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 global context surface. Paths are ~-templates expanded against the Manager's home directory, keyed by GOOS ("darwin", "linux", "windows").
func FindTarget ¶
FindTarget returns the supported target for slug.
type UnsupportedClient ¶
UnsupportedClient is a linked client with no writable global context file mechanism. Surfaced honestly in status output instead of hacks.
func Unsupported ¶
func Unsupported() []UnsupportedClient
Unsupported returns linked clients with no file mechanism to sync to.
type UnsyncResult ¶
type UnsyncResult struct {
Slug string `json:"slug"`
TargetPath string `json:"target_path"`
Fragment string `json:"fragment,omitempty"`
// Action is "removed-file", "removed-region", or "already-gone".
Action string `json:"action"`
}
UnsyncResult describes the removal of one client's managed artifact.