Documentation
¶
Overview ¶
Package candywalk is the SHARED entity-discovery walk over a charly repo: the superproject plus every box/<distro> submodule, reading every candy/ + box/ charly.yml and returning one Entity per top-level node. It is the ONE discovery abstraction (R3) both out-of-process generator plugins use — candy/plugin-docs (the docs site generator) and candy/plugin-marketplace (the harness/marketplace generator) — so the two never fork their own directory conventions.
Discovery is PURE FILESYSTEM walking + os.ReadFile — it never shells out to git, never runs charly commands. "Defined, not default-active": it returns every DEFINED entity (what an author wrote down), never the resolved/enabled set (the resolver's surfaces are wrong for a catalog — `charly box list boxes` resolves through main's import: closure and omits every debian.* and ubuntu.* box). Each repo (the superproject + each checked-out box/<distro> submodule) is walked as its own root and the results unioned; an uninitialised submodule (no charly.yml) is skipped.
The candy de-submodule cutover (Phases 1-2) moved every candy to a standalone kind-prefixed repo (layer-*, pod-*, vm-*, plugin-*). Phase 4 deletes the in-repo candy/ dirs, after which the local FS walk alone finds only the residual fixtures. CollectEntitiesRemote extends discovery with a REMOTE-REF-AWARE mode: it also collects every @github.com/opencharly/... ref from the walked files' require:/candy: lists, resolves each repo+tag via a CALLER-SUPPLIED resolver seam (out-of-process generators use spec/refs.DownloadRepo; an in-proc caller could supply the loader's EnsureRepoDownloaded), and walks each fetched repo's candy dir (or repo-root charly.yml for config candies) — fix-pointing until no new refs surface, the same transitive closure the runtime scan materialises. The seam keeps this kit import-free: candywalk itself still carries NO charly or sdk import — only the stdlib + yaml.v3.
The package carries only the CONTRACT module + the pure kit (github.com/opencharly/spec for ParseRemoteRef (F4.3), github.com/opencharly/sdk/kit for RepoLayoutDirs (F4.4)) — never charly, never a mechanism kit with registry/host coupling. It returns the raw per-node kind discriminator + value node; the caller owns the projection into its own types (the docs plugin decodes `candy:` into its candyView/boxView; the marketplace plugin decodes `skill:`/`hook:`/ `marketplace:` into the generated spec types).
Index ¶
Constants ¶
const ( // AgentsSkillsDir is the Agent-Skills standard location: what a candy's `skills:` list means // when it declares none. AgentsSkillsDir = ".agents/skills" )
skillfile.go — the `.agents/skills` FILE source: the ONE reader of a skill's prose.
A skill's body lives in the repository that owns it, not in its `skill:` entity:
<tree>/<name>/SKILL.md frontmatter (name + description) + the markdown body <tree>/<name>/references/<stem>.md no frontmatter, by contract
where <tree> is the tree the owning candy's `skills:` list declares (default AgentsSkillsDir) and <name> is the skill id: the DIRECTORY name, which the frontmatter `name` must agree with. The entity keeps the routing (family/owner/triggers/category); the file keeps the prose. The version of record is the github ref that fetched the repo — the marketplace refs list's pin, or a consumer's `candy:` list — so nothing here fetches anything.
The shape read here is BOTH the marketplace corpus's (banner included) and the bare shape a repo authors after extraction: the generator's DO-NOT-EDIT marker is stripped either way. That is what makes the relocation's round trip exact — `extract` writes the bare file, the corpus emitter re-writes it with the banner, and the two read back to the same Content.
It lives in the shared kit, not in either generator, because BOTH need it (candy/plugin-marketplace and candy/plugin-docs) and R2 allows one implementation of one read. The docs projection's readSkill — fence split, two-key frontmatter, marker strip, stem-sorted references — is this function's prior art; it converges on this one.
const UnifiedFileName = "charly.yml"
UnifiedFileName is the one entity filename (the project rulebook's "one filename charly.yml").
Variables ¶
This section is empty.
Functions ¶
func ReadSkillTree ¶ added in v0.2026288.1246
ReadSkillTree reads every skill one tree declares: every <treeDir>/<name>/SKILL.md, keyed by directory name. A directory without an entry file is not a skill and is skipped; a tree that does not exist at all is (empty, nil), because a repo that has not moved yet is a normal state and not an error.
Types ¶
type Entity ¶
type Entity struct {
Name string // the top-level node name (as authored)
Namespace string // "" for the superproject, else the box/<distro> submodule name
Dir string // directory holding this charly.yml, relative to the repo root
Kind string // the node's kind discriminator ("candy", "skill", "hook", "marketplace", …)
Value yaml.Node // the kind value body
SourceRoot string // absolute repo root ("" = local root; else the fetched remote cache dir)
}
Entity is one top-level node of a discovered charly.yml, regardless of kind. The value node is the node's kind value (the body under `<name>: {<kind>: …}`).
Value is a VALUE yaml.Node, not a pointer: yaml.v3's decode into `map[string]map[string]*yaml.Node` silently drops nested structs when the caller then Decodes the leaf (a plugin-fleet candy's `plugin:` block came back nil), while the value-node decode round-trips faithfully. The value-node form is what the original plugin-docs walk used.
SourceRoot is the ABSOLUTE path of the repo root this entity was read from: "" for a local superproject/box root (the caller knows the local root it passed in), or the fetched repo cache dir for a REMOTE entity from CollectEntitiesRemote. Schema reads and other per-entity file lookups must resolve relative to SourceRoot (when non-empty) rather than the local project root.
func CollectEntities ¶
CollectEntities reads every entity node across every repo root, for the two discovery directories the loader uses (candy/ and box/). Discovery is by directory convention (candy/<name>/charly.yml, box/<name>/charly.yml) — the same two the loader uses — so the result is exactly what an author has written down.
func CollectEntitiesRemote ¶ added in v0.2026237.1646
func CollectEntitiesRemote(roots []Root, resolve RemoteResolver) ([]Entity, error)
CollectEntitiesRemote extends CollectEntities with the candy de-submodule cutover's remote-ref-aware discovery (Phase 3): every @github.com/opencharly/... ref found in the walked files' require:/candy: lists is resolved through the caller-supplied seam and the fetched repo's own candy entities (candy/<name>/charly.yml or the repo-root charly.yml for config candies) are unioned into the result, TRANSITIVELY — a fetched repo's own require:/candy: lists may surface new refs, which are fetched in turn, until closure (the same fix-point the runtime scan materialises). Entity.SourceRoot carries the fetched repo dir for remote entities so schema reads resolve against the fetched tree, not the local project root.
func ReadEntityFile ¶
ReadEntityFile decodes one charly.yml into its top-level nodes. The file is a map of entity NAME to a single kind key (name-first shape); each (name, kind, value) triple is one Entity. A charly.yml that does not fit the name-first shape (a multi-doc bed, a template) is skipped rather than failing the whole discovery. Scalar top-level keys (the standalone repos' prepended `version: <calver>` schema header) are tolerated and ignored — they are not entities.
type RemoteResolver ¶ added in v0.2026237.1646
RemoteResolver resolves one remote candy ref to a LOCALLY FETCHED repo directory. It is the caller-supplied seam that keeps this kit import-free: an out-of-process generator supplies spec/refs.DownloadRepo (or a wrapper), an in-proc caller supplies the loader's EnsureRepoDownloaded. The returned dir must hold the fetched repo export (the same shape the runtime's repo cache produces) so the walker can read its candy/ + root charly.yml.
type Root ¶
type Root struct {
Namespace string // "" for the superproject, else the box/<distro> submodule name
Dir string // absolute path
}
Root is one project tree to walk: the superproject, or a box/<distro> submodule.
type SkillFile ¶ added in v0.2026288.1246
type SkillFile struct {
// Name is the resolved skill id — the directory name, which the frontmatter `name` must agree
// with when it states one.
Name string
// Dir is the directory the file was read from (equal to Name; kept separate so a caller that
// reports a mismatch can name both sides).
Dir string
// Description is the frontmatter `description`: the Skill-tool dispatch keyword, and the public
// subtitle when the corpus page is rendered.
Description string
// Content is the markdown body VERBATIM — marker stripped, and deliberately NOT trimmed. The
// corpus emitter writes TrimSuffix(content, "\n") + "\n", so returning the body as authored is
// exactly what makes read -> emit byte-identical.
Content string
// References are the skill's split files, stem-sorted so every consumer's output is deterministic.
References []SkillReferenceFile
}
SkillFile is the prose half of a `skill:` entity, read from its file.
func ReadSkillFile ¶ added in v0.2026288.1246
ReadSkillFile reads ONE skill's files: <treeDir>/<name>/SKILL.md, plus every <treeDir>/<name>/references/<stem>.md beside it.
A missing entry file is (nil, nil), not an error — whether a skill MUST have one is the caller's call: the marketplace generator hard-errors when an entity carries neither a body nor a file, because publishing nothing there would silently drop a published skill. Everything else is an error, because each case would otherwise publish the wrong skill under the right name: an unreadable file, malformed frontmatter, or a frontmatter `name` that disagrees with its directory.
type SkillReferenceFile ¶ added in v0.2026288.1246
type SkillReferenceFile struct {
Stem string // the file stem: references/<stem>.md
Content string // the file's markdown verbatim, marker stripped
}
SkillReferenceFile is one `references/<stem>.md` split file of a skill.