skills

package
v1.0.0-rc.2 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Index

Constants

View Source
const (
	ResourceKindSkill = "skill"
	ResourceKindAgent = "agent"
)

ResourceKindSkill and ResourceKindAgent restrict an import to one kind.

View Source
const ImportLockVersion = lockVersionPackSources

ImportLockVersion is the highest skills.lock.yaml schema version this gridctl reads. Version 1 added the version field itself and per-source agents; version 2 added per-source pack records; version 3 added pack variable declarations; version 4 added ssh-key auth fields (auth_method, ssh_user, ssh_key_path); version 5 added pack stack records and unresolved-detail maps; version 6 added local directory sources (kind: local and per-skill tree_hash); version 7 added pack external sources (LockedPack.Sources and LockedSource.PackMember). Files are written at the lowest version that can represent them (see WriteLockFile). A reader whose maximum is 4 understands SSH fields only and must refuse a file that carries a stack record or an unresolved-detail map rather than drop those keys on the next write. A reader whose maximum is 5 must refuse a file that carries a local source. A reader whose maximum is 6 must refuse a file that carries pack sources or a pack member marker.

View Source
const SourceKindLocal = "local"

SourceKindLocal is the origin and lockfile kind for a directory import.

Variables

View Source
var ErrAmbiguousOwner = errors.New("ambiguous duplicate owners")

ErrAmbiguousOwner is returned when more than one lock source owns a name. Local update reports it before any mutation.

View Source
var ErrImportLockBusy = errors.New("timeout acquiring import lock")

ErrImportLockBusy signals contention on the cross-process import lock: the operation should be retried, nothing is corrupted.

View Source
var ErrInsideGridctlHome = fmt.Errorf("refusing to import from inside the gridctl home")

ErrInsideGridctlHome is returned when a local source resolves inside the gridctl home. The home holds the registry, pack checkouts, the clone cache, and projection backups.

View Source
var ErrNewerImportLockVersion = errors.New("import lockfile was written by a newer gridctl version")

ErrNewerImportLockVersion signals a skills.lock.yaml written by a newer gridctl. Callers must never paper over it: acting on state a newer version wrote risks silent data loss (the pkg/project lesson).

View Source
var ErrSourceConflict = errors.New("source name conflict")

ErrSourceConflict is a source-name or source-kind collision. REST maps it to 409. The error text names the existing source and, for a directory collision, tells the caller to pass --source-name.

Functions

func AgentDir

func AgentDir(registryDir, name string) string

AgentDir returns the canonical directory for one imported agent; the definition itself lives at AgentDir(...)/AGENT.md.

func AgentsRoot

func AgentsRoot(registryDir string) string

AgentsRoot returns the canonical agent store directory under a registry directory.

func AllowOverwrite

func AllowOverwrite(lf *LockFile, kind, name, sourceName string, selected, explicitForce, agent bool) bool

AllowOverwrite reports whether an existing resource may be replaced. Local re-imports of an unowned name, or a name owned only by sourceName, do not need --force. A local owner always requires explicit force from a git or pack caller. Git-only selected overwrite is unchanged.

func BackupSkillFileInDir

func BackupSkillFileInDir(skillDir, shortSHA string) (string, error)

BackupSkillFileInDir is BackupSkillFile for callers that already hold the skill directory (skill projection adopt shares the convention, so there is exactly one hand-edit backup vocabulary).

func BehavioralChanges

func BehavioralChanges(old, new *Fingerprint) []string

BehavioralChanges compares two fingerprints and returns human-readable changes.

func BuildAuther

func BuildAuther(cfg AuthConfig) (gitpkg.Auther, error)

BuildAuther constructs a git.Auther matching the AuthConfig's Method. Returns an error for unknown methods. Individual Auther implementations also validate their own inputs (e.g. HTTPSTokenAuth rejects empty tokens).

func CheckUpdatesBackground

func CheckUpdatesBackground(registryDir string, logger *slog.Logger)

CheckUpdatesBackground runs update checks in a background goroutine. Results are written to the cache file for display on next CLI command.

func CheckedLocations

func CheckedLocations(client, home string) []string

CheckedLocations lists the path templates a client scan visits.

func CombineTrackedSourceHash

func CombineTrackedSourceHash(skills map[string]LockedSkill, agents map[string]LockedAgent) string

CombineTrackedSourceHash hashes tracked skill tree hashes and agent content hashes. Records are domain-separated and length-delimited so a skill and an agent cannot alias. Order is kind, then name.

func ContentHashFile

func ContentHashFile(path string) (string, error)

ContentHashFile computes a SHA-256 hash of a file.

func DeleteAgent

func DeleteAgent(registryDir, name string) error

DeleteAgent removes one agent from the canonical store.

func DeleteOrigin

func DeleteOrigin(skillDir string) error

DeleteOrigin removes the .origin.json file from a skill directory.

func DetectAgentDrift

func DetectAgentDrift(ctx context.Context, registryDir string) ([]string, error)

DetectAgentDrift returns the names of imported agents whose on-disk AGENT.md has been edited since the last import. Same fail-open policy as DetectDrift: a missing origin or InstalledHash is not drift.

func DetectDrift

func DetectDrift(ctx context.Context, store *registry.Store, lockPath, sourceName string) ([]string, error)

DetectDrift returns the names of imported skills in sourceName whose on-disk SKILL.md has been edited since the last import or sync. Drift is detected by comparing the current file hash against the InstalledHash snapshot written when the skill was last installed.

Skills imported before InstalledHash was tracked (empty value) are treated as not drifted — DetectDrift fails open rather than reporting noise. Skills with no Origin (purely local) are not considered.

Pass an empty sourceName to scan every imported skill in the registry.

func FetchAndCompare

func FetchAndCompare(repo, ref, currentSHA string, auth AuthConfig, logger *slog.Logger) (string, bool, error)

FetchAndCompare fetches the latest from a remote and compares with current.

func FormatFindings

func FormatFindings(findings []SecurityFinding) string

FormatFindings returns a human-readable summary of security findings.

func FormatUpdateNotice

func FormatUpdateNotice() string

FormatUpdateNotice returns a user-friendly message about available updates.

func GuardPackOwnership

func GuardPackOwnership(lf *LockFile, sourceName string, result *CloneResult, opts ImportOptions) error

GuardPackOwnership refuses a pack import that would replace a local-owned resource. Packs have no force option, so this is an error before any write.

