Documentation
¶
Overview ¶
Package fleet is the declarative registry of the things an agentic host runs with: tools, models, and agents.
tool an agentic CLI harness (claude, codex, opencode, aider, agy)
model an inference backend (a subscription seat, a metered API, a
pooled local model)
agent a tool bound to a model — written tool:model — under one or
more nicknames
A bare tool is not an agent and a bare model is not an agent: an agent always names both. Roles are an orthogonal axis and do not belong to the binding.
Nicknames ¶
An agent's identity is its tool:model binding; its names are aliases. `007` and `smarty` may both name claude:fable. Any number of nicknames collapse to one capability-matrix row, because that matrix is keyed by the binding, never by the nickname.
Two identities, and they answer different questions ¶
The paragraph above is about CAPABILITY: what an agent can do is its binding, so aliasing never fragments the matrix.
At RUNTIME the identity is the agent's NAME, and that is a different question with a different answer. A running agent has one conversation store, one kb attribution, one bus cursor and one API key, and all four hang off its name. So a name is a singleton on a host: `bashy chat --agent X` while X is live is refused, not duplicated. Handing one identity two live tasks mixes their context, and an agent answering about one task from the other's history is confidently, plausibly wrong — the failure mode that is worse than a crash because nothing reports it.
The two views meet at the useful conclusion: two NAMES on one BINDING are one capability and two identities. That is a legal, ordinary thing to write here — claimName only guards names — and it is the answer to "can I run two of these at once". You do not run one agent twice; you give the second one a name. `bashy agents clone` is that write, plus the parent's context as of now (see CloneAgent), plus provenance.
An agent minted for a single task carries Ephemeral, which keeps it out of the roster and marks it for removal when its task closes.
Rings, and where the truth lives ¶
Entries are merged over pkg/assetring's rings — embedded baseline, shared catalog dirs, an optional org overlay, and the host-local store, in that precedence order. Every local entry is one file whose bytes are exactly the Content blob an org catalog would serve, so a definition round-trips in both directions without a transform.
The package is standalone-first and effect-free: it reads and writes YAML and probes the host, but it never spawns an agent. Launching is the launcher's job; fleet only says how.
Index ¶
- Constants
- func BandLabel(band int) string
- func BandLabelWithSource(band int, source string) string
- func CloudCacheRoot(root string) string
- func CompareVersions(a, b string) int
- func DefaultRoot() string
- func DetectTool() (string, bool)
- func ExitCode(err error) int
- func Main(cmd *cobra.Command, args []string)
- func MarkerEnvs() []string
- func Marshal(v any) ([]byte, error)
- func NewAgentsCmd(opts ...Option) *cobra.Command
- func NewModelsCmd(opts ...Option) *cobra.Command
- func NewToolsCmd(opts ...Option) *cobra.Command
- func NounDir(root, noun string) string
- func Probes(cache spacetime.Cache) *spacetime.ProbeSet
- func ResolveLaunchModel(tool, launchModelDisplay string) (band int, canonical string)
- type Agent
- type AgentFile
- type AgentInstruction
- type AgentLedger
- type AgentRole
- type AliasCollision
- type Catalog
- func (c *Catalog) Agent(name string) (Agent, bool)
- func (c *Catalog) Agents() ([]Agent, []error)
- func (c *Catalog) Binding(nick string) (Agent, Tool, Model, error)
- func (c *Catalog) CheckAliases() []AliasCollision
- func (c *Catalog) CloneAgent(parentName, newName string, ephemeral bool, task string) (Agent, error)
- func (c *Catalog) DetectTool() (string, bool)
- func (c *Catalog) Host(name string) (Host, bool)
- func (c *Catalog) Hosts() ([]Host, []error)
- func (c *Catalog) LiveProbeAgent(ctx context.Context, name string, timeout time.Duration) (Check, bool)
- func (c *Catalog) MaterializeAgent(name string) (string, error)
- func (c *Catalog) MaterializeModel(name string) (string, error)
- func (c *Catalog) MaterializeTool(name string) (string, error)
- func (c *Catalog) Model(name string) (Model, bool)
- func (c *Catalog) Models() ([]Model, []error)
- func (c *Catalog) People() ([]Person, []error)
- func (c *Catalog) Person(name string) (Person, bool)
- func (c *Catalog) RemoveAgent(name string) error
- func (c *Catalog) RemoveHost(name string) error
- func (c *Catalog) RemoveModel(name string) error
- func (c *Catalog) RemovePerson(name string) error
- func (c *Catalog) RemoveTool(name string) error
- func (c *Catalog) ResolveLaunchModel(tool, launchModelDisplay string) (band int, canonical string)
- func (c *Catalog) Root() string
- func (c *Catalog) SaveAgent(a Agent) error
- func (c *Catalog) SaveHost(h Host) error
- func (c *Catalog) SaveModel(m Model) error
- func (c *Catalog) SavePerson(p Person) error
- func (c *Catalog) SaveTool(t Tool) error
- func (c *Catalog) SmokeArgv(tool string) ([]string, bool)
- func (c *Catalog) Tool(name string) (Tool, bool)
- func (c *Catalog) Tools(all bool) ([]Tool, []error)
- func (c *Catalog) VerifyAgent(name string, ps *spacetime.ProbeSet) Check
- func (c *Catalog) VerifyModel(name string, _ *spacetime.ProbeSet) Check
- func (c *Catalog) VerifyTool(name string, ps *spacetime.ProbeSet) Check
- type Check
- type CloudClient
- type CloudConfig
- type Config
- type ContextCloner
- type EventsDone
- type EventsOutcome
- type Host
- type LiveProbe
- type Model
- type ModelHost
- type Option
- type Person
- type SyncResult
- type Tool
- func (t Tool) Argv(modelID, prompt string) []string
- func (t Tool) ArgvPrefix(modelID string) ([]string, bool)
- func (t Tool) ArgvPrefixWithWorkspace(workspace, modelID string) ([]string, bool)
- func (t Tool) ArgvWithWorkspace(workspace, modelID, prompt string) []string
- func (t Tool) Binary() string
- func (t Tool) CanFork() bool
- func (t Tool) CredentialRefFor(m Model) string
- func (t Tool) CurrentSession() string
- func (t Tool) EventsArgv(path string) []string
- func (t Tool) EventsOnStdout() bool
- func (t Tool) EventsStdoutArgv() []string
- func (t Tool) ForkArgv(modelID, session, prompt string) []string
- func (t Tool) ForkArgvPrefix(modelID, session string) ([]string, bool)
- func (t Tool) ForkArgvPrefixWithWorkspace(workspace, modelID, session string) ([]string, bool)
- func (t Tool) ForkArgvWithWorkspace(workspace, modelID, session, prompt string) []string
- func (t Tool) HasEventsArg() bool
- func (t Tool) IsCLI() bool
- func (t Tool) ModelFlag() string
- func (t Tool) Names() []string
- func (t Tool) ReportsTurnEnd() bool
- func (t Tool) SteerArgvPrefix(modelID string) ([]string, bool)
- func (t Tool) SteerArgvPrefixWithWorkspace(workspace, modelID string) ([]string, bool)
- func (t Tool) SteerTakesPrompt() bool
- func (t Tool) StreamsEvents() bool
- func (t Tool) TakesModel() bool
- func (t Tool) VersionProbeArgv() []string
- func (t Tool) WorkspacePreflightArgv(workspace, modelID, prompt string) ([]string, bool)
- type ToolCLI
- type ToolLaunch
- type ToolVersion
- type ToolWatchdog
- type Verdict
Constants ¶
const ( // BandDeclared — a considered guess from vendor tier and priors. Nothing has // tested it. Shown with a `~`. BandDeclared = "declared" // BandOperator — pegged from an operator's lived experience across real runs. // Not a controlled experiment, but evidence from work that actually shipped, // which beats a prior. This is what corrected Gemini Pro and DeepSeek Pro from // L3 to L2: their VENDOR's top tier is not this fleet's L3. BandOperator = "operator" // BandMeasured — earned by running the model up a difficulty ladder to the // rung where it FAILED. The only thing a band really means. BandMeasured = "measured" // BandCascade — not a single model's band at all: a COMPOSITE agent that // SERVES at the numeric band by escalation. A cheap base does the work and // escalates to premium help (a model ladder) only when stuck. Rendered `X4`, // not `L4`, so it never reads like one frontier model — it is a cascade that // reaches L4 when it must and runs cheap the rest of the time. BandCascade = "cascade" )
How a band came to be believed, weakest evidence first.
The distinction is load-bearing, because this fleet has already spent months trusting numbers that nothing had ever checked. A band with no evidence behind it must not READ like one that has some.
const ( KindTool = "tool" KindModel = "model" KindAgent = "agent" KindPerson = "person" KindHost = "host" )
Entry kinds — the noun a name resolves to.
const ( ToolKindCLI = "cli" ToolKindFunc = "func" ToolKindWeb = "web" ToolKindSystem = "system" // ToolCredentialModelProvider means the harness calls the bound model's // provider API directly and therefore needs that provider's credential in // its child environment. Subscription-native CLIs such as claude and codex // leave this empty because they authenticate through their own login. ToolCredentialModelProvider = "model-provider" )
Tool kind discriminators. The cloudbox Tool registry is shared between MCP-style function kits and agentic CLI harnesses; only ToolKindCLI is a fleet tool. The others are recognized so they can be skipped by name rather than silently mis-parsed.
const ( ModelKindSubscription = "subscription" // interactive login; the CLI authenticates on the host ModelKindAPI = "api" // an API key, named by APIKeyRef ModelKindLocal = "local" // no credential; pooled local inference via the outpost )
Model kind — HOW YOU AUTHENTICATE. Nothing else.
This used to be "the access/billing discriminator" — one field naming two things — and it worked only because the two axes happened to travel together in every model we had: a seat plan authenticated by an interactive login, an API authenticated by a key and billed per token.
z.ai's GLM Coding Plan is the case that separates them: FLAT-RATE BILLING OVER AN API KEY. Economically a subscription, operationally a plain HTTP call. There is no value of a single enum that can say that.
The tell was already in verify.go, which had to describe BOTH axes in every message ("metered api; bills against the vault key", "subscription seat; the CLI authenticates interactively"). A field whose every description needs two clauses is two fields.
A fourth value (`api-subscription`) would have worked for GLM and then grown as the PRODUCT of the axes — a metered vendor CLI needs a fifth, a flat-rate local pool a sixth. Same mistake in a new costume. So: Kind is auth, Billing is billing, and each names one thing.
const ( BillingMetered = "metered" // per token, always. The next token costs money. BillingFlat = "flat" // a prepaid seat with a HARD quota. Exhausted -> blocked until it resets. BillingFree = "free" // your own hardware. No bill at all. // BillingFlatThenMetered is a prepaid seat that FALLS BACK TO PER-TOKEN BILLING once // the quota is gone, instead of blocking. Anthropic Max/Pro and Codex work this way. // // At the margin, below quota, it prices exactly like `flat`. The difference is not // the price — it is that overrunning does not fail, it BILLS. Routing treats it as // flat; `models verify` says the quiet part out loud. BillingFlatThenMetered = "flat_then_metered" )
Model billing — HOW YOU PAY. Orthogonal to Kind.
Optional. When absent it is DERIVED from Kind by Model.BillingMode(), which reproduces the old collapsed behaviour exactly — so every model written before this field existed keeps its meaning, and there is no migration. The values differ in WHAT HAPPENS WHEN THE QUOTA RUNS OUT, and that is the part that matters most — because the two failure modes are opposites:
flat -> the agent STOPS WORKING. A reliability event. Loud. flat_then_metered -> the agent KEEPS WORKING AND STARTS CHARGING YOU. A cost event. SILENT.
The second is the dangerous one. An unattended fleet run that exhausts a subscription seat does not fail — it quietly moves onto pay-as-you-go and you find out on the invoice. That is why it is a first-class value and not a footnote on `flat`.
const ( ModelSourceCloud = "cloud" ModelSourceLocal = "local" )
Model source — where the row came from, not how it is billed.
const ( PromptToken = "{prompt}" ModelToken = "{model}" WorkspaceToken = "{workspace}" SessionToken = "{session}" // the current session id, for a context-inheriting fork )
PromptToken, ModelToken, WorkspaceToken and SessionToken are the launch-template placeholders.
const ( FlatPlanDiscountNum = 1 FlatPlanDiscountDen = 2 )
FlatPlanDiscount — what a prepaid seat is worth at the margin, as a fraction of its list price. Half: real, but nowhere near free, because the quota is finite.
const MaxBand = 4
MaxBand is the top capability band. Four is a deliberate ceiling, not a placeholder: bands exist to make a coarse routing decision cheap ("who is worth seating?"), and a ladder fine enough to argue about would just be the quality score with extra steps.
const SmokeToken = "SMOKE-OK"
SmokeToken is the exact stdout token a live fleet probe requires. Exit status is deliberately not evidence: harnesses can report provider errors and still exit zero.
Variables ¶
This section is empty.
Functions ¶
func BandLabel ¶
BandLabel renders a band for humans. An unpegged model shows as "-" rather than "L0", because 0 is not a band — it is a model nobody has placed yet, and it should look unanswered, not weak.
func BandLabelWithSource ¶
BandLabelWithSource marks an unmeasured band with a `~`, and a composite cascade band with an `X` prefix (X4 = "serves L4 by escalation", vs L4 = a single model pegged there).
A band is the highest rung a model CLEARS, and until something has watched a model fail, it has not been placed — it has been guessed at. The tilde is one character and it is the difference between a fact and an opinion.
func CloudCacheRoot ¶
CloudCacheRoot is where a pulled overlay lives.
func CompareVersions ¶
CompareVersions orders two model versions, returning -1, 0, or +1.
Versions are dotted sequences whose segments are compared NUMERICALLY where both sides are numbers: 4.10 is newer than 4.8, which plain string ordering gets backwards — and getting it backwards would silently point the floating family alias at a stale model, which is the one failure this whole mechanism exists to prevent.
A shorter version is older than a longer one that agrees on every shared segment (4 < 4.1), so a bare `5` loses to `5.1`. Non-numeric segments fall back to string order, which is arbitrary but stable — enough to keep the comparator total so the highest-version pick is never ambiguous.
func DefaultRoot ¶
func DefaultRoot() string
DefaultRoot is the parent of every noun's local store. $BASHY_FLEET_DIR overrides it; each noun may be redirected individually (see NounDir).
func DetectTool ¶
DetectTool reports the harness driving this process, using the default catalog.
bashy calls this on every start, so the marker index is built at most once per process. Only the INDEX is cached — it comes from the registry and does not change under a running process. The environment is read on every call, because that is the question being asked.
func ExitCode ¶
ExitCode maps a fleet command's Execute error to the repo exit convention: 2 usage, 1 otherwise, 0 for nil.
func Main ¶
Main runs a fleet verb tree as a standalone program. Hosts that mount the tree themselves (bashy) call the New*Cmd constructors directly.
func MarkerEnvs ¶
func MarkerEnvs() []string
MarkerEnvs lists every environment variable DetectTool consults, plus the two name-valued conventions.
Exported because the marker set is DATA (it comes from the tool registry, so `bashy tools add` can extend it) and callers need to enumerate it rather than hardcode it:
- a test that wants a genuinely agent-free environment must clear all of them, and a hardcoded list would silently rot the first time a harness is added;
- `bashy doctor` can say WHY it believes an agent is driving the shell.
func Marshal ¶
Marshal renders an entry as canonical YAML — the exact bytes an asset registry would serve as its Content blob. Emitting is always canonical: a legacy `kit:`/`type:` document rewrites to `name:`/`kind:` the first time it is saved.
func NewAgentsCmd ¶
NewAgentsCmd builds the `agents` verb tree.
func NewModelsCmd ¶
NewModelsCmd builds the `models` verb tree.
func NewToolsCmd ¶
NewToolsCmd builds the `tools` verb tree.
func Probes ¶
Probes builds the probe set fleet checks read. It is the same engine pkg/skills gates applicability on, so a tool that a skill's `has=codex` clause can see is a tool `fleet verify` can see.
func ResolveLaunchModel ¶
ResolveLaunchModel maps the provider display string persisted in a launch record back to the catalog's canonical model name. Matching is deliberately conservative: normalized canonical names must occur as a substring and the longest match wins. Unknown values stay unknown (band zero); this function never guesses a neighboring model.
Types ¶
type Agent ¶
type Agent struct {
Name string `yaml:"name" json:"name"` // the primary nickname
Aliases []string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
Display string `yaml:"display,omitempty" json:"display,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
// Nick is the agent's human name — the one you say out loud. Leave it
// empty and the catalog assigns one deterministically from the binding,
// so every agent has a memorable handle without anyone naming it.
Nick string `yaml:"nick,omitempty" json:"nick,omitempty"`
Tool string `yaml:"tool" json:"tool"` // → Tool.Name
Model string `yaml:"model" json:"model"` // → Model.Name
// A CASCADE agent (band_source: cascade) is not a plain tool:model binding.
// It runs a cheap Base agent and, when the base gets stuck, escalates through
// Escalation (a ladder of agent names, tried in order — e.g. an L3 then an L4)
// for a content-full steer. It SERVES at Band via that ladder while running
// cheap most of the time. When Base is set, Model is ignored.
Base string `yaml:"base,omitempty" json:"base,omitempty"`
Escalation []string `yaml:"escalation,omitempty" json:"escalation,omitempty"`
// Band + BandSource are the SERVED band of a cascade agent (BandSource
// "cascade") — the level the ladder REACHES, not the base model's peg. This
// is the one legitimate agent-level band: it is the cascade's contract, not a
// stored model peg that would rot. For a plain tool:model agent these are
// empty and the band is inherited from the model, as always.
Band int `yaml:"band,omitempty" json:"band,omitempty"`
BandSource string `yaml:"band_source,omitempty" json:"band_source,omitempty"`
Role *AgentRole `yaml:"role,omitempty" json:"role,omitempty"`
Ledger *AgentLedger `yaml:"ledger,omitempty" json:"ledger,omitempty"`
Instruction *AgentInstruction `yaml:"instruction,omitempty" json:"instruction,omitempty"`
Functions []string `yaml:"functions,omitempty" json:"functions,omitempty"`
// ClonedFrom and ClonedAt record that this agent was BRANCHED off another,
// and when.
//
// An agent is a singleton identity — one conversation store, one kb
// attribution, one bus cursor — so two concurrent tasks cannot be given to
// one agent without mixing their context, and mixed context produces
// confidently wrong answers. Parallelism is therefore expressed as MORE
// AGENTS, and a clone is how you get one that starts from somewhere rather
// than from nothing: it inherits its parent's context as of ClonedAt and
// diverges from that moment on.
//
// The provenance is kept because the alternative is a fleet of same-binding
// agents with no way to tell which was the original, which was branched off
// what, or when their histories parted.
ClonedFrom string `yaml:"cloned_from,omitempty" json:"cloned_from,omitempty"`
ClonedAt string `yaml:"cloned_at,omitempty" json:"cloned_at,omitempty"`
// Ephemeral marks a clone minted for ONE task, to be removed when that task
// closes. It is hidden from `agents list` unless --all, because a fleet
// roster listing every in-flight task's worker is a roster nobody reads.
// Task, when set, names the work it was minted for.
Ephemeral bool `yaml:"ephemeral,omitempty" json:"ephemeral,omitempty"`
Task string `yaml:"task,omitempty" json:"task,omitempty"`
// AutoNick and Derived are computed by the catalog at load: the
// assigned human name (when Nick is empty) and the floating family
// alias (`claude-opus` for a binding on `opus5`). Both are functions
// of the whole catalog, so neither is ever persisted.
AutoNick string `yaml:"-" json:"auto_nick,omitempty"`
Derived []string `yaml:"-" json:"derived,omitempty"`
Ring assetring.Ring `yaml:"-" json:"ring"`
}
Agent is a tool bound to a model, under a nickname.
func (*Agent) IsCascade ¶
IsCascade reports whether this agent is a composite cascade (a cheap Base that escalates through a ladder), as opposed to a plain tool:model binding.
func (Agent) MatrixKey ¶
MatrixKey is the agent's identity: tool:model. Every nickname for the same binding yields the same key, which is why aliasing never fragments the capability matrix.
func (Agent) Names ¶
Names returns the agent's canonical nickname and every alias it answers to: declared aliases, its human name, and the catalog-derived family alias. One list, so every resolver — whois, chat, meet, weave — sees the same set of names without knowing which were declared and which derived.
type AgentFile ¶
type AgentFile struct {
New bool `yaml:"new,omitempty" json:"new,omitempty"`
LogLevel string `yaml:"log_level,omitempty" json:"log_level,omitempty"`
Agents []Agent `yaml:"agents" json:"agents"`
}
AgentFile is the on-disk envelope for agents. It mirrors the asset registry's shape, where one file may declare several agents.
type AgentInstruction ¶
type AgentInstruction struct {
Content string `yaml:"content,omitempty" json:"content,omitempty"`
}
type AgentLedger ¶
type AliasCollision ¶
type AliasCollision struct {
Kind string
Name string
Holds []string // canonical names of the entries claiming it
}
AliasCollision reports a name claimed by two entries of the same kind.
func (AliasCollision) Error ¶
func (a AliasCollision) Error() string
type Catalog ¶
type Catalog struct {
// contains filtered or unexported fields
}
Catalog reads the merged fleet across every ring.
func (*Catalog) Agent ¶
Agent resolves an agent by nickname, human name, family alias, or declared alias. It also accepts a bare tool:model binding, so `claude:opus4.8` names its agent even before anyone has nicknamed it.
func (*Catalog) Binding ¶
Binding resolves an agent nickname to its tool and model entries. It is the bridge from a name a human typed to the two assets that make the agent runnable. A dangling tool or model is reported, never silently dropped: an agent whose halves do not resolve cannot be launched.
func (*Catalog) CheckAliases ¶
func (c *Catalog) CheckAliases() []AliasCollision
CheckAliases reports names claimed by more than one entry of the same kind. Aliasing is free — `007` and `smarty` may both name one agent — but one name may never mean two things, or `whois` would have to guess.
func (*Catalog) CloneAgent ¶
func (c *Catalog) CloneAgent(parentName, newName string, ephemeral bool, task string) (Agent, error)
CloneAgent mints a copy of an agent under a new name.
What is inherited: the binding (tool + model, or the cascade), the band contract, role, ledger, instruction, functions, description. What is NOT: name, aliases and nick — those ARE the identity, and copying them is what would recreate the collision this exists to avoid.
func (*Catalog) DetectTool ¶
DetectTool reports the agentic harness driving this process, from the environment markers each one sets (the CI=true analog of the agent world).
The marker table used to be a Go literal in pkg/skills. It now lives beside every other fact about a tool, so teaching bashy to recognize a new harness is `bashy tools add`, not a code change.
Detection yields a TOOL, never an agent. A running claude is not `007` — a nickname is minted by whoever launched it, and inventing one here would put a name in the record that resolves to nothing.
func (*Catalog) LiveProbeAgent ¶
func (c *Catalog) LiveProbeAgent(ctx context.Context, name string, timeout time.Duration) (Check, bool)
LiveProbeAgent launches an agent and reports what happened. Reports !ok when no probe is wired: a caller must be able to tell "not verified" from "verified OK".
func (*Catalog) MaterializeAgent ¶
MaterializeAgent copies an agent into the local store if needed.
func (*Catalog) MaterializeModel ¶
MaterializeModel copies a model into the local store if needed.
func (*Catalog) MaterializeTool ¶
MaterializeTool copies a tool into the local store if it is not already there, and returns the path an editor should open.
func (*Catalog) Model ¶
Model resolves a model by canonical name or alias — including the derived family alias, so `opus` finds whichever opus is newest.
func (*Catalog) RemoveAgent ¶
RemoveAgent deletes an agent from the local store.
func (*Catalog) RemoveHost ¶
RemoveHost deletes a static host alias from the local store.
func (*Catalog) RemoveModel ¶
RemoveModel deletes a model from the local store.
func (*Catalog) RemovePerson ¶
RemovePerson deletes a person from the local store.
func (*Catalog) RemoveTool ¶
RemoveTool deletes a tool from the local store.
func (*Catalog) ResolveLaunchModel ¶
ResolveLaunchModel is the catalog-bound form of ResolveLaunchModel, for callers that already hold a (possibly pinned) catalog.
func (*Catalog) SaveAgent ¶
SaveAgent writes an agent into the local store, wrapped in the asset envelope so the file is a valid catalog entry on either side.
func (*Catalog) SavePerson ¶
SavePerson writes a human principal into the local store.
func (*Catalog) SmokeArgv ¶
SmokeArgv renders a real, minimal headless turn for tool. A bare tool has no model in its name, so select its first declared binding; catalog agents are name-sorted, making that choice deterministic.
func (*Catalog) Tools ¶
Tools returns every agentic-CLI tool, name-sorted.
The asset registry's tool namespace is shared with MCP-style function kits (kind func/web/system). Those are not fleet tools and are omitted; pass all=true to see them.
func (*Catalog) VerifyAgent ¶
VerifyAgent reports whether an agent can actually be launched: both halves of its binding resolve, the tool is operable, and the tool can select the model it is bound to.
func (*Catalog) VerifyModel ¶
VerifyModel reports whether a model is usable from this host.
The default is a structural check with no network: a probe that dialed a provider on every `verify` would make an offline host look broken.
func (*Catalog) VerifyTool ¶
VerifyTool reports whether a tool is installed and has a launch declaration.
Standalone and offline: it asks the PATH whether the binary exists and what version it reports. It never runs the tool's own work.
type Check ¶
type Check struct {
Kind string `json:"kind"`
Name string `json:"name"`
OK bool `json:"ok"`
// Skipped marks an entry that was never a candidate — a harness we
// recognize but do not drive, say. Not usable and not a failure: a
// healthy host must not report an error for a tool it never intended
// to launch.
Skipped bool `json:"skipped,omitempty"`
Reason string `json:"reason"`
Detail string `json:"detail,omitempty"` // version, target id, launch argv
// Warn carries something true but not disqualifying — an entry that
// works yet is missing something a caller may be counting on. It never
// affects OK: a warning that failed the check would just get silenced.
Warn string `json:"warn,omitempty"`
}
Check is one entry's verdict at this host's coordinate.
type CloudClient ¶
CloudClient reads an org catalog over the Bearer asset API.
func (CloudClient) Sync ¶
func (c CloudClient) Sync(cacheRoot, noun string) (SyncResult, error)
Sync pulls one noun's org catalog into the overlay cache.
Tools are filtered to the agentic-CLI kind. The asset registry's tool namespace is shared with MCP-style function kits, and a kit is not something the fleet can launch — pulling one into the tool ring would list a name that `verify` can only ever report as unusable.
type CloudConfig ¶
CloudConfig resolves how to reach the overlay.
func (CloudConfig) Resolve ¶
func (c CloudConfig) Resolve() (CloudClient, error)
Resolve fills the base URL and Bearer token, in order:
URL: --url flag > $BASHY_CLOUDBOX_URL > https://ai.dhnt.io Token: --token flag > $BASHY_FLEET_TOKEN > $BASHY_API_KEY > paired outpost
The token needs the read scopes for the nouns being synced. Minting it read-only is the point: a token that pulls a catalog should not be able to rewrite it.
type Config ¶
type Config struct {
// contains filtered or unexported fields
}
Config assembles the rings a Catalog reads. Build one with New.
type ContextCloner ¶
ContextCloner copies an agent's conversation context to a clone, as of now.
INJECTED, for the same import-graph reason as LiveProbe: knowing where an agent's store lives means pkg/chat + pkg/agentlaunch, and both read this registry. The registry declares the hole; the binary fills it.
It returns a NOTE describing what actually happened, and that return is the point. Only a tool whose store bashy relocates can have its context copied; for every other tool the clone starts fresh, and the caller must be able to say which of the two it got. A clone that reports inherited context it did not inherit is the same class of lie as an agent answering from another task's history — the failure this whole model exists to prevent.
type EventsDone ¶
type EventsDone struct {
// Field is the JSON key carrying the event kind — "type" for most, "event"
// for agy. Empty defaults to "type", which is the majority spelling.
Field string `yaml:"field,omitempty" json:"field,omitempty"`
// Values are the kinds that mean the turn ENDED. Any match is a boundary.
Values []string `yaml:"values,omitempty" json:"values,omitempty"`
}
EventsDone matches the event that means a turn finished.
func (EventsDone) Declared ¶
func (d EventsDone) Declared() bool
Declared reports a usable matcher. An EventsDone with no values matches nothing, and must never be treated as "matches everything" — that would end every turn on its first event.
func (EventsDone) Match ¶
func (d EventsDone) Match(line []byte) bool
Match reports whether one NDJSON line announces the end of a turn.
A line that is not JSON, or carries no kind, is NOT a boundary. Tools emit banners, warnings and progress noise on the same stream, and treating an unparseable line as a turn end would cut a run off mid-thought — the same class of error as the silence heuristic, arriving faster.
type EventsOutcome ¶
type EventsOutcome struct {
// Path is a dotted path into the terminal event object — "is_error" for
// claude, "result.status" for agy. Nesting differs per tool, so the path is
// declared rather than assumed.
Path string `yaml:"path,omitempty" json:"path,omitempty"`
// OK are the values at Path that MEAN success, rendered as strings so one
// declaration covers a bool (`false`) and an enum (`SUCCESS`) alike.
OK []string `yaml:"ok,omitempty" json:"ok,omitempty"`
}
EventsOutcome locates the verdict inside a tool's terminal event.
func (EventsOutcome) Declared ¶
func (o EventsOutcome) Declared() bool
Declared reports an outcome rule worth consulting.
func (EventsOutcome) Read ¶
func (o EventsOutcome) Read(line []byte) Verdict
Read extracts the verdict from a terminal event line.
Everything that is not a recognised success reads as Unverified — an unparseable line, a missing path, a value nobody declared. There is deliberately no way for this to return "failed": that would require a failure spelling nobody has observed, and inventing one is exactly the unverified claim this function exists to replace.
type Host ¶
type Host struct {
Name string `yaml:"name" json:"name"`
Aliases []string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
Display string `yaml:"display,omitempty" json:"display,omitempty"`
// Address is where the machine actually answers — a DNS name or an
// address literal. Empty means "resolve the name itself".
Address string `yaml:"address,omitempty" json:"address,omitempty"`
// SSHUser is the account name on THIS host. It is per-host on purpose:
// the local $USER usually does not exist on the remote box.
SSHUser string `yaml:"ssh_user,omitempty" json:"ssh_user,omitempty"`
SSHPort int `yaml:"ssh_port,omitempty" json:"ssh_port,omitempty"`
// LANEndpoint is a service URL reachable only from the same network.
LANEndpoint string `yaml:"lan_endpoint,omitempty" json:"lan_endpoint,omitempty"`
Notes string `yaml:"notes,omitempty" json:"notes,omitempty"`
Ring assetring.Ring `yaml:"-" json:"ring"`
}
Host is a static reach alias for a machine.
Hosts are NOT a registry the way tools, models, and agents are. A machine is discovered (mDNS), owned (an account), or aliased (ssh_config) — three sources of truth a fourth writable store would immediately drift from. This type exists only for reach hints that cannot be discovered: an address on a network with no mDNS, a non-default ssh port, the account name to use there.
Resolution merges these entries with live discovery; see pkg/principal.
type LiveProbe ¶
type LiveProbe func(ctx context.Context, agent string, timeout time.Duration) (status, note string, ok bool)
LiveProbe launches an agent on a trivial prompt and reports whether it can actually speak.
It is INJECTED rather than implemented here, and the reason is the import graph: launching an agent means pkg/chat, and chat reads this registry. So the registry declares the hole and the binary fills it (see WithLiveProbe).
Plain strings, deliberately: the status vocabulary belongs to pkg/agentctl, which also imports this package. Naming its type here would close the cycle.
type Model ¶
type Model struct {
Name string `yaml:"name" json:"name"` // the alias clients pass
Aliases []string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
Display string `yaml:"display,omitempty" json:"display,omitempty"`
// Kind is HOW YOU AUTHENTICATE: subscription | api | local.
Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`
// Billing is HOW YOU PAY: metered | flat | free. Optional — when empty it is
// derived from Kind by BillingMode(), reproducing the old collapsed behaviour, so
// no existing model needs touching.
//
// It exists because z.ai's GLM Coding Plan is flat-rate billing over an API key,
// and no single value of Kind can say that. See the constants above.
Billing string `yaml:"billing,omitempty" json:"billing,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"`
APIKeyRef string `yaml:"api_key_ref,omitempty" json:"api_key_ref,omitempty"`
// UpstreamID is the provider-side model id — the value handed to a
// tool's --model flag. Its YAML key is `model:`, matching the asset
// registry's column.
UpstreamID string `yaml:"model,omitempty" json:"model,omitempty"`
// ToolIDs override UpstreamID for a specific tool, because THE ID A MODEL
// ANSWERS TO IS A PROPERTY OF THE TOOL, NOT OF THE MODEL.
//
// One model, three spellings, all live today:
//
// aider/opencode deepseek/deepseek-v4-pro (litellm wants provider/model)
// ycode deepseek-v4-pro (it detects the provider itself)
// agy Gemini 3.1 Pro (High) (a display string, not a slug)
//
// Treating UpstreamID as one global value made ycode's bindings dead on
// arrival: the registry handed it litellm's prefixed form and ycode rejected
// it, while the same model worked perfectly when ycode was run by hand. That
// is the whole dead-binding failure mode again, and `agents verify --live`
// caught it within a minute of the tool being registered.
//
// Keyed by TOOL name. Absent → UpstreamID.
ToolIDs map[string]string `yaml:"ids,omitempty" json:"ids,omitempty"`
// Family and Version make the canonical name version-explicit. The
// catalog derives the floating family alias from them: `opus` names
// whichever member of family `opus` has the highest Version. A record
// therefore stores `claude:opus5`, which is true forever, while the
// convenient `opus` re-points on its own when a release lands.
//
// Family is declared, never parsed out of the name: `kimi-k2.7-code`
// and `kimi-k2.6` are separate product lines, and no amount of clever
// suffix-stripping gets that right.
Family string `yaml:"family,omitempty" json:"family,omitempty"`
Version string `yaml:"version,omitempty" json:"version,omitempty"`
// Band is the model's capability peg, 1 (basic) to MaxBand (frontier); 0 is
// unpegged. It is normalized ACROSS providers — a provider's own tier
// ladder is never mapped positionally, so four vendor tiers may all
// land in one band. Agents inherit it; they never carry their own.
Band int `yaml:"band,omitempty" json:"band,omitempty"`
// BandSource says whether the band was MEASURED or merely DECLARED, and it
// exists because the fleet has already been burned once by not knowing.
//
// "declared" is a considered guess from provider tier + priors. "measured"
// means the model was run up a difficulty ladder and pegged at the highest
// rung it reliably cleared — which is the only thing a band actually means.
//
// The distinction is load-bearing: a quiz cannot validate a band. Every agent
// in this fleet scores 5/5 on L1-difficulty questions, so passing an easy test
// is evidence of nothing. A band is the highest rung you CLEAR, not a score,
// and until a model has failed something it has not been placed.
//
// Empty means declared. Nothing should present an unmeasured band as fact.
BandSource string `yaml:"band_source,omitempty" json:"band_source,omitempty"`
// Tier is the provider's own word for its tier, carried from an org
// overlay. It is not Band and is not routable.
Tier string `yaml:"tier,omitempty" json:"tier,omitempty"`
Capabilities []string `yaml:"capabilities,omitempty" json:"capabilities,omitempty"`
Domain []string `yaml:"domain,omitempty" json:"domain,omitempty"`
ContextLength int64 `yaml:"context_length,omitempty" json:"context_length,omitempty"`
Price float64 `yaml:"price,omitempty" json:"price,omitempty"`
// Quality is the model's overall capability prior in [0,1]; Spec holds
// per-capability adjustments where a model is notably stronger or
// weaker than its tier. CostMicro is the relative per-turn cost the
// routing objective divides by. All three are read by the capability
// matrix.
Quality float64 `yaml:"quality,omitempty" json:"quality,omitempty"`
CostMicro int64 `yaml:"cost_micro,omitempty" json:"cost_micro,omitempty"`
Spec map[string]float64 `yaml:"spec,omitempty" json:"spec,omitempty"`
XHosts []ModelHost `yaml:"x_hosts,omitempty" json:"x_hosts,omitempty"`
// Derived holds names the catalog computed at load — today, the family
// alias. It is a function of the whole catalog, not of this entry, so
// it is never persisted (`yaml:"-"`): writing it back would freeze a
// pointer that is supposed to float.
Derived []string `yaml:"-" json:"derived,omitempty"`
Ring assetring.Ring `yaml:"-" json:"ring"`
}
Model is an inference backend.
func ParseModel ¶
ParseModel reads a model asset.
func (Model) BillingMode ¶
BillingMode returns how this model is paid for, deriving it from Kind when the Billing field is absent.
The derivation reproduces exactly what the collapsed enum used to mean, which is what makes this a purely additive change: a model written before `billing:` existed keeps its old semantics with no edit.
subscription -> flat (a seat you already paid for) api -> metered (you pay per token) local -> free (your own hardware)
func (Model) MarginalCostMicro ¶
MarginalCostMicro is the cost of the NEXT token — the only cost a routing decision can actually act on, and not always CostMicro.
Under a FLAT plan no invoice moves when you use it, so the naive reading is "free at the margin, prefer it over everything". THAT IS WRONG, and a test caught it: pricing every flat plan at a constant floor made a premium Opus/Codex SEAT marginally cheaper than metered DeepSeek, so the router would have sent every trivial task to the most expensive model in the fleet — inverting the whole point of the band ladder ("don't send a premium model to add a line of YAML").
The thing a flat plan is short of is QUOTA, and quota scarcity SCALES WITH THE MODEL. A premium seat's quota is precious; a commodity seat's is not. Burning Opus quota on a YAML edit is expensive even though no invoice moves.
So a flat plan is a DISCOUNT ON ITS OWN LIST PRICE, never a flat floor:
metered -> CostMicro (you pay per token) flat -> CostMicro * FlatPlanDiscount (prepaid, but the quota is finite) free -> 0 (your own hardware)
That keeps both truths at once: a flat model beats a METERED PEER of the same class (using capacity you already bought is not a saving to forgo), while a premium seat still costs more than a commodity one (its quota is worth more).
func (Model) Names ¶
Names returns the model's canonical name and every alias it answers to, including the catalog-derived family alias.
func (Model) OverrunsIntoMoney ¶
OverrunsIntoMoney reports whether exhausting this model's quota starts BILLING rather than blocking. The one thing an unattended run needs to know before it starts.
func (Model) Target ¶
Target is the id passed to a tool's model flag: the provider-side id when known, else the alias itself.
Prefer TargetFor: the id a model answers to depends on WHICH TOOL is asking.
func (Model) TargetFor ¶
TargetFor is the id THIS TOOL will accept for this model.
The same model is spelled differently by different harnesses — litellm wants `deepseek/deepseek-v4-pro`, ycode wants `deepseek-v4-pro`, agy wants `Gemini 3.1 Pro (High)`. A registry that stores one global id hands the wrong string to somebody, and a wrong model id is a DEAD BINDING: it looks perfectly healthy until an agent tries to speak.
type ModelHost ¶
type ModelHost struct {
Host string `yaml:"host" json:"host"`
Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`
}
ModelHost names a paired host serving a projected local model.
type Option ¶
type Option func(*Config)
Option configures a Catalog.
func WithBaselineFS ¶
WithBaselineFS replaces the compiled-in baseline. Tests use it to pin a catalog to a known world.
func WithContextCloner ¶
func WithContextCloner(f ContextCloner) Option
WithContextCloner supplies the copier `agents clone` uses to branch an agent's context. Without it, cloning still mints the record and says plainly that the clone starts fresh.
func WithLiveProbe ¶
WithLiveProbe supplies the launcher `agents verify --live` uses.
Without it, --live refuses rather than quietly falling back to the structural check. A verification that silently checked something weaker than it claimed would be the exact failure it exists to prevent: a dead binding that looks verified.
func WithRoot ¶
WithRoot pins the local store's parent directory.
An explicit root also disables the per-noun $BASHY_*_DIR overrides: a caller that named a root meant that root, and ambient environment must not redirect writes out of it.
func WithSource ¶
WithSource adds a read-only source for one noun, above the baseline and below the local store.
func WithoutCloudOverlay ¶
func WithoutCloudOverlay() Option
WithoutCloudOverlay drops the org-overlay ring.
func WithoutLocalStore ¶
func WithoutLocalStore() Option
WithoutLocalStore drops the host-local ring. Tests use it to read the baseline without touching the developer's real store.
type Person ¶
type Person struct {
Handle string `yaml:"handle" json:"handle"`
Aliases []string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
Display string `yaml:"display,omitempty" json:"display,omitempty"`
Email string `yaml:"email,omitempty" json:"email,omitempty"`
// OSUsers maps a host name to this person's account name there. It is
// deliberately per-host: assuming the local $USER exists on a remote
// box is the single most common way a cross-host reach fails.
OSUsers map[string]string `yaml:"os_users,omitempty" json:"os_users,omitempty"`
// DefaultOSUser is used for hosts absent from OSUsers.
DefaultOSUser string `yaml:"default_os_user,omitempty" json:"default_os_user,omitempty"`
Hosts []string `yaml:"hosts,omitempty" json:"hosts,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"` // local | cloud
Ring assetring.Ring `yaml:"-" json:"ring"`
}
Person is a human principal. Standalone-first: a local entry needs no account. When the host is paired, Email is the authoritative identity.
func ParsePerson ¶
ParsePerson reads a person asset.
type SyncResult ¶
type SyncResult struct {
Noun string `json:"noun"`
Fetched int `json:"fetched"`
Skipped int `json:"skipped,omitempty"`
Dir string `json:"dir"`
}
SyncResult reports what a pull wrote.
type Tool ¶
type Tool struct {
Name string `yaml:"name" json:"name"`
Kind string `yaml:"kind" json:"kind"` // cli | func | web | system
Aliases []string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
Display string `yaml:"display,omitempty" json:"display,omitempty"`
// Hidden keeps a tool in the registry (still detected, still resolvable by
// explicit name) but omits it from `bashy tools` list/help unless --all.
Hidden bool `yaml:"hidden,omitempty" json:"hidden,omitempty"`
CLI ToolCLI `yaml:"cli,omitempty" json:"cli"`
Quirks string `yaml:"quirks,omitempty" json:"quirks,omitempty"`
// Harness scores the capabilities a tool governs regardless of the
// model behind it (operability, shell, tool-use, isolation). The
// capability matrix reads these as priors.
Harness map[string]float64 `yaml:"harness,omitempty" json:"harness,omitempty"`
Ring assetring.Ring `yaml:"-" json:"ring"`
}
Tool is an agentic CLI harness.
The canonical YAML keys are `name:` and `kind:`. Assets written before that was settled spell them `kit:` and `type:`; both are accepted on parse and neither is emitted. See parse.go.
func ParseTool ¶
ParseTool reads a tool asset. name is the fallback identity when the document carries none (the file's own basename).
func (Tool) Argv ¶
Argv renders the launch template. modelID is the bound model's upstream id ("" when the tool must choose its own model); prompt is the task text. When modelID is empty, {model} and any flag token immediately before it are dropped.
Tokens are whitespace-separated: launch templates are flag lists, never shell. A template that needs quoting is a template in the wrong place.
func (Tool) ArgvPrefix ¶
ArgvPrefix renders everything between the binary and the prompt, for launchers that append the prompt themselves.
It reports false when the template has no {prompt}, or when {prompt} is not the final token. Both cases mean the launcher cannot simply append — and quietly appending anyway would hand the task text to the wrong flag.
func (Tool) ArgvPrefixWithWorkspace ¶
func (Tool) ArgvWithWorkspace ¶
ArgvWithWorkspace renders the headless launch and, when the tool declares a workspace binding, inserts it directly after the binary. Passing an empty workspace preserves the historical template rendering.
func (Tool) CredentialRefFor ¶
CredentialRefFor returns the single credential reference this tool needs to invoke m. An explicit model key always wins. A direct-provider harness may derive the conventional key name from the provider; subscription-native tools receive nothing.
func (Tool) CurrentSession ¶
CurrentSession returns the tool's current session id from the first SessionEnv var that is set, or "" — the id needed to fork THIS session rather than start a fresh one.
func (Tool) EventsArgv ¶
func (Tool) EventsOnStdout ¶
EventsOnStdout reports a tool whose event stream IS its stdout.
The distinction matters to the launcher, not just to the argv: for these tools stdout stops being a transcript to scrape and becomes a stream to parse, so it must be a pipe rather than a pty and nothing else may write to it.
func (Tool) EventsStdoutArgv ¶
EventsStdoutArgv renders the fixed argv that puts a tool's event stream on stdout. No {path}: these tools take no path, which is the whole difference.
func (Tool) ForkArgv ¶
ForkArgv renders the ForkExec template. Like Argv, but also substitutes {session} with the current session id.
func (Tool) ForkArgvPrefix ¶
ForkArgvPrefix is ArgvPrefix for the fork template: the argv between the binary and the trailing {prompt}, with {session}/{model} already substituted. Returns false when the tool declares no ForkExec (delegate self then falls back).
func (Tool) ForkArgvPrefixWithWorkspace ¶
ForkArgvPrefixWithWorkspace is ForkArgvPrefix with an optional declared workspace binding rendered immediately after the binary.
func (Tool) ForkArgvWithWorkspace ¶
ForkArgvWithWorkspace is ForkArgv with an optional declared workspace binding rendered immediately after the binary.
func (Tool) HasEventsArg ¶
EventsArgv renders the tool's event-channel flag for a given path, or nil when the tool cannot stream events (which is every third-party CLI we have).
func (Tool) IsCLI ¶
IsCLI reports whether this tool is an agentic CLI — the only tool kind the fleet drives. A missing kind means an old asset that predates the discriminator; those were all function kits, so absence is not cli.
func (Tool) ModelFlag ¶
ModelFlag is the flag token that carries the model — the token immediately before {model} in the launch template, when it is a flag.
A caller that already holds an argv from somewhere else (a self-healed tool profile, say) needs the flag's spelling to add the model to it. It returns "" when the template positions the model without a flag, and such a template can only be rendered whole, by Argv.
func (Tool) ReportsTurnEnd ¶
ReportsTurnEnd says whether this tool tells us when a turn is over, instead of leaving us to infer it from silence.
EITHER ROUTE COUNTS. This used to read EventsArg alone, which was true when ycode's side-channel file was the only way a tool could speak — and became wrong the moment a stdout streamer was declarable. Left as it was, claude, codex and agy would each stream a perfectly good turn boundary while this reported them silent, and every caller would keep paying the 25-second tax to re-derive a fact already on the wire.
func (Tool) SteerArgvPrefix ¶
SteerArgvPrefix renders the STEERABLE launch — the interactive session, not the headless one-shot.
Reports false when the tool has no steer_exec, i.e. it has no session to open. A caller that wants to interrupt an agent must be told that plainly rather than handed a one-shot that will exit before the first steer arrives.
func (Tool) SteerArgvPrefixWithWorkspace ¶
SteerArgvPrefixWithWorkspace is SteerArgvPrefix with an optional declared workspace binding rendered immediately after the binary.
func (Tool) SteerTakesPrompt ¶
SteerTakesPrompt reports whether the steerable launch accepts an opening prompt on the command line (agy -i does; codex and opencode open an empty session).
func (Tool) StreamsEvents ¶
StreamsEvents reports a tool that emits structured events at all, by either route — a side-channel file or its own stdout.
func (Tool) TakesModel ¶
TakesModel reports whether the launch template can select a model. A tool without a {model} placeholder cannot: binding it to a model is a label, not a selection.
func (Tool) VersionProbeArgv ¶
VersionProbeArgv renders the provider-declared capability probe. Tools with no declaration retain the universal --version convention.
type ToolCLI ¶
type ToolCLI struct {
Binary string `yaml:"binary,omitempty" json:"binary,omitempty"`
Versions []ToolVersion `yaml:"versions,omitempty" json:"versions,omitempty"`
Launch ToolLaunch `yaml:"launch,omitempty" json:"launch"`
}
type ToolLaunch ¶
type ToolLaunch struct {
// Exec is the argv template. {prompt} is replaced by the task text and
// {model} by the bound model's upstream id. When no model is bound,
// {model} and the flag token immediately preceding it are dropped, so
// a template with a model flag degrades exactly to one without.
Exec string `yaml:"exec,omitempty" json:"exec,omitempty"`
// Credential declares how this harness authenticates a bound model.
// "model-provider" grants only the credential named by Model.APIKeyRef, or
// by Model.Provider when no explicit key reference exists. Values remain in
// the launcher environment; this field carries names and policy only.
Credential string `yaml:"credential,omitempty" json:"credential,omitempty"`
// WorkspaceArg is an optional argv fragment that binds the launched tool to
// the orchestrator's allocated workspace. {workspace} is replaced by that
// absolute path. It is rendered immediately after the binary, before the
// exec template's model and prompt arguments. Tools that do not declare it
// retain their existing argv exactly.
WorkspaceArg string `yaml:"workspace_arg,omitempty" json:"workspace_arg,omitempty"`
// WorkspacePreflightExec is an optional read-only launch template used to
// ask the tool which PWD/project directory it selected. The launcher supplies
// a reporting prompt and refuses to start the source-writing invocation
// unless the reported path equals the allocated workspace.
WorkspacePreflightExec string `yaml:"workspace_preflight_exec,omitempty" json:"workspace_preflight_exec,omitempty"`
// VersionProbeExec is an optional provider-declared, read-only command used
// by fleet capability probing instead of assuming every CLI accepts --version.
VersionProbeExec string `yaml:"version_probe_exec,omitempty" json:"version_probe_exec,omitempty"`
// PromptPosition records where the prompt goes for consumers that
// cannot read the template (cloudbox conductor). Advisory here: the
// {prompt} placeholder is authoritative.
PromptPosition string `yaml:"prompt_position,omitempty" json:"prompt_position,omitempty"`
// TrustPreseed names a config file the host must pre-seed so the CLI
// does not no-op on a first-run trust prompt.
TrustPreseed string `yaml:"trust_preseed,omitempty" json:"trust_preseed,omitempty"`
Watchdog ToolWatchdog `yaml:"watchdog,omitempty" json:"watchdog"`
// SupportsSay marks a tool that CAN be steered mid-run — a capability fact
// about the tool, MEASURED (pkg/agentpty/steer_live_test.go), not asserted.
SupportsSay bool `yaml:"supports_say,omitempty" json:"supports_say,omitempty"`
// ACPExec is the argv template that launches this tool as an ACP AGENT
// speaking JSON-RPC on stdio. Empty means the tool does not speak ACP and
// the launcher falls to the next rung.
//
// NO {prompt} and NO {model}. The prompt travels in the ACP session rather
// than argv, which is what gives the transport a real turn boundary. The
// model is fixed by the binding before the launch: ACP carries no
// model-selection call, and a bound model refuses the ACP rung outright
// (see agentlaunch.ACPArgv). A {model} token here renders LITERALLY.
ACPExec string `yaml:"acp_exec,omitempty" json:"acp_exec,omitempty"`
// EventsArg is how this tool is told to stream STRUCTURED EVENTS, if it can.
//
// This is the difference between a first-party harness and a third-party one,
// and it is not cosmetic. Without it, bashy decides a turn has ended by
// WATCHING FOR SILENCE — 25 seconds of no output (see chat.Session.WaitIdle).
// That heuristic is wrong in both directions: an agent that pauses to think
// looks finished, and an agent that renders a spinner never does. Every turn
// also pays the 25 seconds on its way out, which is why `meet --steerable` is
// a flag and not the default.
//
// A tool that declares this gets a real boundary instead: it says `turn.end`,
// and bashy believes it, because it is a fact the agent reported rather than a
// silence bashy interpreted.
//
// Template with one token: {path}. e.g. `--events {path}`.
// The events are NDJSON, one object per line, with at minimum:
// {"type":"turn.start"} {"type":"tool.call"} {"type":"turn.end", ...}
EventsArg string `yaml:"events_arg,omitempty" json:"events_arg,omitempty"`
// EventsStdout is the same capability for tools that stream on STDOUT
// rather than into a file bashy names.
//
// EventsArg above was written for ycode, which takes a path and writes
// NDJSON there. Every third-party tool measured on the wire does the
// opposite — claude, codex and agy each stream to stdout and accept no path
// at all — so declaring EventsArg for them would render a flag that does not
// exist. Two fields, because they are two different plumbing paths: one
// opens a side channel, the other means stdout is no longer a transcript to
// scrape but a stream to parse.
//
// Fixed argv, no {path} token. Measured 2026-07-31:
//
// claude -p {prompt} --output-format stream-json --verbose
// codex exec --json
// agy -p {prompt} --output-format stream-json
//
// THESE ARE PRINT-MODE FLAGS, AND THAT BOUNDS WHAT THEY BUY. All three
// belong to the one-shot Exec path (`-p`, `exec`) — claude states outright
// that stream-json works only with --print — and none is available on
// SteerExec, the bare interactive TUI.
//
// So they do NOT remove the 25-second silence tax, and it would be wrong to
// wire them expecting that. The tax lives on the STEERING path (coach,
// foreman, meet, herald all call Session.WaitIdle), where a session persists
// across turns and these flags cannot be passed. A one-shot already has an
// exact boundary: the process exits.
//
// What they buy instead is the fleet-evidence property — a tool call, a
// stop_reason and an error status as STRUCTURED FACTS rather than lines
// scraped back out of a terminal and guessed at. That is worth having, and
// it is a different thing from turn detection.
EventsStdout string `yaml:"events_stdout,omitempty" json:"events_stdout,omitempty"`
// EventsDone declares how THIS tool spells "the turn ended".
//
// The EventsArg contract documented `{"type":"turn.end"}` and no third-party
// tool says that. Measured on the wire, the same fact has three spellings —
// codex `type: turn.completed`, claude `type: result`, agy `event: result` —
// and even the KEY differs, so a matcher that assumed `type` would silently
// never fire on agy. Silently, because a boundary that never arrives is
// indistinguishable from a tool that is still thinking: the reader would
// fall back to the 25-second silence tax it was trying to escape, and
// nothing would report that the declaration was wrong.
//
// Declared per tool rather than inferred, for the reason this package
// declares everything else: a guess that happens to work is a guess that
// breaks on the next release with nobody watching.
EventsDone EventsDone `yaml:"events_done,omitempty" json:"events_done,omitempty"`
// EventsOutcome locates the VERDICT inside that terminal event.
//
// The exit code is not it. "All three harnesses EXITED 0 WHEN THEY FAILED"
// is recorded in the umbrella's own notes, so a caller reading the status
// learns nothing and reports success. The stream carries what the exit does
// not. See eventsoutcome.go for why only SUCCESS is declarable.
EventsOutcome EventsOutcome `yaml:"events_outcome,omitempty" json:"events_outcome,omitempty"`
// SteerExec is the argv template that ACTUALLY accepts steering, and it is
// usually NOT Exec.
//
// A headless one-shot has nothing to steer: `codex exec` and `agy -p` run the
// prompt and exit. Steering needs the tool's interactive session — bare `codex`,
// or `agy -i` ("run an initial prompt interactively and CONTINUE the session").
//
// Two templates, because the choice is a real trade. Exec gives a clean captured
// answer (stdout and stderr stay apart on a pipe). SteerExec gives a session you
// can interrupt, at the cost of a pty that merges the tool's chrome into the
// transcript. A launcher picks by what it needs; the registry refuses to pretend
// one launch does both.
SteerExec string `yaml:"steer_exec,omitempty" json:"steer_exec,omitempty"`
// ForkExec is the argv template that FORKS the tool's current session — a new,
// independent session that inherits the live transcript — instead of starting
// fresh. {session} = the current session id, {prompt} = the directive, {model}
// = the model. This is what makes `delegate self` a true context-inheriting
// fork ("delegate to yourself, no re-briefing"). ONLY a tool with a genuine
// HEADLESS, NON-mutating fork declares this: claude has one
// (`--resume <id> --fork-session -p`), codex does NOT — its headless `resume`
// APPENDS to the parent thread, which would corrupt the steward's own session,
// so codex has no ForkExec and `delegate self` falls back to a fresh instance.
ForkExec string `yaml:"fork_exec,omitempty" json:"fork_exec,omitempty"`
// SessionEnv names the env var(s) that carry this tool's current session id
// when it drives a subprocess (e.g. CLAUDE_CODE_SESSION_ID). First non-empty
// wins. Without a readable session id, a ForkExec that needs {session} cannot
// fire, and delegate self falls back to a fresh instance.
SessionEnv []string `yaml:"session_env,omitempty" json:"session_env,omitempty"`
// SupportsGracefulQuit marks a tool that exits cleanly on a quit signal.
SupportsGracefulQuit bool `yaml:"supports_graceful_quit,omitempty" json:"supports_graceful_quit,omitempty"`
// TrustClear is the steering input that clears a trust prompt.
TrustClear string `yaml:"trust_clear,omitempty" json:"trust_clear,omitempty"`
// AuthHint explains an interactive sign-in the tool needs before it
// can run headless at all.
AuthHint string `yaml:"auth_hint,omitempty" json:"auth_hint,omitempty"`
// Notes is the free-text launch contract commentary.
Notes string `yaml:"notes,omitempty" json:"notes,omitempty"`
// EnvMarkers are environment variables whose presence identifies this
// tool as the one currently running.
EnvMarkers []string `yaml:"env_markers,omitempty" json:"env_markers,omitempty"`
}
ToolLaunch is how the orchestrator invokes a tool headlessly.
type ToolVersion ¶
type ToolWatchdog ¶
type Verdict ¶
type Verdict string
Verdict is what a terminal event says about the turn.
const ( // VerdictSucceeded — the tool reported success, in its own words. VerdictSucceeded Verdict = "succeeded" // VerdictUnverified — no success signal was found. NOT failure: the tool // may have failed, or may simply not say. Either way the caller still owes // a gate, which is the same conclusion the evidence invariant reaches. VerdictUnverified Verdict = "unverified" )