Documentation
¶
Overview ¶
Package cobra is the docent cobra adapter. It is the only package in github.com/matcra587/docent that may import github.com/spf13/cobra — depguard enforces this boundary.
Because the package name collides with github.com/spf13/cobra, consumers import it under an alias:
import ( "github.com/spf13/cobra" "github.com/matcra587/docent" docentcobra "github.com/matcra587/docent/cobra" "github.com/matcra587/docent/harness" )
The two entry points are Tree, which walks a cobra.Command tree and returns the framework-neutral Command IR defined in the parent package, and NewCommand, which returns the mountable "agent" command group:
cfg := docent.Config{Guides: guides, Command: docentcobra.Tree(root)}
root.AddCommand(docentcobra.NewCommand(cfg))
Hosts that share a harness skills root can qualify names emitted by the two built-in skill formats at mount time. Omit the option to keep the default <slug>/SKILL.md names:
root.AddCommand(docentcobra.NewCommand(
cfg,
docentcobra.WithSkillNameQualifier("jira"),
))
NewGuideCommand additionally mounts the same guide browser as a first-class human command (e.g. top-level "app guide").
Consumer-aware rendering ¶
docent emits plain Markdown through Config.Out and carries no rendering dependency; styling belongs to the host. Because Markdown renderers such as charm.land/glamour/v2 style complete documents, the seam is buffer-then-render, not a streaming writer: agents and pipes get the raw bytes, and only an interactive human terminal gets the styled form.
interactive := term.IsTerminal(int(os.Stdout.Fd())) // golang.org/x/term
_, agentInvoked := harness.DetectAgent(os.LookupEnv) // docent/harness
var buf bytes.Buffer
out := io.Writer(os.Stdout) // agents and pipes: raw Markdown
if interactive && !agentInvoked {
out = &buf // humans: capture now, style after Execute
}
root.AddCommand(docentcobra.NewGuideCommand(docent.Config{Guides: guides, Out: out}))
err := root.Execute()
if out == &buf {
styled := buf.String() // fall back to the raw bytes if rendering fails
// charm.land/glamour/v2; WithEnvironmentConfig respects GLAMOUR_STYLE
if r, rerr := glamour.NewTermRenderer(glamour.WithEnvironmentConfig()); rerr == nil {
if s, serr := r.Render(buf.String()); serr == nil {
styled = s
}
}
fmt.Print(styled)
}
A runnable version of this dispatch lives in _examples/glamour-host, a nested module (in a directory all Go tooling ignores) so its rendering dependencies never enter docent's go.mod. Users without host styling can pipe the raw output through a Markdown pager instead: "app guide <slug> | glow -p -".
Index ¶
Examples ¶
Constants ¶
const AnnotationEnum = "docent.enum"
AnnotationEnum is the pflag annotation key hosts set to declare a flag's finite set of allowed values when the flag's value type does not implement an Enum() method. Each annotation entry is one allowed value:
_ = fs.SetAnnotation("output", docentcobra.AnnotationEnum, []string{"json", "text"})
const AnnotationReadOnly = "docent.readonly"
AnnotationReadOnly is the cobra command annotation hosts set to mark a command as performing no writes. Any non-empty value marks the command read-only in the schema IR:
cmd.Annotations = map[string]string{docentcobra.AnnotationReadOnly: "true"}
Variables ¶
var ( // ErrNoGuides is the sentinel agent export wraps when Config.Guides is // nil: exporting has nothing to render and, unlike the guide command's // index view, no meaningful empty output to emit. ErrNoGuides = errors.New("docent: no guides configured") // ErrUnsupportedFormat is the sentinel agent export wraps when --format // names no registered renderer. ErrUnsupportedFormat = errors.New("docent: unsupported export format") // ErrUnsupportedHarness is the sentinel agent export wraps when // --harness names no harness in harness.Supported. ErrUnsupportedHarness = errors.New("docent: unsupported agent harness") // ErrGuideNotFound is the sentinel the guide command wraps when a slug // lookup finds no guide in the set. Core's GuideSet.Get signals absence // with its comma-ok result and never returns this error. ErrGuideNotFound = errors.New("docent: guide not found") // ErrSectionNotFound is the sentinel the guide command wraps when // --section names no section in the guide. The core package never // returns it. ErrSectionNotFound = errors.New("docent: section not found") )
Sentinel errors for use with errors.Is. Flag-usage mistakes (conflicting flags, an empty --dir) stay plain errors — they are corrected at the command line, not branched on by hosts.
Functions ¶
func NewCommand ¶
NewCommand returns the mountable "agent" command group configured for the given host integration surface. The host adds it to their cobra root, importing this package under the conventional docentcobra alias (see the package documentation):
import docentcobra "github.com/matcra587/docent/cobra" root.AddCommand(docentcobra.NewCommand(cfg))
Optional integration behavior is controlled via Option values. For example, a host sharing a harness skills root qualifies only its exported built-in skills while mounting domain-specific agent commands:
root.AddCommand(docentcobra.NewCommand(
cfg,
docentcobra.WithSkillNameQualifier("jira"),
docentcobra.WithExtraCommands(adfMatrix, fieldTypes),
))
The agent command group provides agent-facing subcommands for guide retrieval, schema introspection, and skill export. It never modifies the host command tree.
Cobra runs the host's persistent pre-run hooks around these subcommands, and docent cannot prevent that: a root hook that resolves credentials kills the discovery surface on machines with none. Exempt the agent group (and any NewGuideCommand mount) from credential resolution — the standard requires the agent surface to work unauthenticated.
func NewGuideCommand ¶
NewGuideCommand returns a standalone guide browser command — index, single guide by slug, --section, --all, and slug completion, byte-identical to the "agent guide" subcommand NewCommand mounts. Cobra forbids mounting one *Command under two parents, so hosts that also want the guides as a first-class human command mount a second instance wherever it fits:
root.AddCommand(docentcobra.NewGuideCommand(cfg)) // "app guide <slug>"
Both surfaces serve the same guide set in the same canonical order, so the human and agent views cannot drift apart. Rendering stays the host's job through Config.Out — docent emits Markdown and never grows a rendering dependency; see the example for consumer-aware dispatch (agents and pipes get raw bytes, an interactive terminal gets the host's renderer).
Example ¶
ExampleNewGuideCommand mounts the guide browser as a first-class human command and dispatches rendering by consumer: an agent harness or a pipe gets raw Markdown bytes, and only an interactive human terminal gets the host's renderer. docent emits Markdown through Config.Out and never grows a rendering dependency — the renderer belongs to the host.
package main
import (
"fmt"
"io"
"os"
"strings"
"testing/fstest"
gocobra "github.com/spf13/cobra"
"github.com/matcra587/docent"
docentcobra "github.com/matcra587/docent/cobra"
)
func main() {
guides, err := docent.LoadGuides(fstest.MapFS{
"hello.md": {Data: []byte(strings.Join([]string{
"---",
"slug: hello",
"title: Hello Guide",
"description: Greets from the guide set.",
"when_to_use: When demonstrating the standalone mount.",
"commands: [app hello]",
"---",
"",
"## Decide",
"",
"## Run",
"app hello",
"",
"## Save",
"",
"## Preconditions",
"",
"## Recover",
"",
"## Next",
}, "\n"))},
})
if err != nil {
fmt.Println("load guides:", err)
return
}
// The dispatch: agents get clean bytes even inside a PTY, pipes get
// clean bytes, and only a human at a terminal gets styled output.
agentInvoked := os.Getenv("CLAUDECODE") != "" || os.Getenv("CODEX_THREAD_ID") != ""
interactive := false // e.g. golang.org/x/term: term.IsTerminal(int(os.Stdout.Fd()))
var out io.Writer = os.Stdout
if !agentInvoked && interactive {
// Wrap stdout with the host's Markdown renderer here — e.g. a
// glamour-backed writer — without docent knowing it exists.
out = os.Stdout
}
root := &gocobra.Command{Use: "app"}
root.AddCommand(docentcobra.NewGuideCommand(docent.Config{Guides: guides, Out: out}))
root.SetArgs([]string{"guide", "hello", "--section", "run"})
if err := root.Execute(); err != nil {
fmt.Println("execute:", err)
}
}
Output: app hello
Types ¶
type Option ¶
type Option func(*cmdOptions)
Option configures the behavior of NewCommand. Use the With* constructors to build options; the zero-argument call NewCommand(cfg) is always valid.
func WithExtraCommands ¶
WithExtraCommands adds host-supplied subcommands under the agent command group. Extra commands are mounted alongside the built-in guide, schema, and export subcommands. Hosts use this to expose domain-specific agent utilities — for example a Jira host might mount "adf-matrix" and "fieldtypes" commands that let agents discover ADF schema and issue field types without leaving the agent namespace. Passing zero commands is valid and has no effect; nil commands are ignored rather than mounted.
func WithExtraFormat ¶
WithExtraFormat registers a host-supplied export format under the given --format name. The renderer's artifacts are written through the same validated, root-scoped path handling as the built-in formats, and the name surfaces everywhere formats do: --format parsing, help text, error messages, and shell completion. Built-in names cannot be shadowed and the first registration of a name wins; an empty name or nil renderer is ignored rather than mounted, matching WithExtraCommands.
WithSkillNameQualifier applies only to the built-in agent-skill and claude-skill renderers. Extra formats receive each original Guide unchanged and own their naming and layout policy; configure or wrap r when an extra format should apply the same qualifier.
func WithSchemaTransform ¶
func WithSchemaTransform(t SchemaTransform) Option
WithSchemaTransform registers a rewrite of the agent schema command's output — the seam for hosts that wrap schema emission in their own envelope (status, metadata, schema-version fields) or re-encode it. It applies to the schema command only; the guide and export surfaces emit their documented shapes unchanged. Without a transform the schema JSON is written as-is with a trailing newline. Multiple transforms compose in registration order, each receiving the previous one's output; nil transforms are ignored rather than mounted, matching WithExtraCommands.
func WithSkillNameQualifier ¶ added in v0.4.0
WithSkillNameQualifier prefixes names emitted by the built-in agent-skill and claude-skill formats, separated from the source guide slug by a hyphen. For example, qualifier "jira" exports guide slug "core-contract" directly beneath the harness skills root as jira-core-contract/SKILL.md, with frontmatter name jira-core-contract.
Qualification is an export-only integration setting: it never mutates the source GuideSet or Guide.Slug, and guide lookup, indexes, and runbooks keep using the source slug. The final composed name is validated against the Agent Skills length, character, and hyphen constraints before any files are written. An empty qualifier preserves the unqualified, byte-identical historical output. When this option is supplied more than once, the last value wins.
Host-supplied formats registered through WithExtraFormat are not rewritten; they receive the original Guide and own their naming and layout policy.
type SchemaTransform ¶
SchemaTransform rewrites the agent schema command's output bytes before they are written. The input is the marshaled schema JSON (contract-version stamp included, no trailing newline); the returned bytes are emitted verbatim, so a transform owns the final shape — envelope, compaction, trailing newline. Returning an error fails the command with that error.