func GuardSourceKey

func GuardSourceKey(lf *LockFile, name string, kind, repo string) error

GuardSourceKey refuses to replace an existing local source key with a different root or source kind. Git-to-git reuse of a non-member key is unchanged. A pack member key cannot be pointed at a different repository by any caller; the same repository under the same key stays allowed so pack add can re-pin and skill update can refresh.

func HasOrigin

func HasOrigin(skillDir string) bool

HasOrigin checks if a skill directory has an .origin.json file.

func InsideGridctlHome

func InsideGridctlHome(path string) (bool, error)

InsideGridctlHome reports whether path resolves to state.BaseDir or a path beneath it. A missing gridctl home is not an error.

func IsPinnedRef

func IsPinnedRef(ref string) bool

IsPinnedRef returns true when ref looks like an immutable pin (a specific version tag containing a ".", or a full 40-character commit SHA). Bare branch names and empty refs return false so they are treated as floating.

This is a heuristic, not a guarantee: a tag like "release-2026" with no dot will read as unpinned, and a branch named "feature.x" will read as pinned. Used by aggregate sync to skip pins by default so a bulk operation does not silently bump a user's intentionally-fixed version.

func IsSemVerConstraint

func IsSemVerConstraint(ref string) bool

IsSemVerConstraint returns true if the ref looks like a semver constraint.

func KnownAgentRoots

func KnownAgentRoots(home string) []resolvedLocation

KnownAgentRoots returns resolved agent-location directories from the table.

func KnownSkillRoots

func KnownSkillRoots(home string) []resolvedLocation

KnownSkillRoots returns resolved skill-location directories from the table.

func KnownSourceName

func KnownSourceName(home, root string) string

KnownSourceName returns the location table's source name when root is a known skill or agent directory. A direct child does not match.

func ListRemoteTags

func ListRemoteTags(repoPath string) ([]string, error)

ListRemoteTags returns all tags from a cached repository.

func LocalSourceHasUpdate

func LocalSourceHasUpdate(ctx context.Context, src LockedSource) (bool, error)

LocalSourceHasUpdate recomputes hashes for tracked entries only. A missing entry counts as changed. Unrelated siblings are ignored. A missing root or a root that now resolves inside the gridctl home is an error.

func LockFilePath

func LockFilePath() string

LockFilePath returns the default path to skills.lock.yaml.

func MutateLockFile

func MutateLockFile(ctx context.Context, path string, fn func(*LockFile) (bool, error)) error

MutateLockFile runs one read-modify-write cycle over skills.lock.yaml while holding the cross-process import lock, so concurrent operations serialize instead of losing each other's updates. fn returns whether its changes should be written; false skips the write and succeeds.

func ProjectionHint

func ProjectionHint(client string) string

ProjectionHint is the single post-import warning for a colliding client.

func ProjectionHintClients

func ProjectionHintClients(ctx context.Context, home string, roots []string) ([]string, error)

ProjectionHintClients returns the client slugs whose projection target equals root, is the parent of root, or whose recorded project.Entry.Path equals root. A missing project lock is not an error. A newer project lock is returned as-is.

func RecordLocalSource

func RecordLocalSource(lf *LockFile, sourceName, repo string, fetchedAt time.Time, installedSkills map[string]LockedSkill, installedAgents map[string]LockedAgent, discovered *CloneResult, canDrop bool) error

RecordLocalSource merges installed entries into the previous local maps. Absent entries are dropped only when canDrop is set. The source hash is recomputed from the merged maps.

func ReleaseInstalledNames

func ReleaseInstalledNames(lf *LockFile, destination string, destLocal bool, skills, agents map[string]struct{})

ReleaseInstalledNames removes installed names from other sources when a local source is involved. Unrelated skills, agents, and pack metadata stay. An emptied source is deleted only when no tracked content or pack record remains. Git-only owners are left in place.

func RepoToName

func RepoToName(repo string) string

RepoToName extracts a short name from a repo URL (the last path segment with any ".git" suffix stripped). Exported so callers like the CLI can match the source names this package uses without duplicating the logic.

func ResolveLocalRoot

func ResolveLocalRoot(path string) (string, error)

ResolveLocalRoot returns the absolute, symlink-resolved directory.

func ResolveSemVerConstraint

func ResolveSemVerConstraint(constraintStr string, tags []string) (string, error)

ResolveSemVerConstraint finds the best matching tag for a constraint.

func ResolveSourceName

func ResolveSourceName(lf *LockFile, in SourceNameInput) (string, error)

ResolveSourceName picks a stable lockfile key. An existing local source with the same resolved root is reused. An explicit name that differs from that key is an error. Client import suffixes a colliding basename with the first eight hex characters of sha256(root); skill add fails instead.

A pack member key is "<pack>/<source>". RepoToName returns a basename and cannot produce a slash, so a plain skill add without --source-name never derives that key. GuardSourceKey is what refuses an explicit --source-name that would re-key a member onto another repository.

func SafeRepoPath

func SafeRepoPath(path string) error

SafeRepoPath validates a path component to prevent directory traversal.

func ShortSHA

func ShortSHA(sha string) string

ShortSHA returns the first 8 characters of a commit SHA, or the whole string when it is shorter (including the empty string from an uncached fetch). It keeps SHA formatting panic-free for logs, backup names, and messages.

func ShouldCheckUpdates

func ShouldCheckUpdates() bool

ShouldCheckUpdates returns false if update checks are disabled. GRIDCTL_NO_SKILL_UPDATE_CHECK accepts the shared boolean vocabulary (env.Bool); a malformed value is ignored and checks proceed.

func SkillTreeHash

func SkillTreeHash(ctx context.Context, skillDir string) (string, error)

SkillTreeHash returns a sha256:-prefixed hash of SKILL.md and allowlisted supporting files, sorted by relative path. Modification times are not hashed. .origin.json, backups, and install temporary files are outside the allowlist and are excluded.

func SkillsConfigPath

func SkillsConfigPath() string

SkillsConfigPath returns the default path to skills.yaml.

func SupportedImportClients

func SupportedImportClients() []string

SupportedImportClients lists the v1 client slugs in the error-message order.

func UpdateCachePath

func UpdateCachePath() string

UpdateCachePath returns the path to the cached update status file.

func ValidateAgentName

func ValidateAgentName(name string) error

ValidateAgentName validates an agent name: non-empty, lowercase letters, digits, and hyphens only.

func WriteLockFile

func WriteLockFile(path string, lf *LockFile) error

WriteLockFile writes skills.lock.yaml atomically. Keys are sorted for minimal merge conflicts.

func WriteOrigin

func WriteOrigin(skillDir string, origin *Origin) error

WriteOrigin writes the .origin.json file to a skill directory.

func WriteUpdateCache

func WriteUpdateCache(status *UpdateStatus) error

WriteUpdateCache writes the update status to the default cache path.

func WriteUpdateCacheAt

func WriteUpdateCacheAt(path string, status *UpdateStatus) error

WriteUpdateCacheAt writes the update status to an explicit path.

Types

type AgentDefinition

type AgentDefinition struct {
	Name        string
	Description string
	// Extra holds every frontmatter key other than name and description,
	// in document order, as raw YAML nodes.
	Extra []AgentExtraField
	// Body is the markdown after the frontmatter block.
	Body string
	// Raw is the file exactly as read; imports write it verbatim.
	Raw []byte
}

AgentDefinition is a parsed Claude Code subagent definition (AGENT.md / agents/<name>.md). Name and Description are the only typed fields; every other frontmatter key (tools, model, hooks, mcpServers, permissionMode, vendor extensions) rides in Extra as raw YAML nodes in document order.

AgentDefinition deliberately does not reuse AgentSkill/ParseSkillMD: that parser types skill-specific keys and would silently strand tools and model in an untyped map, and its render path normalizes frontmatter. Agents are stored and projected verbatim (identity render), so Raw is the source of truth; the parsed form exists for validation, scanning, and listing.

func ParseAgentMD

func ParseAgentMD(data []byte) (*AgentDefinition, error)

ParseAgentMD parses an agent definition file. Frontmatter with a non-empty description is required: a markdown file without it (a README dropped into an agents/ directory, say) is not an agent definition. The frontmatter mapping is walked by hand so unknown keys are preserved in order rather than silently dropped, and a duplicate key cannot discard the valid ones.

func SaveAgent

func SaveAgent(registryDir, name string, raw []byte) (*AgentDefinition, error)

SaveAgent validates raw as an agent definition named name and writes it into the canonical store byte-for-byte. No normalization happens here on purpose: identity projections copy the canonical bytes verbatim, so any rewrite (key reordering, trailing-newline fixes) would surface as drift on every synced client after an edit. Returns the parsed definition so callers can inspect what was written.

func (*AgentDefinition) DeclaredModel

func (d *AgentDefinition) DeclaredModel() (string, bool)

DeclaredModel reads the definition's top-level `model:` value. ok is false when the declaration exists but is not a scalar (a structured value cannot be honored or safely rewritten); an absent key reports ("", true). Every surface that answers "what model does this agent declare" (sync, CLI, REST) goes through this one helper so they can never disagree on non-scalar handling.

func (*AgentDefinition) ExtraByKey

func (d *AgentDefinition) ExtraByKey(key string) (*yaml.Node, bool)

ExtraByKey returns the raw frontmatter node for one passthrough key. Renderers use it for key-level access without scanning Extra at every call site.

type AgentExtraField

type AgentExtraField struct {
	Key   string
	Value *yaml.Node
}

AgentExtraField is one passthrough frontmatter key.

type AuthConfig

type AuthConfig struct {
	Method         string // "", "none", "token", "ssh-agent", "ssh-key"
	Token          string // resolved plaintext — transient, never persisted
	CredentialRef  string // e.g. "${vault:GIT_TOKEN}" — persisted
	SSHUser        string // defaults to "git" when empty
	SSHKeyPath     string // required for method "ssh-key"
	SSHPassphrase  string // transient
	KnownHostsPath string // reserved for future host-key policy work
}

AuthConfig carries authentication configuration for a git operation. The Token and SSHPassphrase fields are transient — they must never be persisted to disk. CredentialRef is the opaque reference string (e.g. "${vault:GIT_TOKEN}") that gets stored in Origin/LockFile so that Update can re-resolve it later.

func ResolveStoredAuth

func ResolveStoredAuth(stored StoredAuth, resolver CredentialResolver) (AuthConfig, error)

ResolveStoredAuth rebuilds an AuthConfig from a stored record. A CredentialRef wins and is resolved to a token. Otherwise an ssh-key path is rebuilt and the passphrase is re-read from the environment. Anything else is ambient (the zero value). The passphrase is never taken from the record, because it is never stored.

type ClientCandidate

type ClientCandidate struct {
	Kind       string
	Name       string
	Location   string
	Source     string
	Root       string
	RelPath    string
	Resolved   string
	Client     string
	ProvPath   string
	SkipReason string
	KnownName  string
}

ClientCandidate is one skill or agent visible in a client location.

func EnumerateClient

func EnumerateClient(ctx context.Context, client, home string) ([]ClientCandidate, error)

EnumerateClient lists home-scoped skill and agent candidates for client. Permission errors are returned. A missing location is skipped. A missing project lock is not an error; a newer project lock is.

type ClientImportEntry

type ClientImportEntry struct {
	Candidate ClientCandidate
	Action    string
	Reason    string
}

ClientImportEntry is one enumerated row after selection is applied.

type ClientImportOptions

type ClientImportOptions struct {
	Trust      bool
	Force      bool
	NoActivate bool
	DryRun     bool
}

ClientImportOptions controls one client-import apply.

type ClientImportResult

type ClientImportResult struct {
	Entries  []ClientImportEntry
	Warnings []string
	Roots    []string
}

ClientImportResult is the outcome of applying a selection.

func ApplyClientImport

func ApplyClientImport(ctx context.Context, imp *Importer, opts ClientImportOptions, selected []ClientCandidate) (*ClientImportResult, error)

ApplyClientImport imports selected candidates, batched by resolved root and kind. Dry-run does not write. Policy skips stay skips.

type ClientLocation

type ClientLocation struct {
	Client     string
	Owner      string
	SourceName string
	Kind       string
	Path       string
	Dialect    string
	Docs       string
	SkipReason string
}

ClientLocation is one home-scoped skill or agent directory a client scans. Owner is the provenance slug. SourceName is the lockfile key. They are recorded separately and are equal for every v1 row.

func ClientLocations

func ClientLocations(client string) ([]ClientLocation, bool)

ClientLocations returns the v1 home-scoped locations for client, in display order. The second result is false for an unknown client.

