Help us shape the future of Go! Take the 2026 Go Developer Survey

updateflow

package
v0.4.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MIT Imports: 16 Imported by: 0

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

View Source
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

View Source
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

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

type ConflictsStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

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

func (s *ConflictsStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

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

type DiscoverStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

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

func (s *DiscoverStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

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

type SelectStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

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

func (s *SelectStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

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

type YieldStage struct {
	initflow.Stage
	// contains filtered or unexported fields
}

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) Result

func (s *YieldStage) Result() any

Result returns nil — terminal stage.

func (*YieldStage) Update

func (s *YieldStage) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update waits for ↵ / q / esc acknowledgement.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL