Documentation
¶
Overview ¶
Package updateflow implements the cinematic 5-stage `bonsai update` flow.
Rail:
探 DISCOVER 択 SELECT 同 SYNC 衝 CONFLICT 結 YIELD
Stages are either on-rail (DISCOVER / SELECT / SYNC / YIELD) or off-rail (CONFLICT — chromeless, spliced lazily when the Sync step surfaces a conflict list). Every chrome primitive is imported from initflow — the package never reimplements header/footer/rail. Plan 31 Phase F.
Index ¶
- Constants
- Variables
- func NewSyncStage(ctx initflow.StageContext, action initflow.GenerateAction) *initflow.GenerateStage
- type AgentDiscoveries
- type ConflictsStage
- type DiscoverStage
- func (s *DiscoverStage) Discoveries() []AgentDiscoveries
- func (s *DiscoverStage) HasValidDiscoveries() bool
- func (s *DiscoverStage) Init() tea.Cmd
- func (s *DiscoverStage) Reset() tea.Cmd
- func (s *DiscoverStage) Result() any
- func (s *DiscoverStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)
- func (s *DiscoverStage) View() string
- type FlowInputs
- type Result
- type SelectStage
- func (s *SelectStage) Chromeless() bool
- func (s *SelectStage) Init() tea.Cmd
- func (s *SelectStage) Reset() tea.Cmd
- func (s *SelectStage) Result() any
- func (s *SelectStage) SelectedKeys() map[string][]string
- func (s *SelectStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)
- func (s *SelectStage) View() string
- type YieldInputs
- type YieldStage
Constants ¶
const ( StageIdxDiscover = 0 StageIdxSelect = 1 StageIdxSync = 2 StageIdxYield = 3 // StageIdxOffRail matches addflow's sentinel — off-rail stages suppress // the rail row in renderFrame (negative rail index). StageIdxOffRail = -1 )
Stage indices in the update-flow rail. Kept as named constants so splicer logic references them by name rather than magic integers. The Conflict stage renders off-rail (StageIdxOffRail) but still lives as a real step in the harness list when wr.HasConflicts() is true.
Variables ¶
var StageLabels = []initflow.StageLabel{
{Kanji: "探", Kana: "さがす", English: "DISCOVER"},
{Kanji: "択", Kana: "えらぶ", English: "SELECT"},
{Kanji: "同", Kana: "どう", English: "SYNC"},
{Kanji: "結", Kana: "むすぶ", English: "YIELD"},
}
StageLabels holds the four on-rail update-flow labels in order. The Conflict stage has its own off-rail label rendered inside its body (see conflicts.go).
Functions ¶
func NewSyncStage ¶
func NewSyncStage(ctx initflow.StageContext, action initflow.GenerateAction) *initflow.GenerateStage
NewSyncStage wraps initflow.GenerateStage with the update-flow rail labels and body title. Renders on-rail at StageIdxSync (rail position 2) so the user sees the active SYNC checkpoint while the generator pipeline runs.
action follows the standard GenerateAction contract — a one-shot func() error that performs the write pipeline. The update-flow caller passes a closure that applies user-selected discoveries, runs the AgentWorkspace / PathScopedRules / WorkflowSkills / SettingsJSON / WriteCatalogSnapshot pipeline, and returns errors.Join of any failures.
Types ¶
type AgentDiscoveries ¶
type AgentDiscoveries struct {
AgentName string // machine name (cfg.Agents key)
AgentLabel string // display name (falls back to AgentName)
Installed *config.InstalledAgent // pointer — mutated downstream when user accepts
Valid []generate.DiscoveredFile // user-selectable rows
Invalid []generate.DiscoveredFile // surfaced as warnings, not user-selectable
}
AgentDiscoveries bundles the scan result for a single installed agent. Valid files are user-promotable (clean frontmatter); Invalid files are surfaced as warnings inside the Discover stage panel.
type ConflictsStage ¶
ConflictsStage is the chromeless full-screen conflict-resolution surface (Plan 27 PR2 §C1-C5). Rendered only when the Grow action produced at least one ActionConflict file — the harness gates construction on wr.HasConflicts(), so this stage never instantiates for clean writes.
Layout: one row per conflict file in a vertical list. Each row carries:
[focus glyph] [action glyph · coloured by pick] [relative path] [action label]
Below the list a batch-resolve row renders three cells (Keep all / Overwrite all / Backup all) that apply the chosen action to every row via uppercase K/O/B.
The stage is chromeless — View() returns the full AltScreen frame without the header/enso-rail/footer chrome used by the four on-rail stages. The rail visible to the user (anchored on OBSERVE) stays unchanged while the conflict picker runs — no rail churn between Observe and Conflict.
Keystrokes (plan-27 PR2 §C2 + §C4):
- ↑ ↓ / j k move focus row (no wrap)
- 1 / 2 / 3 set focused row's action to Keep / Overwrite / Backup
- ␣ cycle focused row's action (Keep → Overwrite → Backup → Keep)
- K / O / B batch-resolve — apply action to every row
- ↵ advance (complete stage)
- shift+tab / esc back to Observe (harness pops cursor)
Result: map[string]config.ConflictAction keyed by FileResult.RelPath — one entry per conflict file. applyCinematicConflictPicks in cmd/add.go reads the map and dispatches per-file mutations.
func NewConflictsStage ¶
func NewConflictsStage(ctx initflow.StageContext, wr *generate.WriteResult) *ConflictsStage
NewConflictsStage constructs the stage over the conflict entries present in wr. When wr has zero conflicts the ctor still returns a usable stage — it simply renders an empty body with a single "nothing to reconcile" line and completes on Enter. Callers should gate on wr.HasConflicts() before splicing.
func (*ConflictsStage) Chromeless ¶
func (s *ConflictsStage) Chromeless() bool
Chromeless reports true so the harness yields View() verbatim without its default header/footer. Embedded initflow.Stage.Chromeless() already returns true, but ConflictsStage declares the method explicitly so the contract is obvious at call-sites that type-scan for Chromeless.
func (*ConflictsStage) Init ¶
func (s *ConflictsStage) Init() tea.Cmd
Init implements tea.Model — no cmd on entry.
func (*ConflictsStage) Reset ¶
func (s *ConflictsStage) Reset() tea.Cmd
Reset clears the completion flag but preserves per-file picks and focus so Esc-back → re-entry reads exactly where the user left off.
func (*ConflictsStage) Result ¶
func (s *ConflictsStage) Result() any
Result returns the per-file ConflictAction map. Keys are RelPath values; every conflict entry is guaranteed to appear in the map (default Keep if untouched).
func (*ConflictsStage) Update ¶
Update handles focus movement + per-row action + batch-resolve + advance + back.
func (*ConflictsStage) View ¶
func (s *ConflictsStage) View() string
View returns the full AltScreen frame. Chromeless — no header/rail/footer. Mirrors initflow.PlantedStage.View for layout parity: centre vertically, compose title + divider + list + batch row + inline key hints.
type DiscoverStage ¶
DiscoverStage is the first on-rail stage — it runs a read-only custom-file scan across every installed agent, splits the hits into valid (promotable) and invalid (frontmatter-missing) groups, and renders a preview panel so the user sees exactly what the flow is about to offer before committing.
The stdout-warning pre-harness code from the legacy cmd/update.go:68-75 is moved into this stage's "warnings" body row — no stdout churn before the AltScreen paint, and the user gets the context inside the cinematic frame.
Result: []AgentDiscoveries — one entry per agent with any discovery (valid or invalid). Empty slice when the workspace is in sync with the catalog; the downstream Sync stage handles that as a no-op.
func NewDiscoverStage ¶
func NewDiscoverStage(ctx initflow.StageContext, cwd string, cfg *config.ProjectConfig, cat *catalog.Catalog, lock *config.LockFile) *DiscoverStage
NewDiscoverStage constructs the Discover stage. The scan is deferred to the first Init call so the stage's ctor remains cheap.
func (*DiscoverStage) Discoveries ¶
func (s *DiscoverStage) Discoveries() []AgentDiscoveries
Discoveries returns the scan result. Exposed for tests + downstream stages that consume the payload without going through the harness Result() path (e.g. the Sync stage reads this pointer directly).
func (*DiscoverStage) HasValidDiscoveries ¶
func (s *DiscoverStage) HasValidDiscoveries() bool
HasValidDiscoveries reports whether any agent has at least one valid (user-selectable) custom file. Used by the splicer to gate the Select stage — no selections ⇒ jump straight to Sync.
func (*DiscoverStage) Init ¶
func (s *DiscoverStage) Init() tea.Cmd
Init implements tea.Model — no cmd on entry.
func (*DiscoverStage) Reset ¶
func (s *DiscoverStage) Reset() tea.Cmd
Reset preserves the scan result; Esc-back redisplays the same findings without re-running the filesystem walk.
func (*DiscoverStage) Result ¶
func (s *DiscoverStage) Result() any
Result returns the discovery bundle. The downstream Select stage reads this through a SetDiscoveries shim rather than the harness prev[] slot to avoid the type-erasure cost; Result() is implemented for tests.
func (*DiscoverStage) Update ¶
Update handles Enter-to-advance. Esc is consumed by the harness root.
func (*DiscoverStage) View ¶
func (s *DiscoverStage) View() string
View composes the Discover stage body inside the shared frame.
type FlowInputs ¶
type FlowInputs struct {
Cwd string
Version string
Cfg *config.ProjectConfig
Cat *catalog.Catalog
Lock *config.LockFile
}
FlowInputs carries the shared dependencies runUpdate passes into the flow. Kept as a struct so the splicer closures can capture by pointer once rather than threading individual args across every stage ctor.
type Result ¶
type Result struct {
// ConfigChanged is true when at least one user-accepted discovery
// mutated the project config (installed.Skills/Workflows/... grew).
// Triggers a cfg.Save post-run.
ConfigChanged bool
// WriteResult is the filesystem snapshot populated by the Sync
// action's generator calls. Post-run consumers read it for conflict
// dispatch and the YIELD success panel's counts.
WriteResult *generate.WriteResult
// SyncErr, when non-nil, is the aggregated error surfaced by Sync.
// The YIELD stage renders an error panel instead of the normal
// success card.
SyncErr error
// Cancelled is true when the user Ctrl-C'd. Caller skips persistence.
Cancelled bool
}
Result is the outcome payload returned from Run. Caller applies any post-flow persistence (cfg.Save / lock.Save) — the flow itself writes through generate.* pathways but leaves final config serialization to the command layer so `cmd/update.go` keeps full control.
func Run ¶
func Run(cwd string, cfg *config.ProjectConfig, cat *catalog.Catalog, lock *config.LockFile, version string) (Result, error)
Run drives the cinematic 5-stage `bonsai update` flow end-to-end. The caller (cmd/update.go) retains responsibility for config/lock persistence — Run just orchestrates the rail and reports the outcome.
Stage order:
Discover (rail 0) → Select (rail 1, chromeless, gated on valid discoveries) → Sync (rail 2) → Conflict (off-rail, gated on wr.HasConflicts()) → Yield (rail 3)
Returns a populated Result even on Ctrl-C so callers can decide on persistence based on Cancelled and SyncErr fields.
func RunStatic ¶
func RunStatic(cwd string, cfg *config.ProjectConfig, cat *catalog.Catalog, lock *config.LockFile, version string) (Result, error)
RunStatic is the non-TTY fallback. Auto-accepts every valid discovery, runs the sync pipeline, and surfaces conflicts as a returned error (no interactive picker). The caller handles persistence identically to the interactive path based on the returned Result.
Invalid discoveries (frontmatter missing/malformed) are surfaced as `warning: <relPath> — <Error>` lines on stderr — they don't fail the run, but CI/non-TTY callers get the signal that's otherwise dropped silently from RunStatic's filter loop.
type SelectStage ¶
SelectStage is a chromeless per-agent multi-select. Each agent with valid discoveries becomes a tab; within a tab each row is a file that can be toggled on/off. All rows default to selected (mirrors legacy huh.Selected(true) default from the pre-cinematic flow).
Keystrokes:
←→/h/l cycle agent tabs ↑↓/j/k move focus within the current tab ␣ toggle the focused file a toggle all files in current tab ↵ advance
Result: map[string][]string — keyed by AgentName, value is the list of selected "type:name" keys (matches legacy buildCustomFileOptions Value shape so applyCustomFileSelection can consume the output unchanged).
func NewSelectStage ¶
func NewSelectStage(ctx initflow.StageContext, agents []AgentDiscoveries) *SelectStage
NewSelectStage constructs the chromeless per-agent multi-select. agents should contain ONLY entries with at least one valid discovery — the caller filters out invalid-only agents upstream.
func (*SelectStage) Chromeless ¶
func (s *SelectStage) Chromeless() bool
Chromeless reports true so the harness yields the stage's View verbatim. Plan 31 §F §4 — chromeless per-agent tab strip.
func (*SelectStage) Init ¶
func (s *SelectStage) Init() tea.Cmd
Init implements tea.Model — no cmd on entry.
func (*SelectStage) Reset ¶
func (s *SelectStage) Reset() tea.Cmd
Reset preserves focus + selection across Esc-back / re-entry.
func (*SelectStage) Result ¶
func (s *SelectStage) Result() any
Result returns the SelectedKeys map so the harness can expose the picks to downstream stages / post-harness callers via prev[].
func (*SelectStage) SelectedKeys ¶
func (s *SelectStage) SelectedKeys() map[string][]string
SelectedKeys returns the per-agent list of selected "type:name" keys. Caller-side applyCustomFileSelection consumes this directly.
func (*SelectStage) Update ¶
Update handles tab cycling + in-tab focus + space toggle + enter advance.
func (*SelectStage) View ¶
func (s *SelectStage) View() string
View renders the chromeless body — vertically-centred inside AltScreen, same pattern as addflow.ConflictsStage.
type YieldInputs ¶
type YieldInputs struct {
WriteResult *generate.WriteResult
ConfigChanged bool
SyncErr error
HintBlock hints.Block
}
YieldInputs carries the data every yield variant needs at ctor time.
type YieldStage ¶
YieldStage is the terminal completion card at rail position 3 (結 YIELD). Three variants, all chromeless — matches addflow.YieldStage presentation so the update flow lands its exit with the same typographic beat.
func NewYieldStage ¶
func NewYieldStage(ctx initflow.StageContext, inputs YieldInputs) *YieldStage
NewYieldStage constructs the Yield stage, auto-selecting the variant based on the inputs.
- SyncErr != nil → yieldModeError
- no changes + no conflicts → yieldModeUpToDate
- otherwise → yieldModeSynced
Caller builds inputs from the cross-stage flow state — Sync's WriteResult + configChanged flag, any error bubbled up, and the pre-computed hints block.
func (*YieldStage) Chromeless ¶
func (s *YieldStage) Chromeless() bool
Chromeless reports true — YieldStage renders its own centred exit card without the enso rail.
func (*YieldStage) Init ¶
func (s *YieldStage) Init() tea.Cmd
Init implements tea.Model — no cmd on entry.
func (*YieldStage) Reset ¶
func (s *YieldStage) Reset() tea.Cmd
Reset clears the completion flag so re-entry renders fresh.
func (*YieldStage) View ¶
func (s *YieldStage) View() string
View renders the exit card. Mirrors addflow.YieldStage — chromeless, vertically-centred body with inline hint row below.