Citations:

type CloneResult

type CloneResult struct {
	RepoPath  string
	CommitSHA string
	Skills    []DiscoveredSkill
	Malformed []MalformedSkill
	// Agents are agent definitions discovered under the agents/*.md
	// convention; MalformedAgents records files in agents/ directories
	// that are not parseable agent definitions.
	Agents          []DiscoveredAgent
	MalformedAgents []MalformedAgent
	// FilteredSkills are relative skill paths DiscoverLocal skipped because
	// they carry .origin.json. Git discovery leaves this nil.
	FilteredSkills []string
}

CloneResult contains the result of a clone + discovery operation.

func CloneAndDiscover

func CloneAndDiscover(repo, ref, subPath string, auth AuthConfig, logger *slog.Logger) (*CloneResult, error)

CloneAndDiscover clones a repo and discovers all SKILL.md files plus any agents/*.md definitions it ships.

type CredentialResolver

type CredentialResolver func(ref string) (string, error)

CredentialResolver resolves an opaque reference like "${vault:GIT_TOKEN}" to its raw value. Callers (CLI, HTTP API) register one via Importer.SetCredentialResolver so that Update can re-resolve credentials recorded in Origin/LockFile without the importer needing to know where the values live.

type DiffResult

type DiffResult struct {
	Skill    string `json:"skill"`
	Local    string `json:"local"`    // current on-disk full SKILL.md text
	Upstream string `json:"upstream"` // content an update would install
	Drifted  bool   `json:"drifted"`  // on-disk file diverges from InstalledHash
}

DiffResult holds a skill's current on-disk SKILL.md alongside the content an update would install, for on-demand comparison. Producing it changes no registry state, SHAs, or InstalledHashes.

type DiscoveredAgent

type DiscoveredAgent struct {
	Name        string
	Path        string // Relative path from repo root to the .md file
	Definition  *AgentDefinition
	ContentHash string
}

DiscoveredAgent represents an agent definition found in a cloned repo under an agents/ directory.

type DiscoveredSkill

type DiscoveredSkill struct {
	Name        string
	Path        string // Relative path from repo root to SKILL.md directory
	Skill       *registry.AgentSkill
	ContentHash string
}

DiscoveredSkill represents a SKILL.md found in a cloned repo.

type EntryProvenance

type EntryProvenance struct {
	Client   string
	Location string
}

EntryProvenance is the client and location written onto one origin.

type Fingerprint

type Fingerprint struct {
	ContentHash string   `json:"contentHash" yaml:"content_hash"`
	ToolsHash   string   `json:"toolsHash" yaml:"tools_hash"`
	Tools       []string `json:"tools,omitempty" yaml:"tools,omitempty"`
}

Fingerprint captures the behavioral identity of a skill.

func ComputeFingerprint

func ComputeFingerprint(skill *registry.AgentSkill) *Fingerprint

ComputeFingerprint generates a behavioral fingerprint for a skill.

type ImportOptions

type ImportOptions struct {
	Repo       string
	Ref        string
	Path       string
	Trust      bool     // Skip security scan confirmation
	NoActivate bool     // Import as draft instead of active
	Force      bool     // Overwrite existing skills
	Rename     string   // Rename the skill on import
	Selected   []string // Only import skills with these names (empty = import all)
	// SelectedAgents imports exactly these agent names. When empty, the
	// legacy behavior holds: all agents when Selected is also empty, no
	// agents when a skill selection is present (the web UI picker's
	// contract). Pack imports always pass fully resolved lists.
	SelectedAgents []string
	// Discovered supplies a pre-cloned discovery result so callers that
	// already ran CloneAndDiscover (pack add reads the manifest first)
	// do not clone twice. Nil means Import clones itself.
	Discovered *CloneResult
	Auth       AuthConfig
	// PreserveState carries over the existing skill's State (draft/active/
	// disabled) instead of resetting it. Used by Update so that re-syncing
	// a source does not silently re-activate skills the user disabled.
	PreserveState bool
	// Kind is SourceKindLocal for a directory import. Empty keeps git import.
	Kind string
	// SourceName overrides RepoToName. Required when a derived name collides.
	SourceName string
	// KnownName is the client-location source name for a new local root.
	KnownName string
	// Provenance maps a discovered relative path to client import metadata.
	// skill add leaves it nil.
	Provenance map[string]EntryProvenance
	// ResourceKind, when set to skill or agent, imports only that kind.
	ResourceKind string
	// PackImport fails before any registry write when a local source key or
	// a local-owned resource would be replaced. Packs have no force option.
	PackImport bool
	// ExactSelection makes Selected and SelectedAgents authoritative. An
	// empty Selected imports no skills and an empty SelectedAgents imports
	// no agents, regardless of ResourceKind and of the legacy empty-means-all
	// rules. Unset preserves those rules.
	ExactSelection bool
	// SelectionIsImportAll marks a non-empty Selected list that still means
	// import-all. An existing skill is skipped instead of overwritten.
	// ExactSelection still decides which names import. Pack add sets this
	// for the pack repository when skills: is empty, so declaring sources
	// does not change that import-all.
	SelectionIsImportAll bool
}

ImportOptions controls the import behavior.

type ImportResult

type ImportResult struct {
	Imported []ImportedSkill `json:"imported"`
	Skipped  []SkippedSkill  `json:"skipped"`
	Warnings []string        `json:"warnings"`
	// ImportedAgents and SkippedAgents record agent definitions the same
	// import discovered under the agents/*.md convention.
	ImportedAgents []ImportedAgent `json:"importedAgents,omitempty"`
	SkippedAgents  []SkippedAgent  `json:"skippedAgents,omitempty"`
}

ImportResult contains the results of an import operation.

type ImportedAgent

type ImportedAgent struct {
	Name     string            `json:"name"`
	Path     string            `json:"path"`
	Origin   *Origin           `json:"origin,omitempty"`
	Findings []SecurityFinding `json:"findings,omitempty"`
}

ImportedAgent records a successfully imported agent definition.

type ImportedSkill

type ImportedSkill struct {
	Name   string  `json:"name"`
	Path   string  `json:"path"`
	Origin *Origin `json:"origin,omitempty"`
	// FilesCopied counts supporting files installed alongside SKILL.md
	// (scripts/, references/, assets/, and package metadata).
	FilesCopied int               `json:"filesCopied"`
	Findings    []SecurityFinding `json:"findings,omitempty"`
}

ImportedSkill records a successfully imported skill.

type Importer

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

Importer orchestrates the skill import process.

func NewImporter

func NewImporter(store *registry.Store, registryDir, lockPath string, logger *slog.Logger) *Importer

NewImporter creates a new skill importer.

func (*Importer) AdvanceTracking

func (imp *Importer) AdvanceTracking(ctx context.Context, skillName, newSHA string) error

AdvanceTracking records that a skill has been reconciled against upstream commit newSHA without changing its on-disk content. It advances only the version-tracking metadata — the skill's origin CommitSHA and the lock-file source's CommitSHA/ContentHash/FetchedAt — leaving the SKILL.md file and its InstalledHash untouched.

Used when a sync skips a locally-edited (drifted) skill: the reviewed upstream version is recorded so it no longer surfaces as an available update, while the user's local edits (and the drift signal that DetectDrift derives from InstalledHash) are preserved.

func (*Importer) AgentInfo

func (imp *Importer) AgentInfo(agentName string) (*SkillInfo, error)

AgentInfo returns details about an imported agent's origin.

func (*Importer) BackupSkillFile

func (imp *Importer) BackupSkillFile(ctx context.Context, skillName, shortSHA string) (string, error)

BackupSkillFile copies a skill's current SKILL.md to SKILL.md.pre-<shortSHA> next to it before an overwrite, so a forced update of a locally-edited skill stays recoverable. It returns the backup file name (relative to the skill directory). A missing SKILL.md is a no-op that returns an empty name.

func (*Importer) Detach

func (imp *Importer) Detach(ctx context.Context, skillName string) error

Detach makes an imported skill local-only by removing its origin sidecar and its lock-file entry. The SKILL.md and the skill itself remain; it simply no longer tracks an upstream source and will not be touched by sync.

func (*Importer) Diff

func (imp *Importer) Diff(ctx context.Context, skillName string) (*DiffResult, error)

Diff fetches the latest upstream SKILL.md for an imported skill and returns both the current on-disk content and the content an update would install, without writing anything to the registry or changing any SHAs/InstalledHash. It is on-demand only — the caller pays for one git fetch.

func (*Importer) Import

func (imp *Importer) Import(ctx context.Context, opts ImportOptions) (*ImportResult, error)

Import clones a repo, discovers skills and agents, validates, scans, and imports. A local directory import sets Kind to SourceKindLocal and skips git. ctx is checked before any registry write and between entries.

func (*Importer) Info

func (imp *Importer) Info(skillName string) (*SkillInfo, error)

Info returns details about a skill's origin and update status.

func (*Importer) Pin

func (imp *Importer) Pin(skillName, ref string) error

Pin updates a skill's ref and disables auto-update.

func (*Importer) Remove

func (imp *Importer) Remove(skillName string) error

Remove removes an imported skill and cleans up origin and lock entries.

func (*Importer) RemoveAgent

func (imp *Importer) RemoveAgent(agentName string) error

RemoveAgent removes an imported agent and cleans up origin and lock entries.

func (*Importer) SetCredentialResolver

func (imp *Importer) SetCredentialResolver(r CredentialResolver)

SetCredentialResolver registers a resolver used to expand CredentialRef values stored in Origin/LockFile when Update fetches the latest state. Without a resolver, Update can still run for sources that have no stored reference (ambient GITHUB_TOKEN / public repos), but will fail fast for sources that do.

func (*Importer) Update

func (imp *Importer) Update(ctx context.Context, skillName string, dryRun, force, trust bool) (*ImportResult, error)

Update fetches latest for a skill and applies changes.

trust forwards to ImportOptions.Trust. It defaults to false at every caller: a sync that surfaces new security findings is skipped with the finding text rather than applied silently. Previously this was hardcoded true, which meant every sync refreshed upstream content with the scan gate disabled, harmless while only the SKILL.md body was scanned, but not once supporting files are installed too.

type InstalledAgent

type InstalledAgent struct {
	Name       string
	Definition *AgentDefinition
	// Dir is the agent's canonical directory (holds AGENT.md and its
	// .origin.json sidecar).
	Dir string
}

InstalledAgent is one agent present in the canonical store.

func GetAgent

func GetAgent(registryDir, name string) (*InstalledAgent, error)

GetAgent returns one agent from the canonical store.

func ListAgents

func ListAgents(registryDir string) ([]InstalledAgent, error)

ListAgents returns the agents in the canonical store, sorted by name. A missing store directory is the normal no-agents state. Entries whose AGENT.md is missing or unparseable are skipped: the store lists what it can serve.

type LocalDiscovery

type LocalDiscovery struct {
	Result   *CloneResult
	Warnings []string
}

LocalDiscovery is the result of walking a local directory for skills and agents. Result.RepoPath is the resolved root. CommitSHA is empty.

func DiscoverLocal

func DiscoverLocal(ctx context.Context, root, subpath string, logger *slog.Logger) (*LocalDiscovery, error)

DiscoverLocal walks a resolved directory root for SKILL.md trees and agents/*.md files. subpath, when set, narrows the walk to that relative entry; the recorded paths stay relative to root. The walkers do not follow symlinks. Managed copies (a skill directory carrying .origin.json) are removed with a warning. Skill names are rewritten to the directory name.

type LockFile

type LockFile struct {
	Version int                     `yaml:"version"`
	Sources map[string]LockedSource `yaml:"sources"`
}

LockFile represents skills.lock.yaml — pins exact versions of imported skills.

func ReadLockFile

func ReadLockFile(path string) (*LockFile, error)

ReadLockFile reads and parses skills.lock.yaml. Version-less files (written before the schema carried a version) migrate to the current version on read; files from a newer gridctl are rejected with ErrNewerImportLockVersion.

func (*LockFile) FindAgentSource

func (lf *LockFile) FindAgentSource(agentName string) (string, *LockedSource, bool)

FindAgentSource finds the source name for a given agent.

func (*LockFile) FindPackSource

func (lf *LockFile) FindPackSource(packName string) (string, *LockedSource, bool)

FindPackSource finds the source carrying a pack by pack name.

func (*LockFile) FindSkillSource

func (lf *LockFile) FindSkillSource(skillName string) (string, *LockedSource, bool)

FindSkillSource finds the source name for a given skill.

func (*LockFile) MemberSources

func (lf *LockFile) MemberSources(packName string) []string

MemberSources returns the lock source keys imported as members of packName, sorted by key. A member source has Pack == nil, so FindPackSource does not return it.

func (*LockFile) RemoveAgent

func (lf *LockFile) RemoveAgent(agentName string)

RemoveAgent removes a single agent from the lock file, cleaning up the source when neither skills nor agents remain under it.

func (*LockFile) RemoveSkill

func (lf *LockFile) RemoveSkill(skillName string)

RemoveSkill removes a single skill from the lock file, cleaning up the source when neither skills nor agents remain under it.

func (*LockFile) RemoveSource

func (lf *LockFile) RemoveSource(name string)

RemoveSource removes a source from the lock file.

func (*LockFile) SetSource

func (lf *LockFile) SetSource(name string, src LockedSource)

SetSource updates or adds a source in the lock file.

type LockedAgent

type LockedAgent struct {
	Path        string `yaml:"path"`
	ContentHash string `yaml:"content_hash"`
}

LockedAgent records per-agent metadata within a source.

type LockedPack

type LockedPack struct {
	Name    string `yaml:"name"`
	Version string `yaml:"version,omitempty"`
	// Description and Author persist the manifest metadata a list view
	// needs, so no consumer ever has to re-clone the repo to show it.
	// Additive fields: files without them keep loading, and version 2
	// (which every pack record already stamps) covers them.
	Description string   `yaml:"description,omitempty"`
	Author      string   `yaml:"author,omitempty"`
	Wiring      bool     `yaml:"wiring,omitempty"`
	Clients     []string `yaml:"clients,omitempty"`
	Skills      []string `yaml:"skills,omitempty"`
	Agents      []string `yaml:"agents,omitempty"`
	// Rules lists context rule fragments imported from the pack repo.
	// Superseded by RuleFiles, which adds per-rule provenance; retained so
	// lockfiles written before that keep loading, and kept in sync on write
	// so a downgrade still sees the selection.
	Rules []string `yaml:"rules,omitempty"`
	// RuleFiles records per-rule provenance keyed by fragment name. An entry
	// with an empty ContentHash means provenance is unknown (migrated from a
	// Rules-only lockfile), and callers must fall back to byte comparison
	// rather than treating the empty hash as a match.
	RuleFiles map[string]LockedRule `yaml:"rule_files,omitempty"`
	// Unresolved lists manifest-selected names discovery could not find,
	// so status can keep reporting them until the upstream repo (or the
	// manifest) is fixed.
	Unresolved []string                             `yaml:"unresolved,omitempty"`
	Variables  map[string]LockedVariableDeclaration `yaml:"variables,omitempty"`
	// Stack records a pinned checkout of a pack-carried stack. Nil when
	// the manifest has no stack, or the stack did not resolve at import.
	Stack *LockedStack `yaml:"stack,omitempty"`
	// UnresolvedDetails maps an unresolved token (for example
	// "stack:stack.yaml") to the reason it did not resolve, so status can
	// show the export or escape error without re-reading the repository.
	// A non-empty map stamps lockVersionStack. An older reader has no
	// field for the key and would drop it on the next write.
	UnresolvedDetails map[string]string `yaml:"unresolved_details,omitempty"`
	// Sources records each external git source pinned at import. A
	// non-empty map stamps lockVersionPackSources. An older reader has no
	// field for the key and would drop it on the next write.
	Sources map[string]LockedPackSource `yaml:"sources,omitempty"`
}

LockedPack is the recorded state of an imported pack: the manifest identity plus the selection as resolved against discovery at import time (never the empty-means-all shorthand).

type LockedPackSource

type LockedPackSource struct {
	// Repo is the authored URL, stored as written, matching the primary
	// pack source. Printed lines and source summaries redact userinfo.
	Repo      string    `yaml:"repo"`
	Ref       string    `yaml:"ref,omitempty"`
	Path      string    `yaml:"path,omitempty"`
	CommitSHA string    `yaml:"commit_sha,omitempty"`
	FetchedAt time.Time `yaml:"fetched_at,omitempty"`
	// SourceKey is the member lock source key (<pack>/<source>).
	SourceKey string   `yaml:"source_key,omitempty"`
	Skills    []string `yaml:"skills,omitempty"`
	Agents    []string `yaml:"agents,omitempty"`
}

LockedPackSource is one external repository pinned by a pack import. Skills and Agents are the names that actually imported, not the manifest shorthand and not skipped or unresolved selections.

type LockedRule

type LockedRule struct {
	Path        string `yaml:"path"`
	ContentHash string `yaml:"content_hash"`
}

LockedRule records per-rule-fragment metadata within a pack source. The content hash is what lets a later install tell an upstream change apart from a local edit; without it the only available comparison is raw bytes against disk, which conflates the two.

type LockedSkill

type LockedSkill struct {
	Path        string `yaml:"path"`
	ContentHash string `yaml:"content_hash"`
	// TreeHash is the sha256:-prefixed hash of SKILL.md plus allowlisted
	// supporting files. Set only for local sources.
	TreeHash    string       `yaml:"tree_hash,omitempty"`
	Fingerprint *Fingerprint `yaml:"fingerprint,omitempty"`
}

LockedSkill records per-skill metadata within a source.

type LockedSource

type LockedSource struct {
	Repo string `yaml:"repo"`
	// Kind is "local" for a directory source and empty for a git source.
	Kind        string                 `yaml:"kind,omitempty"`
	Ref         string                 `yaml:"ref"`
	ResolvedRef string                 `yaml:"resolved_ref,omitempty"`
	CommitSHA   string                 `yaml:"commit_sha"`
	FetchedAt   time.Time              `yaml:"fetched_at"`
	ContentHash string                 `yaml:"content_hash"`
	Skills      map[string]LockedSkill `yaml:"skills"`
	// Agents records agent definitions imported from this source.
	Agents map[string]LockedAgent `yaml:"agents,omitempty"`
	// CredentialRef is an opaque reference like "${vault:GIT_TOKEN}" used to
	// re-resolve credentials on source update. Raw tokens are never stored.
	CredentialRef string `yaml:"credential_ref,omitempty"`
	// AuthMethod, SSHUser, and SSHKeyPath persist ssh-key authentication.
	// Only the path is stored, never key material or a passphrase.
	AuthMethod string `yaml:"auth_method,omitempty"`
	SSHUser    string `yaml:"ssh_user,omitempty"`
	SSHKeyPath string `yaml:"ssh_key_path,omitempty"`
	// Pack records the pack manifest this source was imported through,
	// with its resolved selection. Nil for plain skill/agent sources.
	// A member source keeps Pack nil so FindPackSource never treats it
	// as the pack record.
	Pack *LockedPack `yaml:"pack,omitempty"`
	// PackMember is the pack name when this source was imported as a
	// named external source of that pack. Empty for every other source.
	// <pack>/<source> cannot collide with RepoToName output, which is a
	// basename and never contains a slash. GuardSourceKey is what stops
	// an explicit --source-name from re-keying the member to another repo.
	PackMember string `yaml:"pack_member,omitempty"`
}

LockedSource records the resolved state of a skill source.

func (LockedSource) IsLocal

func (s LockedSource) IsLocal() bool

IsLocal reports whether this source is a local directory import.

func (LockedSource) StoredAuth

func (s LockedSource) StoredAuth() StoredAuth

StoredAuth returns the authentication this source recorded at import.

type LockedStack

type LockedStack struct {
	Path        string `yaml:"path"`
	Name        string `yaml:"name"`
	ContentHash string `yaml:"content_hash"`
	CheckoutDir string `yaml:"checkout_dir"`
}

LockedStack is the pinned checkout of a pack-carried stack file.

type LockedVariableDeclaration

type LockedVariableDeclaration struct {
	Required    *bool  `yaml:"required,omitempty"`
	Secret      *bool  `yaml:"secret,omitempty"`
	Type        string `yaml:"type,omitempty"`
	Description string `yaml:"description,omitempty"`
	Docs        string `yaml:"docs,omitempty"`
}

LockedVariableDeclaration persists one value-free pack prerequisite.

type MalformedAgent

type MalformedAgent = MalformedSkill

MalformedAgent aliases MalformedSkill so agent call sites read as what they are; the shape and JSON encoding are identical.

type MalformedSkill

type MalformedSkill struct {
	Path string `json:"path"` // Relative path from repo root
	Err  string `json:"error"`
}

MalformedSkill records a SKILL.md that could not be read or parsed (or a directory that could not be walked), so callers can surface the failure instead of silently dropping it.

type Origin

type Origin struct {
	Repo        string    `json:"repo"`
	Ref         string    `json:"ref"`
	Path        string    `json:"path,omitempty"`
	CommitSHA   string    `json:"commitSha"`
	ImportedAt  time.Time `json:"importedAt"`
	ContentHash string    `json:"contentHash"`
	// InstalledHash is the SHA-256 of the SKILL.md as written to disk
	// immediately after the last import. DetectDrift compares the current
	// on-disk hash against this to surface local user edits. Distinct from
	// ContentHash, which records the upstream file as fetched (and which
	// diverges from the installed file because of frontmatter render
	// normalization and state injection).
	InstalledHash string       `json:"installedHash,omitempty"`
	Fingerprint   *Fingerprint `json:"fingerprint,omitempty"`
	// SupportingFilesInstalled records that the importer evaluated and
	// installed the complete allowlisted package tree, even when it was empty.
	// Its absence identifies imports created before supporting-file installs.
	SupportingFilesInstalled bool `json:"supportingFilesInstalled,omitempty"`
	// CredentialRef is an opaque reference like "${vault:GIT_TOKEN}" used to
	// re-resolve credentials on skill update. Raw token values are never
	// persisted, only the reference string.
	CredentialRef string `json:"credentialRef,omitempty"`
	// AuthMethod, SSHUser, and SSHKeyPath persist ssh-key authentication so a
	// later update can rebuild it. Only the path is stored, never key material
	// or a passphrase.
	AuthMethod string `json:"authMethod,omitempty"`
	SSHUser    string `json:"sshUser,omitempty"`
	SSHKeyPath string `json:"sshKeyPath,omitempty"`
	// Kind is "local" for a directory import and empty for a git import.
	// Empty is the historical zero value so existing sidecars keep decoding
	// as git origins.
	Kind string `json:"kind,omitempty"`
	// Client and Location are optional client-import provenance. skill add
	// does not write them and does not clear a pair that still matches.
	Client   string `json:"client,omitempty"`
	Location string `json:"location,omitempty"`
}

Origin tracks the remote source of an imported skill. Stored as .origin.json alongside the SKILL.md file.

func ReadOrigin

func ReadOrigin(skillDir string) (*Origin, error)

ReadOrigin reads the .origin.json file from a skill directory.

func (*Origin) IsLocal

func (o *Origin) IsLocal() bool

IsLocal reports whether this origin tracks a local directory.

func (*Origin) StoredAuth

func (o *Origin) StoredAuth() StoredAuth

StoredAuth returns the authentication this origin recorded at import.

type ScanResult

type ScanResult struct {
	SkillName string            `json:"skillName"`
	Findings  []SecurityFinding `json:"findings"`
	Safe      bool              `json:"safe"`
}

ScanResult contains the security scan results for a skill.

func ScanAgent

func ScanAgent(def *AgentDefinition) *ScanResult

ScanAgent checks an agent definition for dangerous patterns in its body and frontmatter values. Like skill bodies (and unlike supporting files), any finding blocks: an agent definition is instructions the client executes with tool access.

func ScanFragment

func ScanFragment(name string, content []byte) *ScanResult

ScanFragment checks a context rule fragment for dangerous patterns. Rule fragments are instructions clients inject into every session, so they get the same blocking scan agents do: the whole file (frontmatter and body) is scanned, and any finding blocks without --trust.

func ScanSkill

func ScanSkill(sk *registry.AgentSkill) *ScanResult

ScanSkill checks a skill for dangerous patterns in its body.

type SecurityFinding

type SecurityFinding struct {
	StepID      string `json:"stepId"`
	Pattern     string `json:"pattern"`
	Description string `json:"description"`
	Severity    string `json:"severity"` // "warning" or "danger"
}

SecurityFinding represents a potentially dangerous pattern found in a skill.

func ScanSkillTree

func ScanSkillTree(sk *registry.AgentSkill, srcDir string) (findings []SecurityFinding, blocking bool)

ScanSkillTree runs the same security gate an import applies to one discovered skill: the SKILL.md body (any finding blocks) plus the supporting-file tree (danger-severity findings block; lower severities surface without blocking). srcDir is the skill's directory in the clone. Callers that refuse before importing (the pack REST surface) use this so their refusal covers exactly what the importer would skip.

type SkillDefaults

type SkillDefaults struct {
	AutoUpdate     bool   `yaml:"auto_update" json:"autoUpdate"`
	UpdateInterval string `yaml:"update_interval" json:"updateInterval"`
}

SkillDefaults defines global defaults for skill sources.

type SkillInfo

type SkillInfo struct {
	Name        string    `json:"name"`
	Origin      *Origin   `json:"origin,omitempty"`
	IsRemote    bool      `json:"isRemote"`
	UpdateAvail bool      `json:"updateAvailable"`
	LatestSHA   string    `json:"latestSha,omitempty"`
	LastChecked time.Time `json:"lastChecked,omitempty"`
}

SkillInfo returns details about an imported skill.

type SkillSource

type SkillSource struct {
	Name           string      `yaml:"name" json:"name"`
	Repo           string      `yaml:"repo" json:"repo"`
	Ref            string      `yaml:"ref,omitempty" json:"ref,omitempty"`
	Path           string      `yaml:"path,omitempty" json:"path,omitempty"`
	AutoUpdate     *bool       `yaml:"auto_update,omitempty" json:"autoUpdate,omitempty"`
	UpdateInterval string      `yaml:"update_interval,omitempty" json:"updateInterval,omitempty"`
	Auth           *SourceAuth `yaml:"auth,omitempty" json:"auth,omitempty"`
}

SkillSource defines a remote skill source in skills.yaml.

type SkillUpdate

type SkillUpdate struct {
	CurrentSHA string `yaml:"current_sha"`
	LatestSHA  string `yaml:"latest_sha"`
	Repo       string `yaml:"repo"`
	Ref        string `yaml:"ref"`
}

SkillUpdate describes an available update for a skill.

type SkillsConfig

type SkillsConfig struct {
	Defaults SkillDefaults `yaml:"defaults,omitempty" json:"defaults,omitempty"`
	Sources  []SkillSource `yaml:"sources" json:"sources"`
}

SkillsConfig represents the skills.yaml file.

func DefaultSkillsConfig

func DefaultSkillsConfig() *SkillsConfig

DefaultSkillsConfig returns a config with sensible defaults.

func LoadSkillsConfig

func LoadSkillsConfig(path string) (*SkillsConfig, error)

LoadSkillsConfig reads and parses a skills.yaml file.

func (*SkillsConfig) EffectiveAutoUpdate

func (c *SkillsConfig) EffectiveAutoUpdate(src *SkillSource) bool

EffectiveAutoUpdate returns the auto_update setting for a source, falling back to the global default.

func (*SkillsConfig) EffectiveUpdateInterval

func (c *SkillsConfig) EffectiveUpdateInterval(src *SkillSource) time.Duration

EffectiveUpdateInterval returns the update_interval for a source, falling back to the global default.

type SkippedAgent

type SkippedAgent = SkippedSkill

SkippedAgent aliases SkippedSkill so agent call sites read as what they are; the shape and JSON encoding are identical.

type SkippedSkill

type SkippedSkill struct {
	Name   string `json:"name"`
	Reason string `json:"reason"`
}

SkippedSkill records a skill (or agent) that was skipped during import.

type SnapshotResult

type SnapshotResult struct {
	// CheckoutDir is the absolute directory the copy landed in.
	CheckoutDir string
	// Reused is true when an existing directory for this destination was
	// returned without copying.
	Reused bool
}

SnapshotResult is a materialized pack checkout.

func SnapshotWorktree

func SnapshotWorktree(ctx context.Context, repoPath, destDir string) (SnapshotResult, error)

SnapshotWorktree copies repoPath, excluding .git, into destDir. The copy skips symlinks, refuses any path that resolves outside the clone root, and fails closed on the repository caps. An existing destDir is reused. The copy holds the cross-process flock on repoPath+".flock" that cloneShallow also acquires, so a cache update cannot rewrite the source mid-copy. A failure leaves no partial destDir.

type SourceAuth

type SourceAuth struct {
	Method        string `yaml:"method,omitempty" json:"method,omitempty"`
	CredentialRef string `yaml:"credential_ref,omitempty" json:"credentialRef,omitempty"`
	SSHUser       string `yaml:"ssh_user,omitempty" json:"sshUser,omitempty"`
	SSHKeyPath    string `yaml:"ssh_key_path,omitempty" json:"sshKeyPath,omitempty"`
}

SourceAuth is the declarative auth block on a skills.yaml source. Raw tokens must NOT appear here — use CredentialRef (e.g. "${vault:GIT_TOKEN}") which is resolved against the live vault at clone/fetch time.

func (*SourceAuth) ToAuthConfig

func (a *SourceAuth) ToAuthConfig() AuthConfig

ToAuthConfig converts the declarative block into a runtime AuthConfig. CredentialRef is copied through unchanged; callers are responsible for resolving it to a raw Token before invoking the importer.

type SourceConflictError

type SourceConflictError struct {
	Name string
	Repo string
	Msg  string
}

SourceConflictError is a source-key collision with an existing record.

func (*SourceConflictError) Error

func (e *SourceConflictError) Error() string

func (*SourceConflictError) Unwrap

func (e *SourceConflictError) Unwrap() error

type SourceNameInput

type SourceNameInput struct {
	Kind         string
	Root         string
	Explicit     string
	KnownName    string
	ClientImport bool
}

SourceNameInput selects the lockfile key for one import.

type StoredAuth

type StoredAuth struct {
	Method        string
	SSHUser       string
	SSHKeyPath    string
	CredentialRef string
}

StoredAuth is the non-secret authentication recorded for a source. Token values and passphrases are never part of it.

type UpdateStatus

type UpdateStatus struct {
	CheckedAt time.Time              `yaml:"checked_at"`
	Updates   map[string]SkillUpdate `yaml:"updates,omitempty"`
	Errors    []string               `yaml:"errors,omitempty"`
}

UpdateStatus records the result of a background update check.

func ReadUpdateCache

func ReadUpdateCache() (*UpdateStatus, error)

ReadUpdateCache reads the cached update status from the default path.

func ReadUpdateCacheAt

func ReadUpdateCacheAt(path string) (*UpdateStatus, error)

ReadUpdateCacheAt reads the cached update status from an explicit path. Returns (nil, nil) when the file does not exist so callers can fail open.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL