Documentation
¶
Overview ¶
Package bundle implements the `.botz` archive format: a ZIP archive that packages an iterion workflow (`main.bot`) with adjacent resources (skills, prompts, presets, default attachments, manifest). A downloaded `.botz` therefore extracts with `unzip` / double-click. Older bundles were gzipped tarballs (tar.gz) — those are still read transparently (the loader auto-detects the container format via magic bytes), so the migration is backward-compatible. A bundle is loaded once per run, extracted into a content-addressed cache directory, and then exposed to the engine as a *Bundle so skills/prompts become visible to claude_code and the claw tool registry without authoring changes.
The bundle content hash (Bundle.Hash / PackResult.Hash) is computed over the LOGICAL content — the sorted sequence of (relative-path, file-bytes) — independent of the container format, so the same files hash identically whether packed as ZIP or read from a legacy tar.gz.
Index ¶
- Constants
- Variables
- func DirForMainBot(path string) string
- func ExtractArchive(r io.Reader, dest string) (int, error)
- func IsPackSkipped(rel string) bool
- type Bundle
- type ConfigShareSpec
- type ExecutionMode
- type ForgeRequirements
- type ForgeWebhookHints
- type Frontmatter
- type Invocation
- type InvocationBoard
- type InvocationCommand
- type InvocationForge
- type InvocationKeepalive
- type InvocationKind
- type InvocationSchedule
- type Kind
- type LaunchHints
- type Manifest
- type ManifestPatch
- type PackResult
- type PresetSpec
- type RepoRequirement
Constants ¶
const ( // DirSkills holds `SKILL.md` files mirrored into the run // workspace's `.claude/skills/`. DirSkills = "skills" // DirPrompts holds reusable `.md` prompts; the filename stem // becomes the prompt name. DirPrompts = "prompts" // DirAttachments holds default binary inputs referenced from the // manifest's `attachments:` map. DirAttachments = "attachments" // DirPresets holds file-based presets (named sous-bots). DirPresets = "presets" )
Layout directory names. A bundle resolves each by convention at its root, so these strings ARE the format — spelling one differently silently disables that resource kind.
const ( // MainBotFile is the workflow source at a bundle's root — the // familiar main.go / main.rs convention, independent of the bundle // directory's own name. MainBotFile = "main.bot" // ManifestFile is the bundle manifest. ManifestFile = "manifest.yaml" // ManifestFileAlt is the accepted `.yml` spelling of ManifestFile. ManifestFileAlt = "manifest.yml" )
Bundle root file names. Like the layout directories, these strings ARE the format: several packages outside pkg/bundle reach into a bundle by name, and they must all spell it the same way.
const ( RepoModeRequired = "required" RepoModeOptional = "optional" RepoModeNone = "none" )
Valid RepoRequirement.Mode values.
const ( ForgeEventPullRequest = "pull_request" ForgeEventPullRequestComment = "pull_request_comment" // ForgeEventIssueLabeled subscribes the repo hook to the forge-native // "issues" event; labeling an issue launches an implementer bot that // opens a PR back-linked to the issue (see the GitHub issues handler). ForgeEventIssueLabeled = "issue_labeled" )
Normalized forge event vocabulary used in a manifest `forge.events` block. The auto-provisioner (pkg/forge) maps each entry to the per-provider native event when it creates the forge-side hook:
pull_request -> gitlab "merge_requests_events",
github / forgejo "pull_request"
pull_request_comment -> gitlab "note_events",
github / forgejo "issue_comment"
issue_labeled -> github / forgejo "issues"
(gitlab "issues_events" — not yet wired inbound)
const ( BoardKindCardCreated = "card.created" BoardKindCardMoved = "card.moved" BoardKindCardLabeled = "card.labeled" BoardKindCardUpdated = "card.updated" )
Board card-event kinds a kind=board invocation may filter on. These mirror the trigger package's Kind* constants; bundle can't import trigger (trigger imports bundle), so they are duplicated here and kept in sync — the closed set is enforced at parse time so a typo fails fast.
const CurrentManifestSchema = 1
CurrentManifestSchema is the manifest schema version this build understands. Bumped only on breaking changes; minor additive fields use the reserved `Compat` map to avoid forcing a version bump on every new key.
const DefaultForgeSecretName = "forge_token"
DefaultForgeSecretName is the workflow-secret name an integration binds the connection's forge token under when a manifest's forge.secret is empty. Matches the name review-pr / revi-converse declare in their .bot `secrets:` block.
const KeepaliveMinInterval = 5 * time.Second
KeepaliveMinInterval is the floor on a keepalive invocation's interval: a guardrail against a launch storm (each tick is a fresh budgeted run).
Variables ¶
var KnownForgeEvents = map[string]bool{ ForgeEventPullRequest: true, ForgeEventPullRequestComment: true, ForgeEventIssueLabeled: true, }
KnownForgeEvents is the closed set of normalized event names a manifest may declare in forge.events. decodeManifest rejects anything else so a typo fails fast at parse time (same bar as attachments:).
var LayoutDirs = []string{DirSkills, DirPrompts, DirAttachments, DirPresets}
LayoutDirs is the canonical order of the layout directories, shared by the loader (which resolves them), the packer (which archives them), and pkg/botscaffold (which creates them). Kept as one list so adding a convention directory is not a three-package hunt.
Functions ¶
func DirForMainBot ¶ added in v1.0.0
DirForMainBot returns the bundle directory holding path, or "" when path is not a bundle's main.bot.
Callers outside pkg/bundle need this to decide whether to open a workflow as a bundle (picking up its skills, prompts, presets and attachments) or as a loose file. It lives here because it encodes what a bundle IS — when two packages answered that question with their own copy of the marker list, they could disagree about it after any change to the layout.
func ExtractArchive ¶
ExtractArchive extracts a `.botz` stream from r into dest, applying the same path-traversal / size / symlink guards as Open. The container format (ZIP or legacy tar.gz) is auto-detected from the leading magic bytes. dest must be a directory the caller exclusively owns (it is created if missing). Returns the number of regular files written.
archive/zip needs a ReaderAt + size and cannot stream a pipe, so the stream is read fully into memory first. Bundles are small (capped at ITERION_BUNDLE_MAX_BYTES uncompressed; the compressed upload is smaller still), so this is acceptable for the .botz-upload path this serves.
Unlike Open it does NOT cache, content-hash, or validate the bundle structure — callers that need a validated Bundle follow with OpenDir(dest). This is the in-memory entry point behind .botz uploads, where the bytes arrive over HTTP rather than from a file on disk.
func IsPackSkipped ¶ added in v1.0.0
IsPackSkipped reports whether a bundle-relative path would be excluded from a .botz by PackDir. Exported so a scaffolder can prove the `.gitignore` it writes only lists things the packer already drops, instead of asserting that in a comment.
Types ¶
type Bundle ¶
type Bundle struct {
// Dir is the absolute path of the extracted (or in-place) bundle
// root. Engine consumers should treat it as read-only.
Dir string
// Manifest holds the parsed `manifest.yaml`. Nil when the bundle
// omits the file (allowed — the field is optional).
Manifest *Manifest
// IterPath is the absolute path of the workflow source file
// inside the bundle (`main.bot`, at the bundle root).
IterPath string
// SkillsDir is `<Dir>/skills` when the directory exists, else "".
SkillsDir string
// PromptsDir is `<Dir>/prompts` when the directory exists, else "".
PromptsDir string
// AttachmentsDir is `<Dir>/attachments` when the directory exists,
// else "". Holds pre-bundled default values for the workflow's
// `attachments:` block — runtime uploads (Launch modal) override.
AttachmentsDir string
// PresetsDir is `<Dir>/presets` when the directory exists, else "".
// Holds file-based presets (`<name>.md`, YAML frontmatter + prompt
// body) — named sous-bots that bias the workflow at launch. Parsed
// by LoadPresets; merged into the runtime workflow's preset set by
// the engine at run start.
PresetsDir string
// Hash is the SHA-256 of the bundle's logical content (the sorted
// (relative-path, file-bytes) sequence), used as the cache key. It is
// independent of the container format, so a ZIP bundle and a legacy
// tar.gz bundle with identical files share the same hash. Empty for
// KindBundleDir bundles (no archive to hash; callers handle directory
// bundles per-run).
Hash string
// SourcePath is the original `.botz` filesystem path for KindBundle,
// or the source directory for KindBundleDir. Persisted with the run
// so resume can re-extract from the same archive after a cache GC.
SourcePath string
// Kind discriminates how the bundle was supplied.
Kind Kind
}
Bundle is a resolved, on-disk bundle ready for runtime consumption. All path fields are absolute; optional resource directories are the empty string when not present in the bundle.
func Open ¶
Open loads a `.botz` archive from path, extracting it to a stable content-addressed location under cacheRoot. Returns the Bundle, a cleanup function (no-op when cached; per-run extraction would clean up here), and an error.
cacheRoot defaults to `<UserCacheDir>/iterion/bundles` when empty. Extraction is idempotent — concurrent runs of the same bundle share the cache via a `.ready` sentinel.
type ConfigShareSpec ¶ added in v0.50.0
type ConfigShareSpec struct {
// (e.g. "feed-watch.json"). Repo-relative; normalized + guarded at mint
// (no traversal, no .git/.github, inside the workspace).
ConfigPath string `yaml:"config_path" json:"config_path"`
// "{category}" placeholder is expanded to the concrete category at mint
// (e.g. "categories.{category}.feeds"), so ONE declaration covers every
// category; a config with no categories lists literal paths. No globs —
// every entry is a full leaf path.
EditablePaths []string `yaml:"editable_paths" json:"editable_paths"`
// context (e.g. "categories.{category}.digest_title"). The GET projection
// returns EditablePaths ∪ VisiblePaths and nothing else. Same {category}
// expansion. Optional.
VisiblePaths []string `yaml:"visible_paths,omitempty" json:"visible_paths,omitempty"`
// signed-in config_editor shell with a bot-specific name (e.g. "Éditeur de
// veilles"). Optional; empty falls back to the generic title.
EditorTitle string `yaml:"editor_title,omitempty" json:"editor_title,omitempty"`
// what the editor edits ("Sources et éditorial de vos veilles"). Optional.
EditorDescription string `yaml:"editor_description,omitempty" json:"editor_description,omitempty"`
}
ConfigShareSpec is a bot's declared scoped config-share surface — the contract that lets an operator mint a share for the bot (pkg/configshare) without knowing its config file's JSON structure, and that a share can never exceed. A second bot adopts the config-share editor by adding this block alone: no server or SPA change.
type ExecutionMode ¶
type ExecutionMode string
ExecutionMode controls how a fired invocation becomes a run.
direct → launch the run immediately (the Revi path:
insertAndLaunchWebhook → publisher → queue → runner). For
fast, read-only, PR-bound work.
board → materialise a kanban issue assigned to the bot; the dispatcher
claims and runs it (tracked, retryable, supports human gates).
For long, mutating, to-be-tracked work.
const ( ExecutionDirect ExecutionMode = "direct" ExecutionBoard ExecutionMode = "board" )
type ForgeRequirements ¶
type ForgeRequirements struct {
// Events is the normalized event vocabulary this bot wants the
// auto-created webhook to subscribe to (see KnownForgeEvents).
Events []string `yaml:"events,omitempty"`
// TokenScopes is a normalized permission map (key -> "read" |
// "write" | "admin"); keys ∈ {pull_requests, repository, issues,
// webhooks}. The provisioner always needs webhook-admin regardless
// of this map — declaring it is informational. Unioned across
// co-enabled bots to size the requested OAuth scope.
TokenScopes map[string]string `yaml:"token_scopes,omitempty"`
// Secret is the workflow-secret name the bundle's main.bot
// `secrets:` block expects (e.g. "forge_token"). Empty defaults to
// DefaultForgeSecretName. The orchestrator binds the connection's
// managed forge token under this name; botregistry cross-references
// it against the parsed .bot secret names.
Secret string `yaml:"secret,omitempty"`
// Webhook carries the launch-side knobs the orchestrator copies into
// the auto-created webhooks.Config.
Webhook *ForgeWebhookHints `yaml:"webhook,omitempty"`
// Rationale is free text shown verbatim in the Integrations enable
// dialog so the operator understands why each scope is requested.
Rationale string `yaml:"rationale,omitempty"`
}
ForgeRequirements is the `forge:` block of a bundle manifest. All fields are optional; a bundle with no forge: block has Forge == nil.
func (*ForgeRequirements) SecretName ¶
func (f *ForgeRequirements) SecretName() string
SecretName returns the workflow-secret name this bot binds its forge token under, applying DefaultForgeSecretName when unset.
type ForgeWebhookHints ¶
type ForgeWebhookHints struct {
// LaunchVars are default vars the auto-created webhook stamps onto
// every run it launches (merged with the handler defaults; operator
// overrides still win).
LaunchVars map[string]string `yaml:"launch_vars,omitempty"`
// MinReplierRole mirrors webhooks.Config.MinReplierRole — the
// minimum forge role a commenter must have to trigger the bot via a
// note. Empty inherits the webhook default.
MinReplierRole string `yaml:"min_replier_role,omitempty"`
// AuthorAllowlist mirrors webhooks.Config.AuthorAllowlist — restrict the
// auto-created webhook to PRs/MRs opened by these author logins (empty =
// any author). A dependency-PR bot sets ["dependabot[bot]",
// "renovate[bot]"] so it reacts only to the dependency bots, not humans.
AuthorAllowlist []string `yaml:"author_allowlist,omitempty"`
}
ForgeWebhookHints are the webhook-launch knobs an auto-provisioned integration copies into webhooks.Config.
type Frontmatter ¶
type Frontmatter struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Triggers []string `yaml:"triggers"`
Capabilities []string `yaml:"capabilities"`
}
Frontmatter is the optional `## ---` … `## ---` YAML block at the top of a main.bot. It lets a loose .bot file or a bundle carry catalog metadata (name / description / triggers / capabilities) inline. For a bundle the manifest is authoritative; a non-empty frontmatter value OVERRIDES the manifest's triggers/capabilities at discovery time (botregistry.parseBundle). bundlelint flags that silent override (C221).
func ParseFrontmatter ¶
func ParseFrontmatter(raw []byte) *Frontmatter
ParseFrontmatter pulls a `## ---` … `## ---` block from the top of the file and YAML-decodes the inner content. The block is allowed only at the very top of the file, optionally after blank lines. Returns nil when the block is absent or malformed.
func ReadFrontmatter ¶
func ReadFrontmatter(path string) *Frontmatter
ReadFrontmatter reads the file at path and returns its parsed frontmatter, or nil when the file is unreadable or carries no `## ---` block.
type Invocation ¶
type Invocation struct {
Kind InvocationKind `yaml:"kind" json:"kind"`
// Mode is the execution mode for this path. Empty defaults to "direct"
// (see EffectiveMode).
Mode ExecutionMode `yaml:"mode,omitempty" json:"mode,omitempty"`
// ArgsVar names the workflow input var that receives the trigger's
// free-text payload (the comment args after the command, etc.). Empty
// injects no payload. Cross-checked against the bot's declared vars by
// botregistry.ListWithSchema (a warning, not a hard error).
ArgsVar string `yaml:"args_var,omitempty" json:"args_var,omitempty"`
// ContextVars are extra launch vars stamped on every run from this
// invocation, merged BEFORE the operator's webhook LaunchVars (operator
// still wins).
ContextVars map[string]string `yaml:"context_vars,omitempty" json:"context_vars,omitempty"`
Forge *InvocationForge `yaml:"forge,omitempty" json:"forge,omitempty"`
Command *InvocationCommand `yaml:"command,omitempty" json:"command,omitempty"`
Schedule *InvocationSchedule `yaml:"schedule,omitempty" json:"schedule,omitempty"`
Board *InvocationBoard `yaml:"board,omitempty" json:"board,omitempty"`
Keepalive *InvocationKeepalive `yaml:"keepalive,omitempty" json:"keepalive,omitempty"`
}
Invocation declares one way this bot can be triggered, plus the execution mode that path uses. The payload field that applies is selected by Kind (Forge for kind=forge, Command for kind=command, Schedule for kind=schedule; kind=board needs none).
func EffectiveInvocations ¶
func EffectiveInvocations(m *Manifest) []Invocation
EffectiveInvocations returns the manifest's explicit invocations when present, else the synthetic set derived from the legacy forge: block. This is the single accessor every consumer (botregistry, the orchestrator, the command router) should use so the migration shim stays in one place.
func SyntheticInvocations ¶
func SyntheticInvocations(m *Manifest) []Invocation
SyntheticInvocations derives the Invocation set a manifest WITHOUT an explicit `invocations:` block should be treated as having, from its legacy `forge:` block. Used by botregistry so a bundle that predates the typed invocations schema still participates in the Integrations picker (its forge-EVENT reachability is preserved). Returns nil when there's nothing to derive.
It deliberately does NOT synthesise slash-commands: a command name can't be inferred generically from a forge.events entry, and inventing one would risk colliding with another bot's real command. In-tree bots declare their commands explicitly (see bots/*/manifest.yaml); this shim only keeps a forge:-only bundle visible as event-capable.
func (Invocation) EffectiveMode ¶
func (i Invocation) EffectiveMode() ExecutionMode
EffectiveMode returns the execution mode, defaulting an empty value to ExecutionDirect (the safe, PR-bound behaviour).
type InvocationBoard ¶
type InvocationBoard struct {
// On filters by card-event kind (subset of knownBoardKinds). Empty = any.
On []string `yaml:"on,omitempty" json:"on,omitempty"`
// ToStates fires only when the card enters one of these board states.
// Empty = any state.
ToStates []string `yaml:"to_states,omitempty" json:"to_states,omitempty"`
// AllLabels requires the card to carry every listed label (AND). Empty =
// no label constraint.
AllLabels []string `yaml:"all_labels,omitempty" json:"all_labels,omitempty"`
// ConsumeLabels strips the AllLabels set from the card atomically before
// firing, so the labels act as a one-shot trigger: a card-event storm
// (forge re-syncs, edits) cannot re-fire, and re-adding the label re-arms
// the trigger. Only meaningful with mode=direct (the promote path is
// already idempotent); requires a non-empty AllLabels.
ConsumeLabels bool `yaml:"consume_labels,omitempty" json:"consume_labels,omitempty"`
}
InvocationBoard is the optional payload of a kind=board invocation. It declares which native-board transitions fire this bot: the card-event kinds (On), the board states the card must have entered (ToStates), and the labels the card must ALL carry (AllLabels). An empty block keeps the legacy behaviour — the bot is a plain dispatcher target picked up when an issue's Bot == this bot. With a block, the orchestrator/operator derives a trigger.Subscription whose Matcher fires the bot the moment a matching card transition lands, instead of waiting for the dispatcher poll.
type InvocationCommand ¶
type InvocationCommand struct {
// Name is the slash-command id WITHOUT the leading "/" (e.g. "revi",
// "featurly"). Lowercase ^[a-z][a-z0-9_-]*$.
Name string `yaml:"name" json:"name"`
// Aliases are additional command ids that route to this bot (e.g. the
// technical name "feature-dev" aliasing the persona "featurly").
Aliases []string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
// Scope restricts where the command is honoured: "pr" (default),
// "issue", or "any".
Scope string `yaml:"scope,omitempty" json:"scope,omitempty"`
// MinReplierRole overrides the webhook's MinReplierRole for THIS
// command — a mutating bot can demand "maintainer" while a reviewer
// stays "developer". Empty inherits the webhook default.
MinReplierRole string `yaml:"min_replier_role,omitempty" json:"min_replier_role,omitempty"`
// Disambiguator resolves a same-name command shared by two co-enabled
// bots (the review-pr vs revi-converse pattern): "when_args_empty"
// claims a bare "/cmd", "when_args_present" claims "/cmd <args>". Empty
// claims the command unconditionally.
Disambiguator string `yaml:"disambiguator,omitempty" json:"disambiguator,omitempty"`
// OpensMR marks this command as one whose bot opens a merge/pull request
// AND should back-link the original issue the human commented on. When set
// and the command fires in board mode, the dispatch layer stamps
// open_mr="true" + source_issue_ref=<subject URL/ref> into the materialised
// card's bot_args, so the routed bot (a code-improvement bot that declares
// the matching open_mr / source_issue_ref vars) opens the MR and links the
// issue. Off for read-only commands (e.g. /revi) so unrelated board
// commands aren't stamped.
OpensMR bool `yaml:"opens_mr,omitempty" json:"opens_mr,omitempty"`
}
InvocationCommand is the payload of a kind=command invocation.
type InvocationForge ¶
type InvocationForge struct {
// Event is one of KnownForgeEvents.
Event string `yaml:"event" json:"event"`
// Actions narrows the trigger to specific provider actions (e.g.
// "opened","reopened" for a PR). Empty applies the handler's default
// reviewable-action filter.
Actions []string `yaml:"actions,omitempty" json:"actions,omitempty"`
}
InvocationForge is the payload of a kind=forge invocation.
type InvocationKeepalive ¶ added in v1.0.0
type InvocationKeepalive struct {
// Interval is how often to relaunch the bot (Go duration, e.g. "30s",
// "5m"). Required, must be >= KeepaliveMinInterval. Sub-minute values
// need the resident in-process scheduler.
Interval string `yaml:"interval" json:"interval"`
// StaleAfter is the silence cutoff: a running run whose last progress is
// older than this is treated as dead, so a fresh run relaunches and the
// zombie is reaped. Empty defaults to schedgate.DefaultStaleAfter.
StaleAfter string `yaml:"stale_after,omitempty" json:"stale_after,omitempty"`
// DefaultVars are vars stamped on each relaunched run.
DefaultVars map[string]string `yaml:"default_vars,omitempty" json:"default_vars,omitempty"`
}
InvocationKeepalive is the payload of a kind=keepalive invocation.
type InvocationKind ¶
type InvocationKind string
InvocationKind classifies the surface that can fire a bot. Closed set, validated at manifest parse time (same bar as KnownForgeEvents) so a typo fails fast.
const ( // InvocationKindForge fires on a forge webhook event (PR/MR open, push). InvocationKindForge InvocationKind = "forge" // InvocationKindCommand fires on a /slash-command in a PR/MR/issue comment. InvocationKindCommand InvocationKind = "command" // InvocationKindSchedule fires on a cron tick (advisory suggested_cron the // Integrations UI proposes; iterion's cloud scheduler owns firing). InvocationKindSchedule InvocationKind = "schedule" // InvocationKindBoard marks the bot as a dispatcher target: an issue whose // Bot == this bot's name is picked up and run. No payload. InvocationKindBoard InvocationKind = "board" // InvocationKindKeepalive runs the bot always-on: a fresh, individually // budgeted run is relaunched every `interval` with at-most-one-live // semantics (a stale run is reaped, not stacked). Sub-minute cadence // requires the resident in-process scheduler (host crontab floors at 1m). // The bot's own supervisor: block, if any, attaches per launched run. InvocationKindKeepalive InvocationKind = "keepalive" )
type InvocationSchedule ¶
type InvocationSchedule struct {
// SuggestedCron is a 5-field cron expression the Integrations UI
// proposes as a default. Advisory — the operator picks the final
// schedule; iterion's cloud scheduler (pkg/cloudsched) owns firing.
SuggestedCron string `yaml:"suggested_cron,omitempty" json:"suggested_cron,omitempty"`
// DefaultVars are vars stamped on each scheduled run.
DefaultVars map[string]string `yaml:"default_vars,omitempty" json:"default_vars,omitempty"`
}
InvocationSchedule is the payload of a kind=schedule invocation.
type Kind ¶
type Kind int
Kind discriminates how a workflow path was supplied.
const ( // KindBot is a plain `.bot` source file. KindBot Kind = iota // KindBundle is a `.botz` archive (ZIP; older bundles: tar.gz). KindBundle // KindBundleDir is a directory whose root already contains a // recognised bundle layout (`main.bot` at the top). // Useful for dev workflows that author bundles in-place. KindBundleDir )
type LaunchHints ¶ added in v0.50.0
type LaunchHints struct {
// Primary lists the bot inputs to surface top-level, in this order.
Primary []string `json:"primary,omitempty" yaml:"primary,omitempty"`
// Hidden lists the bot inputs the launch form never renders.
Hidden []string `json:"hidden,omitempty" yaml:"hidden,omitempty"`
}
LaunchHints is a bot's declared launch-form opinion (manifest `launch:` block), carried by discovery onto the bot entry served at /api/v1/bots so the studio can order and prune the var form.
type Manifest ¶
type Manifest struct {
// Name is the bundle's technical id (falls back to the file stem
// when empty). Distinct from the workflow's own name. Surfaced in
// the studio's bundle picker, `iterion bots list`, and on the run
// header next to the workflow name.
Name string `yaml:"name"`
// DisplayName is the bundle's friendly persona — the name an
// operator actually uses in conversation (e.g. "Nexie" for the
// whats-next bot, "Billy" for some future feature_dev variant).
// Optional and free-form. When set, the studio's RunHeader gilds
// the bot chip with a ✨ icon so dispatcher-spawned runs are
// instantly recognisable by persona, not just by technical name.
// Empty falls back to the Name + WorkflowName pair as before.
DisplayName string `yaml:"display_name,omitempty"`
// Icon is a short emoji identity for the bot (e.g. "🦉"), surfaced
// on catalog cards, the bot home page, and the launch modal. Kept a
// free string (an emoji may be multi-codepoint) but capped at
// maxIconLen bytes at parse time so a manifest can't smuggle prose
// into it. Empty falls back to the studio's persona/hash identity.
Icon string `yaml:"icon,omitempty"`
// Version is a free-form bundle version string (semver or any
// other scheme — the engine does not parse it).
Version string `yaml:"version"`
// Description is a one-line summary surfaced by `iterion inspect`
// and the studio's bundle picker.
Description string `yaml:"description"`
// Author is a free-form attribution string.
Author string `yaml:"author"`
// SchemaVersion identifies the manifest format. Unknown values
// produce a clear error pointing at the user's iterion build.
SchemaVersion int `yaml:"schema_version"`
// Compat is a forward-compatible bag for additive fields. Unknown
// keys here are ignored without breaking loads from newer bundles.
Compat map[string]any `yaml:"compat,omitempty"`
// Attachments declares default values for the workflow's
// `attachments:` block: keys are attachment names, values are
// paths inside the bundle's `attachments/` directory (relative).
// Runtime uploads (Launch modal) override these.
Attachments map[string]string `yaml:"attachments,omitempty"`
// Triggers are free-form labels the orchestrator uses to match
// issues to this bundle (e.g. "refactor", "feature_request").
// Consumed by `iterion bots list` to build the bot catalog;
// the runtime itself doesn't read them today.
Triggers []string `yaml:"triggers,omitempty"`
// Capabilities lists the host capabilities this bundle expects
// to be granted (e.g. "board.create"). Documentation-only — the
// runtime gates capabilities per node, not per bundle.
Capabilities []string `yaml:"capabilities,omitempty"`
// WhenToUse is the orchestrator-facing "use when" guidance for this
// bot — the same role as the "when to use it" block in a Claude Code
// skill. Free text, may be multi-line. Surfaced verbatim in the
// generated iterion-bot-catalog "Use when" card that Nexie reads to
// route a task to a bot. Optional; an empty value drops the card.
// Edited via the studio Bot-metadata panel.
WhenToUse string `yaml:"when_to_use,omitempty"`
// DispatchVars maps the issue into THIS bot's input vars when the
// dispatcher runs it (e.g. {"feature_prompt": "{{issue.title}}\n\n
// {{issue.body}}"} for feature-dev, {"scope_notes": "…"} for a
// reviewer). Values are dispatcher var templates ({{issue.*}}),
// rendered per issue; per-ticket bot_args merge on top. This makes
// the per-bot dispatch wiring DISCOVERY-DRIVEN — the stock
// `iterion dispatch` no longer hardcodes a name→vars map; it reads
// this from each discovered bot's manifest, so adding/renaming a bot
// (shipped or custom) needs zero dispatcher-code edits. Optional;
// empty = the bot receives only the global dispatch vars.
DispatchVars map[string]string `yaml:"dispatch_vars,omitempty"`
// Enabled toggles whether this bot is advertised in the catalog
// exposed to orchestrator bots (Nexie). Tri-state on purpose:
// nil → key absent → treated as enabled, so manifests authored
// before the toggle existed stay visible.
// true → explicitly enabled.
// false → explicitly disabled: dropped from the generated catalog
// and not auto-dispatched, but still surfaced by the studio
// so an operator can flip it back on.
// A workspace overlay (.iterion/bot-overrides.yaml) may override this
// per-workspace without editing the manifest — see
// botregistry.ResolveEnabled.
Enabled *bool `yaml:"enabled,omitempty"`
// Forge declares the forge-access requirements this bot needs to be
// auto-provisioned onto a connected repo through the studio's
// Integrations flow. Advisory + discovery-time metadata, like
// DispatchVars — the runtime itself does not read this; the
// auto-provisioning orchestrator (pkg/forge) does, to compute the
// forge webhook events, request the right token-scope subset, and
// create the matching webhooks.Config + bot-secret binding in one
// transaction. Nil when the bot declares no forge ambitions (the
// Integrations "enable on this repo" picker filters those out).
Forge *ForgeRequirements `yaml:"forge,omitempty"`
// Invocations declare HOW this bot can be triggered (forge event,
// /slash-command, schedule, or board pickup) and WHICH execution mode
// each path uses (direct launch vs board-tracked dispatch). Distinct
// from Triggers (free-form advisory catalog labels) and Forge (the
// credential/token-scope requirements): Invocations are the typed,
// machine-read routing contract consumed by the command router
// (pkg/webhooks), the auto-provisioner (pkg/forge), and the cloud
// scheduler (pkg/cloudsched). Empty = the bot is not directly
// triggerable on a repo (orchestrators like Nexie/Evoly). A bundle that
// declares only a legacy forge: block is treated as having the
// synthetic set from SyntheticInvocations.
Invocations []Invocation `yaml:"invocations,omitempty"`
// Repo declares this bot's RUNTIME repository need: whether a run
// should target a git repository, and whether the launch surface may
// offer to CREATE a new one on a connected forge (Appy's "new app,
// new repo" journey). Advisory launch-surface metadata like
// DispatchVars — the runtime only consumes the resolved
// repo_url/repo_ref on the launch spec. It expresses a NEED ("point
// me at a repo"), never a target-repo assumption: catalog bots stay
// repo-agnostic.
Repo *RepoRequirement `yaml:"repo,omitempty"`
// fields of its workspace config file a non-operator may edit through a
// scoped share URL (pkg/configshare), so a share can be minted for THIS
// bot without the operator hand-typing the config file's JSON paths. The
// mint DERIVES the grant's editable/visible paths and config file from
// this block (expanding a {category} placeholder for a per-category
// config), pinning them at mint time — a share can never be minted
// outside the surface the bot committed to git. Advisory declaration like
// Repo/Forge (the runtime never reads it; the config-share mint does).
// A second bot adopts the whole config-share editor by adding this block
// alone — no server or SPA change.
ConfigShare *ConfigShareSpec `yaml:"config_share,omitempty"`
// Launch opinionates the studio launch form for this bot: which
// workflow vars are primary (surfaced top-level, in order) and which
// are hidden (never rendered, still settable via --var). Advisory
// launch-surface metadata like Repo/DispatchVars — the runtime never
// reads it. Names are normalized at parse time (trim, drop empties,
// dedupe) but NOT validated against the workflow's vars block —
// manifests load without the DSL, so an unknown name is a soft
// authoring mistake for the studio to surface, never a load error.
Launch *LaunchHints `yaml:"launch,omitempty"`
}
Manifest is the parsed `manifest.yaml` shipped at the bundle root. All fields are optional except SchemaVersion, which defaults to 1 when omitted (treated as "explicit v1"). Future minor extensions add to Compat without changing SchemaVersion.
func DecodeManifest ¶ added in v0.43.0
DecodeManifest parses + validates manifest bytes without touching the filesystem — the seam generators (pkg/botscaffold) use to hold their rendered manifest to the same bar as a loaded one before writing it.
func LoadManifest ¶
LoadManifest reads and parses a manifest.yaml file. Missing file is not an error (returns nil, nil); only parse or schema errors fail.
func WriteManifest ¶
func WriteManifest(path string, patch ManifestPatch) (*Manifest, error)
WriteManifest applies patch to the manifest.yaml at path, preserving comments, key order, and the original block/flow style of keys it does not touch.
- When path is missing or empty, a minimal manifest is scaffolded (schema_version + the patched keys). This supports first-time authoring; the discovery layer never feeds a non-bundle path here.
- Every nil patch field is left exactly as it was. Every non-nil field overwrites the matching key in place (carrying over any comments attached to the old value); a key that does not yet exist is inserted after `description` (or appended) for readability.
- The rewritten bytes are validated through LoadManifest before an atomic temp+rename, so a structurally-broken or schema-incompatible result aborts without clobbering the original.
Returns the canonical, re-parsed Manifest on success.
func (*Manifest) IsEnabled ¶
IsEnabled reports whether this bot should be advertised in the orchestrator-facing catalog by default. A nil Enabled (key absent from the manifest) is treated as enabled, so bots authored before the toggle existed remain visible. A workspace overlay may still override this — see botregistry.ResolveEnabled.
type ManifestPatch ¶
type ManifestPatch struct {
Name *string
DisplayName *string
// Icon sets the manifest's emoji identity; the empty string clears it
// while keeping the key. Validated (trim + byte cap) by the
// decodeManifest pass WriteManifest runs before committing.
Icon *string
Version *string
Description *string
Author *string
WhenToUse *string
Enabled *bool
// Triggers is nil for "no change"; a non-nil slice (even empty) sets
// the manifest's triggers list. Note: when the bundle's main.bot
// declares its own `## triggers:` frontmatter, discovery overlays it
// over the manifest value (see botregistry.parseBundle).
Triggers *[]string
// Forge is nil for "no change"; a non-nil pointer rewrites the whole
// `forge:` block (forge-access requirements). Reserved for a future
// studio Integrations editor — the value is encoded with its yaml
// tags and re-validated through decodeManifest before the file lands.
Forge *ForgeRequirements
}
ManifestPatch carries the user-editable subset of a Manifest for WriteManifest. A nil pointer field means "leave this key untouched" (preserving its existing value, comments, and YAML style); a non-nil pointer sets the key, where the empty string is a valid value that clears it while keeping the key present.
type PackResult ¶
type PackResult struct {
OutputPath string // absolute path of the .botz file
Hash string // SHA-256 of the logical bundle content — matches Bundle.Hash on Open
Entries int // number of archive entries written (files + directories)
BytesIn int64 // sum of uncompressed file bytes
BytesOut int64 // size of the .botz on disk
}
PackResult summarises a successful PackDir invocation.
func PackDir ¶
func PackDir(srcDir, outPath string) (*PackResult, error)
PackDir creates a .botz ZIP archive at outPath from the contents of srcDir. The bundle layout is the same as accepted by Open / OpenDir: main.bot at the root, plus optional manifest.yaml, skills/, prompts/, presets/, attachments/.
The output is a standard ZIP archive (PK\x03\x04) so a downloaded `.botz` extracts with `unzip` / double-click. Older bundles were gzipped tarballs; Open / ExtractArchive still read those for backward compatibility (format auto-detect via magic bytes).
The archive is deterministic — entries are sorted alphabetically, timestamps pinned, ownership stripped, modes uniformly set — so two PackDir invocations on the same directory tree produce byte-identical output.
The content hash is computed over the LOGICAL bundle content (the sorted sequence of (relative-path, file-bytes)), NOT over the container bytes. It is therefore independent of the archive format: the same files yield the same hash whether packed as ZIP or read back from a legacy tar.gz bundle. This keeps cache keys and persisted run hashes stable across the format migration.
Returns an error when:
- srcDir is not a directory
- srcDir contains no main.bot at root
- any entry is a symlink, device, or non-regular file
- outPath already exists (use --force at the CLI layer to overwrite)
func PackTree ¶ added in v0.43.0
func PackTree(srcDir, outPath string) (*PackResult, error)
PackTree is PackDir without the bot-bundle layout requirement: it packs ANY directory tree into the same deterministic ZIP (sorted entries, pinned timestamps, symlinks refused, same skip rules and content hash). Used for non-bot archives — e.g. the marketplace serving a plugin's source tree as a downloadable ZIP.
type PresetSpec ¶
type PresetSpec struct {
// Name is the preset id, selected via `--preset <name>`. Defaults to
// the file stem unless the frontmatter `name:` overrides it.
Name string
// DisplayName is the operator-facing label (e.g. "Improve Quality
// (SRE)"). Optional; the studio falls back to Name.
DisplayName string
// Description is a one-line summary for the studio Launch picker.
Description string
// Vars are variable overrides applied to the run with precedence
// defaults < preset < --var. Values are YAML-native (string / bool /
// int / float); the engine coerces each to the declared var's type and
// silently drops keys the workflow doesn't declare, exactly like a
// stray --var.
Vars map[string]any
// Skills lists bundle skill names this preset makes relevant (e.g.
// "lang-js-fallow"). Every bundle skill is mirrored into the workspace
// regardless; this list is surfaced as a hint in the run-time "## Focus"
// prompt section and in the studio.
Skills []string
// Prompt is the markdown body: the bias appended to every LLM node's
// system prompt at run time. Supports `{{vars.X}}` template refs,
// resolved per node. Empty for a var-only preset.
Prompt string
}
PresetSpec is a file-based preset parsed from a bundle's presets/<name>.md (YAML frontmatter + markdown body). It is the on-disk authoring form of a "sous-bot": a named launch-time specialization that layers variable overrides, a system-prompt bias, and relevant skill hints onto an existing bot. The runtime converts it into an ir.Preset — the bundle package stays decoupled from pkg/dsl/ir.
func LoadPresets ¶
func LoadPresets(dir string) ([]PresetSpec, []error)
LoadPresets reads every presets/<name>.md file under dir and returns the parsed presets sorted by name. dir is a bundle's PresetsDir; an empty or missing dir returns nil without error. A single file that fails to parse is skipped and its error collected in the second return value, so one malformed preset never blocks the rest of the bundle's presets.
type RepoRequirement ¶ added in v0.50.0
type RepoRequirement struct {
// Mode is "required" (launch soft-blocks without a target repo),
// "optional" (section offered, skippable), or "none" (explicit
// repo-independence — same as omitting the block; kept so a bot can
// document the choice).
Mode string `yaml:"mode" json:"mode"`
// AllowCreate offers "create a new repository" (forge RepoCreator)
// next to "attach an existing one".
AllowCreate bool `yaml:"allow_create,omitempty" json:"allow_create,omitempty"`
// Purpose is a one-line operator-facing explanation of what the bot
// does with the repo, shown under the section title.
Purpose string `yaml:"purpose,omitempty" json:"purpose,omitempty"`
// DefaultBranch seeds a created repo's default branch name (empty =
// the forge's default).
DefaultBranch string `yaml:"default_branch,omitempty" json:"default_branch,omitempty"`
// Visibility seeds a created repo's visibility: "private" (the
// default) or "public".
Visibility string `yaml:"visibility,omitempty" json:"visibility,omitempty"`
}
RepoRequirement is a bot's declared repository need, rendered by the launch surfaces as a "Target repository" section (active repo preselected → other connected repo → create new → none).