flowstore

package
v0.10.8 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package flowstore persists task-centric Flow records beside the agent-session store.

Agents persist Flow changes through the `approach flow` CLI (cmd/approach/flow.go), never by editing approach.db — direct edits bypass the store's transactions, validation, and phase-ID normalization.

SQLite schema versions: v3 adds prepared_at + receipt trigger; v4 requires boolean `done` in epic progression JSON; v5 fences the progression-claim marker; v6 adds the protected flows.preparation_nonce generation projection; v7 fences recovered-launch capabilities from older writers; v8 projects and fences the durable phase-untracked owner identity.

Index

Constants

View Source
const (
	TierMissing      = "missing"
	TierOpen         = "open"
	TierNotWritable  = "not_writable"
	TierMalformed    = "malformed"
	TierNotADatabase = "not_a_database"
	TierHeader       = "header"
)

The six tiers Inspect can land in. They are values, not step indexes: two separate conditions report not_a_database and two report not_writable, so a caller discriminates on Reason.

View Source
const (
	PhaseResetReasonAwaitSession = "await-session"
	PhaseResetReasonEndedSession = "ended-session"
)
View Source
const (
	StatusPending        = "pending"
	StatusInProgress     = "in_progress"
	StatusNeedsAttention = "needs_attention"
	StatusBlocked        = "blocked"
	StatusCompleted      = "completed"
	StatusMerged         = "merged"
	StatusAbandoned      = "abandoned"
	StatusClosed         = "closed"
)
View Source
const (
	MergePending = "pending"
	MergeMerged  = "merged"
	MergeBlocked = "blocked"
)
View Source
const (
	OutcomeApproved             = "approved"
	OutcomeApprovedWithConcerns = "approved_with_concerns"
	OutcomeChangesRequested     = "changes_requested"
	OutcomeBlocked              = "blocked"
)
View Source
const (
	GraphRecoveryPresetEdgesRestored    = "preset_edges_restored"
	GraphRecoveryMissingEdgesUnresolved = "missing_edges_unresolved"
)
View Source
const (
	OutcomePhaseResultMissing = "phase_result_missing"
	OutcomePhaseResultStale   = "phase_result_stale"
)

Variables

View Source
var (
	// ErrPreparationIncomplete means authoritative state confirms that no
	// protected preparation receipt was persisted.
	ErrPreparationIncomplete = errors.New("flow preparation is incomplete")
	// ErrPreparationUnknown means a failed persistence attempt could not be
	// reconciled by an authoritative read.
	ErrPreparationUnknown = errors.New("flow preparation outcome is unknown")
	// ErrPreparationStale means the Flow ID now names a different preparation
	// generation, so callers must not compensate the returned replacement.
	ErrPreparationStale = errors.New("flow preparation generation is stale")
	// ErrPreparationReservation means Compensate could not acquire the
	// launch/close reservation, so the one-shot finalizer was not consumed and
	// remains retryable.
	ErrPreparationReservation = errors.New("flow preparation reservation is unavailable")
)
View Source
var (
	ErrRestoreBackupUnusable     = errors.New("flow database backup is not usable")
	ErrRestoreBlockedByOwners    = errors.New("flow database restore blocked by a live owner")
	ErrRestoreGenerationMismatch = errors.New("flow database backup is from a different generation")
)

The refusals Restore returns. Each is typed because each has a different remedy: fix or re-take the backup, close the named processes, or acknowledge with --force.

View Source
var ErrAutoLaunchOutdated = errors.New("auto launch outdated")

ErrAutoLaunchOutdated is the sentinel every outdated-auto-launch rejection wraps. It is exported so callers outside this package can build the rejection their AutoMode handling has to survive.

View Source
var ErrBeadFlowActive = errors.New("bead already has an active flow")

ErrBeadFlowActive is the sentinel every duplicate Bead-linked creation refusal wraps.

View Source
var ErrBeadFlowUnreadable = errors.New("bead may have an unreadable flow")

ErrBeadFlowUnreadable is the sentinel for a refusal caused by a row that claims the requested Bead but whose stored record cannot be decoded.

View Source
var ErrDatabaseGenerationChanged = errors.New("flow database changed generation underneath this handle")

ErrDatabaseGenerationChanged marks a handle whose database was replaced or migrated underneath it.

Distinct from every other write failure because the remedy is distinct: the request was fine, the database is fine, and this PROCESS is holding a handle on state that no longer exists. Retrying cannot help, and a caller that treated it as a transient error would loop.

View Source
var ErrEpicProgressionActivationChanged = errors.New("epic progression activation changed")

ErrEpicProgressionActivationChanged reports that a progression edge was observed under an activation other than the one currently stored.

View Source
var ErrFlowClosed = errors.New("flow is closed")

ErrFlowClosed is the sentinel every closed-Flow mutation refusal wraps. A reservation reports a closed Flow and an unreadable one through the same error return, and only the first may block a caller: callers that must stay permissive when a Flow is missing test for this rather than for any error.

View Source
var ErrFlowNotFound = errors.New("flow not found")

ErrFlowNotFound is the sentinel every missing-record error wraps. It is exported alongside IsNotFound so callers and their test doubles can build the same error the store returns.

View Source
var ErrFlowUntrackedOwned = errors.New("flow worktree is owned by a phase-untracked launch")

ErrFlowUntrackedOwned reports that a live or reserved phase-untracked launch already owns the Flow worktree.

View Source
var ErrMigrationBlockedByOwners = errors.New("flow database migration blocked by a live owner")

ErrMigrationBlockedByOwners marks a migration refused because a long-lived process is holding this database open at a build that could not read the result.

Distinguishable from every other refusal because the remedy is different and entirely in the operator's hands: nothing is wrong with the database, and the action is to close the named processes.

View Source
var ErrUntrackedOwnerChanged = errors.New("phase-untracked Flow owner changed")

ErrUntrackedOwnerChanged reports that an identity-fenced owner mutation no longer names the persisted launch.

Functions

func AgentSettablePhaseStatuses

func AgentSettablePhaseStatuses() []string

AgentSettablePhaseStatuses returns the canonical list of statuses agents may set on a Flow phase, in canonical order.

func AllowedNextPhaseStatuses

func AllowedNextPhaseStatuses(current string) []string

AllowedNextPhaseStatuses returns the canonical agent-settable statuses a phase may transition to from current, in canonical order. It returns nil for unknown statuses. Same-status updates are idempotent no-ops handled separately and are not listed.

func BeadFlowSlotOccupied added in v0.10.6

func BeadFlowSlotOccupied(record FlowRecord) bool

BeadFlowSlotOccupied reports whether record holds its repository+Bead slot.

A Flow occupies the slot for its Bead unless it reached a terminal status: completed, merged, abandoned, and closed release it, because a follow-up Flow for the same Bead is then legitimate. Everything else — including blocked and needs_attention — holds it, because those are the states where a human is meant to intervene on the existing Flow rather than fork a second one.

Occupancy is a function of the derived status alone, deliberately including receipt-less preparations: PreparationNonce is never consulted. A prepared Flow holds its slot because seeded phases derive pending, not because it lacks a receipt — and nothing clears the nonce on close, so exempting receipt-less records from the terminal cases would make the documented remedy ("close it from the Flows view with C, then press f/F") leave the Bead occupied forever. Creating a Flow whose supplied phases already derive terminal is not a production path; if it were, releasing the slot is what the terminal rule asks for.

DeriveStatus returns exactly eight values, so the switch below is total.

func DatabaseSchemaVersion added in v0.10.5

func DatabaseSchemaVersion() int

DatabaseSchemaVersion is the physical flow database schema this build writes. It is exported so launch and diagnostic surfaces can report the schema an agent's binary must be able to write without importing store internals.

func DefaultRoot

func DefaultRoot() (string, error)

DefaultRoot returns the default artifact root, matching sessions and plans.

func DeriveStatus

func DeriveStatus(record FlowRecord) string

DeriveStatus computes the flow-level status from phase and merge state.

func EffectiveAutoMerge added in v0.10.8

func EffectiveAutoMerge(record FlowRecord, global bool) bool

EffectiveAutoMerge resolves the per-Flow override against the global setting.

func FlowClosed added in v0.10.1

func FlowClosed(record FlowRecord) bool

FlowClosed reports whether a Flow was deliberately closed.

func HasIssueTarget

func HasIssueTarget(issue Issue) bool

HasIssueTarget reports whether issue metadata contains enough target context to open the issue in a browser.

func HasPRTarget

func HasPRTarget(pr PullRequest) bool

HasPRTarget reports whether PR metadata contains enough target context for downstream Autoreview work.

func IsAutoLaunchOutdated

func IsAutoLaunchOutdated(err error) bool

IsAutoLaunchOutdated reports whether err means an automatic launch request lost its race with newer Flow state and should be ignored.

func IsBeadFlowActive added in v0.10.6

func IsBeadFlowActive(err error) bool

IsBeadFlowActive reports the duplicate refusal.

func IsBeadFlowRefusal added in v0.10.6

func IsBeadFlowRefusal(err error) bool

IsBeadFlowRefusal reports whether the Bead-slot guard refused a creation, in either of its two forms. Both refusals are raised before anything is written, so a caller can use this to distinguish "nothing was created" from a Flow that exists but failed later preparation.

func IsBeadFlowUnreadable added in v0.10.6

func IsBeadFlowUnreadable(err error) bool

IsBeadFlowUnreadable reports the unreadable-candidate refusal.

func IsFlowClosed added in v0.10.1

func IsFlowClosed(err error) bool

IsFlowClosed reports whether err is a refusal to mutate or launch a closed Flow, as opposed to a missing record or a failed read.

func IsMigrationBlockedByOwners added in v0.10.6

func IsMigrationBlockedByOwners(err error) bool

IsMigrationBlockedByOwners reports whether err is the owners-lease refusal. Like the compatibility refusals it must stay distinguishable: the database is healthy, so any advice about moving it aside or restoring a backup would be a wrong diagnosis.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err means the requested Flow record does not exist.

func IsPreparationIncomplete added in v0.10.2

func IsPreparationIncomplete(err error) bool

IsPreparationIncomplete reports a confirmed receipt-less outcome.

func IsPreparationReservation added in v0.10.3

func IsPreparationReservation(err error) bool

IsPreparationReservation reports that Compensate could not acquire the launch/close reservation and therefore left the one-shot finalizer usable.

func IsPreparationStale added in v0.10.2

func IsPreparationStale(err error) bool

IsPreparationStale reports that the Flow ID now names another preparation generation and is therefore unsafe to mutate as compensation.

func IsPreparationUnknown added in v0.10.2

func IsPreparationUnknown(err error) bool

IsPreparationUnknown reports a persistence outcome that could not be read authoritatively after a failed receipt write.

func IsPreparedEpicProgressionCommitUnknown added in v0.10.2

func IsPreparedEpicProgressionCommitUnknown(err error) bool

IsPreparedEpicProgressionCommitUnknown reports that an atomic enable reached commit but SQLite could not confirm whether that commit became durable.

func IsSchemaCompatibilityRefusal added in v0.10.6

func IsSchemaCompatibilityRefusal(err error) bool

IsSchemaCompatibilityRefusal reports whether err means "this binary must not touch this database": the database was written by a newer build, or this opener's role forbids the migration it needs. Both are refusals about the pairing of build and database, not about the request, so a caller that can defer its write to a compatible process (the launch controller's spool path) should do that rather than fail. Every other error — a locked file, a corrupt page, a typo'd root — is not one of these.

func LatestPhaseLaunchID

func LatestPhaseLaunchID(phase FlowPhase) string

func PhaseAwaitingSession

func PhaseAwaitingSession(phase FlowPhase) bool

PhaseAwaitingSession reports whether the newest phase launch has not attached any session record yet. A malformed attached record is handled separately by callers as missing session metadata.

func PhaseGateSatisfied

func PhaseGateSatisfied(record FlowRecord, phase FlowPhase) bool

PhaseGateSatisfied reports whether phase satisfies the semantic gate that unlocks downstream phases in the Flow graph.

func PhaseGraphLaunchEligible added in v0.10.3

func PhaseGraphLaunchEligible(record FlowRecord, orderedIndex int) bool

PhaseGraphLaunchEligible is PhaseLaunchEligible without the preparation receipt fence. Create-time root snapshots and compensation still need the graph-ready set while a nonce-bearing Flow is receipt-less.

func PhaseIsActionable added in v0.10.1

func PhaseIsActionable(phase FlowPhase) bool

PhaseIsActionable reports whether a phase is one the Flow is currently sitting on: ready, running, needs_attention, or blocked. It is deliberately broader than "executing" — blocked and needs-attention phases are actionable because they are where a human or agent must intervene next.

func PhaseLatestLaunchEnded

func PhaseLatestLaunchEnded(phase FlowPhase) bool

PhaseLatestLaunchEnded reports whether the newest phase launch attached only ended session records. It ignores older launch history.

func PhaseLaunchEligible

func PhaseLaunchEligible(record FlowRecord, orderedIndex int) bool

PhaseLaunchEligible reports whether the phase at orderedIndex can be offered for launch. It is index-aware so stale duplicate rows from hand-authored records cannot be launched even when their stored status is ready.

func PhasePredecessorsSatisfied

func PhasePredecessorsSatisfied(record FlowRecord, phaseID string) bool

PhasePredecessorsSatisfied reports whether all graph prerequisites for phaseID satisfy the Flow gate rules used to derive downstream readiness.

func PhaseSessionLaunchMismatch

func PhaseSessionLaunchMismatch(phase FlowPhase) bool

PhaseSessionLaunchMismatch reports whether any attached session cannot be matched back to one of the phase launch attempts.

func PhaseStatusTerminal

func PhaseStatusTerminal(status string) bool

PhaseStatusTerminal reports whether a phase has finished (successfully or by being skipped), as opposed to states that still expect agent work.

func PreparationLaunchBlocked added in v0.10.3

func PreparationLaunchBlocked(record FlowRecord) bool

PreparationLaunchBlocked reports that a nonce-bearing preparation has not yet stamped its receipt, so another process must not launch or persist a launch ID. Migrated records without a nonce stay launchable.

func RecoverableRunningPhaseResetReason

func RecoverableRunningPhaseResetReason(phase FlowPhase) (string, bool)

RecoverableRunningPhaseResetReason reports whether a running phase can be reset by approach-owned stale-session recovery.

func ResolvePhaseAgentSettings added in v0.10.1

func ResolvePhaseAgentSettings(prefs agent.Preferences, raw PhaseAgentSettings) (agent.Settings, error)

ResolvePhaseAgentSettings validates a phase's raw persisted stamp before applying field-by-field fallbacks from the current provider preferences. An empty raw stamp follows the globally selected command. A non-empty agent selects that provider's global model and effort, even when another provider is selected globally. The literal "default" is non-empty and therefore remains an explicit provider-default choice.

func SamePreparationIdentity added in v0.10.3

func SamePreparationIdentity(a, b FlowRecord) bool

SamePreparationIdentity reports whether two records name the same receipt-less preparation generation. Prefer the storage-only nonce whenever either record carries one. Compare the legacy PreparationGeneration field only for migrated records that have no nonce.

func ValidatePreset

func ValidatePreset(preset Preset) error

ValidatePreset checks whether preset can seed a Flow phase graph.

Types

type AutoMergeUpdate added in v0.10.8

type AutoMergeUpdate struct {
	FlowID  string
	Enabled *bool
}

AutoMergeUpdate sets a Flow override for automatic merge-phase launches. A nil value inherits the global setting; false and true are explicit policy.

type AutoModeUpdate

type AutoModeUpdate struct {
	FlowID  string
	Enabled bool
}

AutoModeUpdate changes whether the TUI may automatically launch ready phases for a single Flow after successful phase completion.

type BeadFlowActiveError added in v0.10.6

type BeadFlowActiveError struct {
	RepoPath string
	BeadID   string
	Existing FlowRecord
}

BeadFlowActiveError names the Flow that already holds the repository+Bead slot so callers can surface it instead of creating a second Flow.

func (*BeadFlowActiveError) Error added in v0.10.6

func (e *BeadFlowActiveError) Error() string

func (*BeadFlowActiveError) Unwrap added in v0.10.6

func (e *BeadFlowActiveError) Unwrap() error

type BeadFlowUnreadableError added in v0.10.6

type BeadFlowUnreadableError struct {
	RepoPath string
	BeadID   string
	FlowID   string
	Err      error
}

BeadFlowUnreadableError names the Flow whose record could not be decoded, so the caller can point a human at the row that needs repair.

Such a row cannot report a derived status, so the guard cannot tell whether it still holds the Bead slot. Creating anyway is the worse answer: it is precisely how a duplicate Flow and a duplicate worktree get made. Unreadable rows for other Beads remain ignorable, so this never becomes a whole-repository outage.

func (*BeadFlowUnreadableError) Error added in v0.10.6

func (e *BeadFlowUnreadableError) Error() string

func (*BeadFlowUnreadableError) Unwrap added in v0.10.6

func (e *BeadFlowUnreadableError) Unwrap() error
type BeadLink struct {
	ID     string `json:"id"`
	EpicID string `json:"epic_id,omitempty"`
}

BeadLink stores the independent Beads issue associated with a Flow. EpicID identifies the selected issue's parent epic when that relationship is known.

func (*BeadLink) UnmarshalJSON added in v0.10.1

func (link *BeadLink) UnmarshalJSON(data []byte) error

UnmarshalJSON distinguishes an omitted link from a present but incomplete object. The zero value remains valid on pre-Bead records because this method is not called when the containing field is absent.

type ChildPhaseUpdate

type ChildPhaseUpdate struct {
	FlowID        string
	ParentPhaseID string
	PhaseID       string
	Title         string
	Order         int
	Fence         PhaseLaunchFence
}

ChildPhaseUpdate creates or updates a stable child phase under Implementation.

type Closure added in v0.10.1

type Closure struct {
	Reason   string     `json:"reason,omitempty"`
	ClosedAt *time.Time `json:"closed_at,omitempty"`
}

Closure records why and when a Flow was deliberately closed. Derivation keys on ClosedAt, never on Reason, so a record can never drift into closed because of an empty-string comparison.

type ClosureUpdate added in v0.10.1

type ClosureUpdate struct {
	FlowID string
	Reason string
}

ClosureUpdate records a deliberate close of a Flow with its required reason.

type CreateOptions

type CreateOptions struct {
	Preset   *Preset
	Headless *bool

	// PhaseAgent is stamped onto every seeded phase, and onto declared phases
	// that carry no settings of their own.
	PhaseAgent PhaseAgentSettings
	// contains filtered or unexported fields
}

CreateOptions configures Flow creation defaults and phase seeding.

type EpicProgression added in v0.10.2

type EpicProgression struct {
	SchemaVersion int
	RepoPath      string
	EpicID        string
	Enabled       bool
	Done          bool
	Halt          *EpicProgressionHalt
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

EpicProgression is the persisted auto-progression state for one epic.

type EpicProgressionHalt added in v0.10.2

type EpicProgressionHalt struct {
	ChildBeadID string `json:"child_bead_id"`
	Status      string `json:"status"`
	Message     string `json:"message"`
}

EpicProgressionHalt is the durable reason progression stopped.

type EpicProgressionHaltUpdate added in v0.10.5

type EpicProgressionHaltUpdate struct {
	Key                EpicProgressionKey
	Halt               EpicProgressionHalt
	ExpectedActivation time.Time
}

EpicProgressionHaltUpdate records why progression stopped. Only authoritative active state may halt, and the first halt tuple is the one that sticks.

type EpicProgressionKey added in v0.10.2

type EpicProgressionKey struct {
	RepoPath string
	EpicID   string
}

EpicProgressionKey identifies one repository-local Beads epic.

type EpicProgressionSuccessorOutcome added in v0.10.2

type EpicProgressionSuccessorOutcome string

EpicProgressionSuccessorOutcome classifies the authoritative relationship between an already-enabled epic and one prepared sequential successor.

const (
	EpicProgressionSuccessorInactive         EpicProgressionSuccessorOutcome = "inactive"
	EpicProgressionSuccessorReleased         EpicProgressionSuccessorOutcome = "released"
	EpicProgressionSuccessorOwnedObstruction EpicProgressionSuccessorOutcome = "owned_obstruction"
	EpicProgressionSuccessorAccepted         EpicProgressionSuccessorOutcome = "accepted"
	EpicProgressionSuccessorRetryable        EpicProgressionSuccessorOutcome = "retryable"
)

type EpicProgressionSuccessorResult added in v0.10.2

type EpicProgressionSuccessorResult struct {
	Outcome     EpicProgressionSuccessorOutcome
	Progression EpicProgression
	Flow        FlowRecord
}

EpicProgressionSuccessorResult returns the records observed in the same writer transaction that produced Outcome. Flow is empty for inactive and for an absent released Flow.

type EpicProgressionSuccessorUpdate added in v0.10.2

type EpicProgressionSuccessorUpdate struct {
	FlowID             string
	Key                EpicProgressionKey
	Bead               BeadLink
	ExpectedActivation time.Time
}

EpicProgressionSuccessorUpdate names the exact prepared Flow that sequential progression owns. Callers hold its launch/close reservation while reconciling.

type EpicProgressionUpdate added in v0.10.2

type EpicProgressionUpdate struct {
	Key                EpicProgressionKey
	Enabled            bool
	Done               bool
	ExpectedActivation time.Time
}

EpicProgressionUpdate requests one complete target state. Enabling clears done and halt; explicit normal off clears done and retains a sticky halt; done may be newly established only from authoritative active state.

type FlowFilter

type FlowFilter struct {
	RepoPath string
}

FlowFilter narrows records returned by List.

type FlowPhase

type FlowPhase struct {
	PhaseID       string    `json:"phase_id"`
	ParentPhaseID string    `json:"parent_phase_id,omitempty"`
	Title         string    `json:"title"`
	Kind          PhaseKind `json:"kind"`
	// Agent, Model, and ReasoningEffort are optional launch overrides; see
	// PhaseAgentSettings and ResolvePhaseAgentSettings.
	Agent              string               `json:"agent,omitempty"`
	Model              string               `json:"model,omitempty"`
	ReasoningEffort    string               `json:"reasoning_effort,omitempty"`
	DependsOn          []string             `json:"depends_on"`
	Status             PhaseStatus          `json:"status"`
	Order              int                  `json:"order"`
	Outcome            string               `json:"outcome,omitempty"`
	Notes              string               `json:"notes,omitempty"`
	Summary            string               `json:"summary,omitempty"`
	Reconciliation     *PhaseReconciliation `json:"reconciliation,omitempty"`
	LaunchIDs          []string             `json:"launch_ids,omitempty"`
	RecoveredLaunchIDs []string             `json:"recovered_launch_ids,omitempty"`
	Sessions           []Session            `json:"sessions,omitempty"`
	CreatedAt          time.Time            `json:"created_at"`
	UpdatedAt          time.Time            `json:"updated_at"`
}

FlowPhase is one phase in the persisted Flow pipeline.

func FindPhaseByKind

func FindPhaseByKind(record FlowRecord, kind string) (FlowPhase, bool)

FindPhaseByKind returns the first phase with the requested semantic kind.

func FirstLaunchablePhase

func FirstLaunchablePhase(record FlowRecord) (FlowPhase, int, bool)

FirstLaunchablePhase returns the first ordered phase that can be launched.

func NextActionablePhase added in v0.10.1

func NextActionablePhase(record FlowRecord) (FlowPhase, bool)

NextActionablePhase returns the first actionable phase in graph order (OrderedPhases), reporting false when the Flow has none.

func OrderedPhases

func OrderedPhases(phases []FlowPhase) []FlowPhase

OrderedPhases returns phases with child phases grouped directly below their parent, sorting siblings by Order and then phase id. Top-level phase order is otherwise preserved for backward compatibility with existing records.

func (FlowPhase) AgentSettings added in v0.10.1

func (p FlowPhase) AgentSettings() PhaseAgentSettings

AgentSettings returns the agent selection captured for this phase.

type FlowRecord

type FlowRecord struct {
	SchemaVersion int         `json:"schema_version"`
	FlowID        string      `json:"flow_id"`
	Title         string      `json:"title"`
	Instructions  string      `json:"instructions"`
	Status        string      `json:"status"`
	RepoPath      string      `json:"repo_path"`
	WorktreePath  string      `json:"worktree_path,omitempty"`
	Branch        string      `json:"branch,omitempty"`
	BaseRef       string      `json:"base_ref,omitempty"`
	Commit        string      `json:"commit,omitempty"`
	PresetName    string      `json:"preset_name,omitempty"`
	PlanID        string      `json:"plan_id,omitempty"`
	PlanPath      string      `json:"plan_path,omitempty"`
	Bead          BeadLink    `json:"bead,omitzero"`
	Issue         Issue       `json:"issue,omitempty"`
	PR            PullRequest `json:"pr,omitempty"`
	Merge         Merge       `json:"merge,omitempty"`
	Closed        Closure     `json:"closed,omitzero"`
	AutoMode      bool        `json:"auto_mode,omitempty"`
	AutoMerge     *bool       `json:"auto_merge,omitempty"`
	// Headless is the per-Flow manual-launch preference. Like AutoMode, it is
	// forced on at creation and can only be changed afterwards through
	// SetHeadless or CreateOptions.Headless — a value set on a record passed to
	// Create is ignored. It is written without omitempty so an explicit false
	// stays distinguishable from a legacy record that predates the field.
	Headless bool        `json:"headless"`
	Phases   []FlowPhase `json:"phases"`
	// UntrackedOwner is the durable Flow-level owner for worktree-writing
	// launches that deliberately do not attach to a phase.
	UntrackedOwner *UntrackedOwner `json:"untracked_owner,omitempty"`
	// ProgressionClaim marks an identity created for the external Beads claim
	// protocol. Recovery repeats the idempotent claim; the marker is retry
	// provenance, not proof that ownership already landed. SQLite schema v5
	// fences this JSON marker with a compatibility trigger.
	ProgressionClaim      bool       `json:"progression_claim,omitempty"`
	PreparationGeneration string     `json:"preparation_generation,omitempty"`
	PreparedAt            *time.Time `json:"prepared_at,omitempty"`
	// PreparationNonce is storage-only generation identity minted by
	// CreatePreparation. General creation strips caller-supplied values.
	// SQLite schema v6 projects it onto flows.preparation_nonce and fences
	// older writers with a compatibility trigger. Schema v4 separately
	// requires boolean `done` in epic progression JSON.
	PreparationNonce string             `json:"-"`
	CreatedAt        time.Time          `json:"created_at"`
	UpdatedAt        time.Time          `json:"updated_at"`
	GraphRecovery    GraphRecoveryState `json:"-"`
}

FlowRecord is the persisted task workflow record.

func ActiveBeadFlow added in v0.10.6

func ActiveBeadFlow(err error) (FlowRecord, bool)

ActiveBeadFlow returns the Flow named by a duplicate refusal.

type GraphRecoveryState

type GraphRecoveryState struct {
	Status string
}

GraphRecoveryState reports an unresolved graph discovered during one-time legacy migration. The SQLite storage codec persists the unresolved marker.

type HeadlessUpdate added in v0.10.0

type HeadlessUpdate struct {
	FlowID  string
	Enabled bool
}

HeadlessUpdate changes the manual launch preference for one Flow.

type InspectExecutable added in v0.10.5

type InspectExecutable struct {
	Path         string `json:"path"`
	BuildVersion string `json:"build_version"`
	Schema       int    `json:"schema"`
}

InspectExecutable names the binary that produced the report.

type InspectMigrationOwner added in v0.10.5

type InspectMigrationOwner struct {
	PID      int  `json:"pid"`
	Verified bool `json:"verified"`
}

InspectMigrationOwner is the PID the bootstrap lock file carries. Verified is always false: a stale PID from a crashed migrator is indistinguishable from a live one, so this is evidence for a human and never drives a refusal.

type InspectOwner added in v0.10.6

type InspectOwner struct {
	PID           int    `json:"pid"`
	BuildVersion  string `json:"build_version"`
	Commit        string `json:"commit,omitempty"`
	Executable    string `json:"executable,omitempty"`
	SchemaVersion int    `json:"schema_version"`
	StartedAt     string `json:"started_at,omitempty"`
	Verified      bool   `json:"verified"`
}

InspectOwner is one live holder of the owners lease.

Verified is always TRUE, and that is the difference from InspectMigrationOwner above: this record was published by a process that still holds an exclusive flock on it, so its liveness was proved rather than inferred from a PID that a crash could have left behind.

type InspectReport added in v0.10.5

type InspectReport struct {
	SchemaVersion           int64      `json:"schema_version"`
	Path                    string     `json:"path"`
	Tier                    string     `json:"tier"`
	Readable                bool       `json:"readable"`
	UserVersion             *int64     `json:"user_version"`
	CheckpointedUserVersion *int64     `json:"checkpointed_user_version"`
	WAL                     InspectWAL `json:"wal"`
	JournalMode             *string    `json:"journal_mode"`
	DirectoryMode           *string    `json:"directory_mode"`
	GenerationID            *string    `json:"generation_id"`
	MinReaderGeneration     *int64     `json:"min_reader_generation"`
	MinWriterGeneration     *int64     `json:"min_writer_generation"`
	// FirstCompatibleRelease is the earliest release that opens a database at
	// the version found here, taken from this build's compatibility manifest.
	// Null when the version is one this build does not declare — a database
	// from a newer build, or a predecessor the manifest cannot map honestly.
	FirstCompatibleRelease *string                `json:"first_compatible_release"`
	SidecarStale           *bool                  `json:"sidecar_stale"`
	Executable             InspectExecutable      `json:"executable"`
	MigrationOwner         *InspectMigrationOwner `json:"migration_owner"`
	// Owners are the live holders of the owners lease, each verified. Always an
	// array, never null: "no live holders" is an answer, and a consumer must
	// not have to distinguish it from "not checked".
	Owners     []InspectOwner `json:"owners"`
	Warnings   []string       `json:"warnings"`
	Reason     *string        `json:"reason"`
	NextAction *string        `json:"next_action"`
}

InspectReport is the answer `approach db inspect --json` prints. Every nullable field is a pointer so "not applicable" and "zero" stay distinct — reporting sidecar_stale: false in a tier that never compared anything would assert agreement nobody checked.

func Inspect added in v0.10.5

func Inspect(root string) (InspectReport, error)

Inspect answers "what is in this state root, and can approach open it" for an operator who is already stuck.

It never constructs a Store, never takes the bootstrap lock — that lock is held for up to two minutes by a running migration, and this is the command you reach for while one is running — never opens with immutable=1, which would hide uncheckpointed WAL content behind a stale checkpointed view, and never routes through SecureCanonicalRoot, which would chmod the very directory the not_writable tier exists to report.

Its error return is reserved for the cases where it cannot produce a report at all. A database it merely could not open is a report, not an error.

type InspectWAL added in v0.10.5

type InspectWAL struct {
	Present    bool `json:"present"`
	ShmPresent bool `json:"shm_present"`
	// Dirty means a -wal file exists with non-zero size.
	Dirty bool `json:"dirty"`
}

InspectWAL reports the write-ahead log's state on disk.

type Issue

type Issue struct {
	Provider string `json:"provider,omitempty"`
	Number   int    `json:"number,omitempty"`
	URL      string `json:"url,omitempty"`
}

Issue stores agent-reported GitHub issue metadata.

type IssueUpdate

type IssueUpdate struct {
	FlowID   string
	Provider string
	Number   int
	URL      string
	Fence    PhaseLaunchFence
}

IssueUpdate records metadata for the issue referenced by a Flow.

type ManualMergeUpdate

type ManualMergeUpdate struct {
	FlowID   string
	PRNumber int
	PRURL    string
	Commit   string
	MergedAt time.Time
	Summary  string
}

ManualMergeUpdate records metadata for a PR that was manually merged in GitHub.

type Merge

type Merge struct {
	Status   string     `json:"status,omitempty"`
	Commit   string     `json:"commit,omitempty"`
	MergedAt *time.Time `json:"merged_at,omitempty"`
}

Merge stores agent-reported merge metadata.

type MergeUpdate

type MergeUpdate struct {
	FlowID   string
	Status   string
	Commit   string
	MergedAt time.Time
	Fence    PhaseLaunchFence
}

MergeUpdate records metadata for the merge that completed or blocked a Flow.

type OpenDiagnostics added in v0.10.5

type OpenDiagnostics struct {
	// DirectoryMode is the resolved root's permission bits as found.
	DirectoryMode os.FileMode
	// JournalMode is what PRAGMA journal_mode reported, lowercased.
	JournalMode string
	// SidecarStale reports an approach.db.meta.json that disagrees with
	// user_version. Only a RoleMigrator repairs it; every other role reports it.
	SidecarStale bool
	// Warnings are operator-facing notices that are not failures.
	Warnings []string
}

OpenDiagnostics reports what an open observed about the root and the database without repairing it. It is the replacement surface for the two checks a RoleReader skips — SecureCanonicalRoot's 0700 assertion and openSQLiteBackend's journal_mode=WAL assertion — so that dropping them is a substitution rather than a deletion.

type PRUpdate

type PRUpdate struct {
	FlowID     string
	Provider   string
	Number     int
	URL        string
	HeadBranch string
	BaseBranch string
	Status     string
	Fence      PhaseLaunchFence
}

PRUpdate records metadata for the pull request created by a Flow.

type PartialListEntry added in v0.10.1

type PartialListEntry struct {
	FlowID string
	Cause  error
}

PartialListEntry identifies one authoritative Flow row that could be scanned but not decoded while listing. Cause retains the row-local corruption error.

func (PartialListEntry) DiagnosticFlowID added in v0.10.1

func (e PartialListEntry) DiagnosticFlowID() string

DiagnosticFlowID renders the authoritative row ID without allowing corrupt control bytes or an ambiguous blank value to reach terminal and log sinks. FlowID itself remains unchanged so callers can inspect the exact stored key.

type PartialListError added in v0.10.1

type PartialListError struct {
	Entries []PartialListEntry
}

PartialListError reports rows omitted from an otherwise usable List result. Entries retain the list query's deterministic SQL order.

func AsPartialList added in v0.10.1

func AsPartialList(err error) (*PartialListError, bool)

AsPartialList classifies a valid standalone partial Flow-list result without requiring callers to match diagnostic text. Ordinary single-error wrapping is accepted, but aggregate error trees are rejected so a joined fatal error can never be downgraded merely because it also contains a partial diagnostic.

func (*PartialListError) Error added in v0.10.1

func (e *PartialListError) Error() string

func (*PartialListError) Unwrap added in v0.10.1

func (e *PartialListError) Unwrap() []error

Unwrap exposes every retained row-local cause to errors.Is and errors.As.

type PhaseAgentSettings added in v0.10.1

type PhaseAgentSettings struct {
	Agent           string
	Model           string
	ReasoningEffort string
}

PhaseAgentSettings is the agent selection captured for a Flow phase.

Each field is read independently: an empty field means nothing was captured and the value must be resolved from the global setting in effect at launch, while "default" means the provider default was captured explicitly. Writing them is not independent — see Validate.

func PhaseAgentSettingsFrom added in v0.10.1

func PhaseAgentSettingsFrom(settings agent.Settings) PhaseAgentSettings

PhaseAgentSettingsFrom converts a resolved launch selection into the persistence shape. It is the only conversion between the two triples, so callers holding an agent.Settings should route through it rather than assigning the three fields by hand.

func (PhaseAgentSettings) IsZero added in v0.10.1

func (s PhaseAgentSettings) IsZero() bool

IsZero reports whether no setting was captured. Emptiness is evaluated after normalization, so whitespace-only fields count as empty.

func (PhaseAgentSettings) Normalize added in v0.10.1

func (s PhaseAgentSettings) Normalize() PhaseAgentSettings

Normalize lowercases and trims every field.

func (PhaseAgentSettings) Validate added in v0.10.1

func (s PhaseAgentSettings) Validate() error

Validate checks the normalized triple as a unit: an unset selection is allowed, but a model or reasoning effort without an agent is not, because a model cannot be interpreted without knowing its agent.

type PhaseAgentSettingsUpdate added in v0.10.1

type PhaseAgentSettingsUpdate struct {
	FlowID   string
	PhaseID  string
	Settings PhaseAgentSettings
	Fence    PhaseLaunchFence
}

PhaseAgentSettingsUpdate replaces one phase's complete persisted agent settings stamp. Empty settings clear the stamp and restore global fallback.

type PhaseKind added in v0.10.7

type PhaseKind string

PhaseKind is a persisted Flow phase kind. JSON keeps the string values.

const (
	KindPlan                PhaseKind = "plan"
	KindPlanReview          PhaseKind = "plan_review"
	KindImplementation      PhaseKind = "implementation"
	KindReviewLoop          PhaseKind = "review_loop"
	KindPRCreation          PhaseKind = "pr_creation"
	KindAutoreview          PhaseKind = "autoreview"
	KindMerge               PhaseKind = "merge"
	KindImplementationChild PhaseKind = "implementation_child"
)

func SemanticKind

func SemanticKind(phase FlowPhase) PhaseKind

SemanticKind returns the normalized semantic kind for a phase. Persisted kind wins; otherwise default preset phase IDs are inferred for legacy records.

type PhaseLaunchEndUpdate

type PhaseLaunchEndUpdate struct {
	FlowID   string
	PhaseID  string
	LaunchID string
	EndedAt  time.Time
}

PhaseLaunchEndUpdate records that a tracked Flow launch has ended.

type PhaseLaunchFence added in v0.10.7

type PhaseLaunchFence struct {
	LaunchID     string
	OwnerPhaseID string
}

PhaseLaunchFence revokes writes from a phase launch removed by recovery and, when OwnerPhaseID is set, from a launch that no longer owns that phase. Store mutations check it inside the same transaction that persists the write.

type PhaseLaunchUpdate

type PhaseLaunchUpdate struct {
	FlowID          string
	PhaseID         string
	LaunchID        string
	Resume          bool
	AutoLaunch      bool
	AutoMerge       bool
	GlobalAutoMerge bool
}

PhaseLaunchUpdate records one agent launch attempt against a Flow phase. Resume marks the launch as a session resume: resuming a phase in a terminal status (completed, skipped) records the launch without reopening the phase, while non-resume launches always mark the phase running.

type PhaseReconciliation added in v0.10.7

type PhaseReconciliation struct {
	Reason   string `json:"reason"`
	LaunchID string `json:"launch_id"`
}

PhaseReconciliation authenticates a demotion written by launch control.

type PhaseRecoveryUpdate added in v0.10.7

type PhaseRecoveryUpdate struct {
	FlowID            string
	PhaseID           string
	ExpectedStatus    PhaseStatus
	ExpectedOutcome   string
	ExpectedLaunchID  string
	ExpectedUpdatedAt time.Time
	Fence             PhaseLaunchFence
}

PhaseRecoveryUpdate identifies one reconciliation-demoted phase snapshot. Recovery compares every field before removing the stale launch so a request can never apply to phase state newer than the command observed.

type PhaseResetUpdate

type PhaseResetUpdate struct {
	FlowID  string
	PhaseID string
	Fence   PhaseLaunchFence
}

PhaseResetUpdate identifies one UI-owned phase recovery mutation.

type PhaseRestartUpdate

type PhaseRestartUpdate struct {
	FlowID  string
	PhaseID string
	Notes   string
	Fence   PhaseLaunchFence
}

PhaseRestartUpdate restarts a blocked or needs-attention phase as running.

type PhaseSpec

type PhaseSpec struct {
	ID        string    `toml:"id"`
	Title     string    `toml:"title"`
	Kind      PhaseKind `toml:"kind"`
	DependsOn []string  `toml:"depends_on"`
}

PhaseSpec is a data-only phase declaration used by Flow phase graph presets.

type PhaseStatus added in v0.10.7

type PhaseStatus string

PhaseStatus is a persisted Flow phase status. JSON keeps the string values.

const (
	PhasePending        PhaseStatus = "pending"
	PhaseReady          PhaseStatus = "ready"
	PhaseRunning        PhaseStatus = "running"
	PhaseNeedsAttention PhaseStatus = "needs_attention"
	PhaseCompleted      PhaseStatus = "completed"
	PhaseBlocked        PhaseStatus = "blocked"
	PhaseSkipped        PhaseStatus = "skipped"
)

type PhaseUpdate

type PhaseUpdate struct {
	FlowID  string
	PhaseID string
	Status  PhaseStatus
	Outcome string
	Notes   string
	Summary string
	Fence   PhaseLaunchFence
}

PhaseUpdate describes one persisted phase status update.

type PlanLinkUpdate

type PlanLinkUpdate struct {
	FlowID   string
	PlanID   string
	PlanPath string
	Fence    PhaseLaunchFence
}

PlanLinkUpdate links a saved approach plan artifact to an existing Flow.

type PreparationFinalizer added in v0.10.2

type PreparationFinalizer interface {
	Finalize(func() error) (FlowRecord, error)
	Compensate(notes string) (FlowRecord, error)
	// CompensateUnderReservation is Compensate for a caller that already holds
	// this Flow's launch/close reservation. Compensate acquires that reservation
	// itself and must not be used while one is already held.
	CompensateUnderReservation(notes string) (FlowRecord, error)
}

PreparationFinalizer is a Flow-bound one-shot capability. The concrete type is private so callers cannot construct a capability for an arbitrary Flow ID.

type PreparedEpicProgressionUpdate added in v0.10.2

type PreparedEpicProgressionUpdate struct {
	FlowID string
	Key    EpicProgressionKey
	Bead   BeadLink
}

PreparedEpicProgressionUpdate binds enablement to one exact prepared child Flow. Callers hold that Flow's launch/close reservation around this update.

type Preset

type Preset struct {
	Name   string      `toml:"name"`
	Phases []PhaseSpec `toml:"phases"`
}

Preset declares a reusable graph of top-level Flow phases.

func DefaultPreset

func DefaultPreset() Preset

DefaultPreset returns the built-in phase graph used when callers do not provide a custom graph.

type PullRequest

type PullRequest struct {
	Provider   string `json:"provider,omitempty"`
	Number     int    `json:"number,omitempty"`
	URL        string `json:"url,omitempty"`
	HeadBranch string `json:"head_branch,omitempty"`
	BaseBranch string `json:"base_branch,omitempty"`
	Status     string `json:"status,omitempty"`
}

PullRequest stores agent-reported PR metadata.

type ReconciliationDemotionUpdate added in v0.10.7

type ReconciliationDemotionUpdate struct {
	PhaseUpdate
	Reason   string
	LaunchID string
}

ReconciliationDemotionUpdate is the launch-control-owned form of PhaseUpdate.

type RestoreOptions added in v0.10.6

type RestoreOptions struct {
	Root       string
	BackupPath string
	// Force proceeds past a generation mismatch. It does not skip the backup
	// verification or the owners refusal: those are not acknowledgements, they
	// are the difference between a restore and data loss.
	Force bool
	// LockTimeout bounds the bootstrap lease wait. Zero uses the package default.
	LockTimeout time.Duration
}

RestoreOptions names what to put back and where.

type RestoreResult added in v0.10.6

type RestoreResult struct {
	SchemaVersion int64  `json:"schema_version"`
	Path          string `json:"path"`
	RestoredFrom  string `json:"restored_from"`
	// PreRestoreBackup is the copy of the database this restore replaced.
	PreRestoreBackup string `json:"pre_restore_backup"`
	UserVersion      int64  `json:"user_version"`
	GenerationID     string `json:"generation_id"`
	Forced           bool   `json:"forced"`
}

RestoreResult is what `approach db restore --json` prints. Keys match `db inspect`'s style so an operator reading both does not have to switch vocabularies.

func Restore added in v0.10.6

func Restore(opts RestoreOptions) (RestoreResult, error)

Restore puts a verified backup back in place of the live flow database.

The order below is the whole safety argument and must not be rearranged:

  1. verify the backup, before anything is touched — a restore that replaced a working database with an unreadable copy is strictly worse than the problem it was called for;
  2. scan the owners lease and refuse while any process holds the database open, naming each. Restoring under a live handle is strictly worse than migrating under one: the file is replaced outright, and the holder's open descriptor keeps referring to an unlinked inode;
  3. take the bootstrap lock — after the lease scan, the same total order the migration path uses;
  4. compare generations, and refuse a mismatch without --force;
  5. copy the live database aside, then replace it, then drop the stale WAL.

type Role added in v0.10.5

type Role int

Role names what an opener is allowed to do to the database it opens.

The zero value is RoleMigrator, deliberately: every bare NewStore(StoreOptions{Root: root}) in the tree — and there are well over a hundred of them in tests alone — keeps today's behaviour byte for byte. A restriction that is inherited by default is a restriction nobody can reason about; naming the role at the entry point is the whole point of this seam.

const (
	// RoleMigrator may advance the schema. Exactly one process-level entry point
	// should hold it: `approach db migrate` and TUI startup.
	RoleMigrator Role = iota
	// RoleReader opens read-only. It never migrates, never repairs the root's
	// mode, never discards a staged database, and refuses writes in Go before
	// SQLite ever sees them.
	RoleReader
	// RoleWriter reads and writes records but will not advance the schema.
	RoleWriter
)

func (Role) String added in v0.10.5

func (r Role) String() string

String names the role the way the refusal messages spell it.

type Session

type Session struct {
	Provider       string    `json:"provider,omitempty"`
	SessionID      string    `json:"session_id,omitempty"`
	LaunchID       string    `json:"launch_id,omitempty"`
	Status         string    `json:"status,omitempty"`
	StartedAt      time.Time `json:"started_at,omitempty"`
	EndedAt        time.Time `json:"ended_at,omitempty"`
	TranscriptPath string    `json:"transcript_path,omitempty"`
}

Session references a provider session without duplicating transcript contents.

func LatestPhaseSession

func LatestPhaseSession(phase FlowPhase, requireSessionID bool) (Session, bool)

LatestPhaseSession returns the display/latest session for a phase. A session attached to the latest non-empty launch ID wins; otherwise timestamps and slice order provide deterministic legacy-record fallback.

type SessionAttachUpdate

type SessionAttachUpdate struct {
	FlowID  string
	PhaseID string
	Session Session
}

SessionAttachUpdate attaches a captured provider session to a Flow phase.

type StartMetadataUpdate

type StartMetadataUpdate struct {
	FlowID       string
	WorktreePath string
	Branch       string
	BaseRef      string
	Commit       string
	PlanID       string
	PlanPath     string
}

StartMetadataUpdate adds launch-start metadata that is only known after a Flow record has been allocated.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store reads and writes Flow rows in the artifact root's approach.db.

func NewStore

func NewStore(opts StoreOptions) (*Store, error)

NewStore creates a Store rooted at an absolute artifact root.

func (*Store) ActivateUntrackedOwner added in v0.10.8

func (s *Store) ActivateUntrackedOwner(update UntrackedOwnerActivation) (FlowRecord, error)

func (*Store) AddChildPhase

func (s *Store) AddChildPhase(update ChildPhaseUpdate) (FlowRecord, error)

AddChildPhase creates or updates a stable child phase under Implementation.

func (*Store) AddPhaseLaunchID

func (s *Store) AddPhaseLaunchID(update PhaseLaunchUpdate) (FlowRecord, error)

AddPhaseLaunchID records a launch attempt. Fresh launches mark the phase running; resume launches of terminal phases preserve the terminal status.

func (*Store) AllocateID added in v0.10.2

func (s *Store) AllocateID(title string) (string, error)

AllocateID returns the canonical timestamped Flow ID CreateWithOptions would choose for title at this instant. Allocation is intentionally non-durable: it does not create or reserve a record, and concurrent callers may receive the same candidate. CreateWithOptions remains the atomic collision arbiter when the caller supplies the returned ID in FlowRecord.FlowID.

func (*Store) AttachSession

func (s *Store) AttachSession(update SessionAttachUpdate) (FlowRecord, error)

AttachSession records a provider session against a phase. Re-attaching the same provider/session id updates the existing reference in place.

func (*Store) ClaimUntrackedOwner added in v0.10.8

func (s *Store) ClaimUntrackedOwner(update UntrackedOwnerClaim) (FlowRecord, error)

func (*Store) Close added in v0.10.0

func (s *Store) Close() error

Close releases the Store's database handles. A Store holds a pooled connection for its whole life, so callers that build one per operation must Close it or leak descriptors; long-lived callers may Close at shutdown or not at all. Using the Store after Close returns an error rather than panicking. It is safe to call more than once.

Callers that discard the error are not being sloppy: every record a caller wrote is already durable when its mutation returned, so a Close failure can only mean the final WAL checkpoint did not run. Turning that into a non-zero exit would report a successful mutation as a failure, which is the worse outcome for a CLI whose caller is usually an agent.

func (*Store) CloseFlow added in v0.10.1

func (s *Store) CloseFlow(update ClosureUpdate) (FlowRecord, error)

CloseFlow marks a Flow deliberately closed with a required reason. Phases are left exactly as they are, so the record still explains where work stopped; terminality is enforced by the launch guards, not by rewriting phase rows.

Unlike SetAutoMode and SetHeadless, a redundant call is an error rather than a no-op: a second close discards a reason the user typed, which is worth reporting.

func (*Store) Create

func (s *Store) Create(record FlowRecord) (FlowRecord, error)

Create writes a new flow record with the default Flow phase graph. New records always start with auto mode enabled; callers that need manual mode should create the Flow, then opt out with SetAutoMode(false).

func (*Store) CreatePreparation added in v0.10.2

func (s *Store) CreatePreparation(record FlowRecord, opts CreateOptions) (FlowRecord, PreparationFinalizer, error)

CreatePreparation creates an ordinary receipt-less Flow and returns the sole capability that may stamp its preparation receipt.

func (*Store) CreateWithOptions

func (s *Store) CreateWithOptions(record FlowRecord, opts CreateOptions) (FlowRecord, error)

CreateWithOptions writes a new flow record, optionally seeding empty phase lists from a preset instead of the default graph.

func (*Store) Delete

func (s *Store) Delete(flowID string) error

Delete removes only the requested Flow row. It takes the launch/close reservation so a delete and same-ID recreate cannot replace a Flow while an admitted launch still owns that identity.

func (*Store) DemoteReconciledPhase added in v0.10.7

func (s *Store) DemoteReconciledPhase(update ReconciliationDemotionUpdate) (FlowRecord, error)

DemoteReconciledPhase persists a launch-control reconciliation marker with the phase transition. Agent-facing phase commands do not expose this method.

func (*Store) EnableEpicProgressionForPreparedFlow added in v0.10.2

func (s *Store) EnableEpicProgressionForPreparedFlow(update PreparedEpicProgressionUpdate) (EpicProgression, FlowRecord, error)

EnableEpicProgressionForPreparedFlow revalidates the exact child Flow and enables progression in one SQLite writer transaction.

func (*Store) HaltEpicProgression added in v0.10.5

func (s *Store) HaltEpicProgression(update EpicProgressionHaltUpdate) (EpicProgression, error)

HaltEpicProgression atomically moves authoritative active progression to halted. Only active state may halt; an already-halted record retains its first halt tuple and both timestamps, so the original cause always wins.

func (*Store) List

func (s *Store) List(filter FlowFilter) ([]FlowRecord, error)

List returns records matching filter, sorted by UpdatedAt descending.

func (*Store) MarkManualMerge

func (s *Store) MarkManualMerge(update ManualMergeUpdate) (FlowRecord, error)

MarkManualMerge completes the merge phase and records verified GitHub merge metadata for a PR that was merged outside approach.

The linked-plan sync runs AFTER the merge commits, giving four cases:

  • sync succeeded: the committed record, nil.
  • sync failed on an already-merged repeat: the record and nil. The error is DISCARDED rather than allowed to demote durable merge state.
  • sync failed otherwise: the compensated record beside the error, or the current state when the compensation guard rejects — deliberately unlike SetPhase, which returns a zero record. A guard-rejected record is still merged and still reaches the TUI beside a merge-failure message; that is the truth of the durable state.
  • sync failed and the compensation failed too: a zero record beside one error carrying both, matching SetPhase.

See syncLinkedPlanPhase for the ordering and its accepted windows.

func (*Store) MarkPhaseLaunchEnded

func (s *Store) MarkPhaseLaunchEnded(update PhaseLaunchEndUpdate) (FlowRecord, error)

MarkPhaseLaunchEnded mirrors launch finalization into Flow-attached session metadata so recovery labels and reset eligibility do not depend on a later provider hook.

func (*Store) OpenDiagnostics added in v0.10.5

func (s *Store) OpenDiagnostics() OpenDiagnostics

OpenDiagnostics reports what this open observed about the root and the database. It is populated for every role and is the surface that makes a RoleReader's skipped checks — the root's 0700 assertion and the WAL assertion — observable rather than simply absent.

func (*Store) PrepareUntrackedOwnerTransport added in v0.10.8

func (s *Store) PrepareUntrackedOwnerTransport(update UntrackedOwnerActivation) (FlowRecord, error)

PrepareUntrackedOwnerTransport records an exact transport identity while the launcher reservation still owns admission. Once the caller confirms spawn, LauncherHandoffComplete transfers reconciliation to that exact transport.

func (*Store) Read

func (s *Store) Read(flowID string) (FlowRecord, error)

Read returns one flow record by ID.

func (*Store) ReadEpicProgression added in v0.10.2

func (s *Store) ReadEpicProgression(key EpicProgressionKey) (EpicProgression, bool, error)

ReadEpicProgression distinguishes a missing row from a malformed or unreadable row. Missing means normal disabled to callers that project state.

func (*Store) ReconcileEpicProgressionSuccessor added in v0.10.2

func (s *Store) ReconcileEpicProgressionSuccessor(update EpicProgressionSuccessorUpdate) (EpicProgressionSuccessorResult, error)

ReconcileEpicProgressionSuccessor authoritatively classifies a prepared sequential successor without enabling or otherwise mutating progression. Inactive progression wins before any Flow condition is inspected.

func (*Store) RecoverReconciledPhase added in v0.10.7

func (s *Store) RecoverReconciledPhase(update PhaseRecoveryUpdate) (FlowRecord, error)

RecoverReconciledPhase atomically removes the exact stale launch from a reconciliation-demoted phase and derives that phase back to ready.

func (*Store) ReleaseUntrackedOwner added in v0.10.8

func (s *Store) ReleaseUntrackedOwner(update UntrackedOwnerRelease) (FlowRecord, error)

func (*Store) ReopenFlow added in v0.10.1

func (s *Store) ReopenFlow(flowID string) (FlowRecord, error)

ReopenFlow clears the closure from a closed Flow, restoring the launchability it had before the close.

func (*Store) ReplaceUntrackedOwner added in v0.10.8

func (s *Store) ReplaceUntrackedOwner(update UntrackedOwnerReplacement) (FlowRecord, error)

ReplaceUntrackedOwner reclaims a proven-dead active owner and installs a new reservation in the same writer transaction.

func (*Store) ReserveAgentLaunch added in v0.10.1

func (s *Store) ReserveAgentLaunch(flowID string) (FlowRecord, func(), error)

ReserveAgentLaunch orders an agent spawn with CloseFlow. The caller must hold the returned reservation until the terminal or external launcher has either started or failed, so a close and a launch have one authoritative cross-process ordering.

func (*Store) ReserveEpicProgressionSuccessor added in v0.10.2

func (s *Store) ReserveEpicProgressionSuccessor(flowID string) (func(), error)

ReserveEpicProgressionSuccessor holds the Flow's launch/close lock while sequential progression classifies it. Unlike launch reservations, this reservation intentionally permits missing and closed Flows so the caller can apply progression-first inactive precedence to every Flow condition.

func (*Store) ReserveRepairLaunch added in v0.10.1

func (s *Store) ReserveRepairLaunch(flowID string) (FlowRecord, func(), error)

ReserveRepairLaunch holds the Flow's cross-process repair-launch lock and returns the current record. Callers must invoke the returned release function after the terminal has either started or failed to start. CloseFlow takes the same lock, so exactly one ordering wins: a completed close makes this call reject, while an admitted repair starts before a concurrent close proceeds. The OS releases the advisory lock automatically if the process exits.

func (*Store) ResetAwaitingSessionPhase

func (s *Store) ResetAwaitingSessionPhase(update PhaseResetUpdate) (FlowRecord, error)

ResetAwaitingSessionPhase removes an orphaned latest launch attempt from a running phase and lets approach derive it back to ready. This is intentionally not part of the agent-facing phase transition table.

func (*Store) ResetRecoverableRunningPhase

func (s *Store) ResetRecoverableRunningPhase(update PhaseResetUpdate) (FlowRecord, error)

ResetRecoverableRunningPhase removes the latest stale launch attempt from a running phase and lets approach derive it back to ready. This is intentionally not part of the agent-facing phase transition table.

func (*Store) RestartPhase

func (s *Store) RestartPhase(update PhaseRestartUpdate) (FlowRecord, error)

RestartPhase atomically restarts a blocked or needs-attention phase as running.

func (*Store) Revalidate added in v0.10.6

func (s *Store) Revalidate() error

Revalidate forces the exact answer the throttled check only approximates. A caller about to do something it cannot undo — and `db restore` itself — should ask rather than wait out the window.

func (*Store) Root added in v0.10.6

func (s *Store) Root() string

Root is the canonical state root this store opened, after symlink resolution. Callers that place files beside the database — the owners lease especially — must use this rather than the path they passed in, or a symlinked spelling would put their file in a directory nothing else looks at.

func (*Store) SetAutoMerge added in v0.10.8

func (s *Store) SetAutoMerge(update AutoMergeUpdate) (FlowRecord, error)

SetAutoMerge sets or clears one Flow's automatic merge-phase override.

func (*Store) SetAutoMode

func (s *Store) SetAutoMode(update AutoModeUpdate) (FlowRecord, error)

SetAutoMode enables or disables TUI-owned automatic phase launching for one Flow.

func (*Store) SetEpicProgression added in v0.10.2

func (s *Store) SetEpicProgression(update EpicProgressionUpdate) (EpicProgression, error)

SetEpicProgression atomically applies one active, normal-off, or done target. Redundant writes preserve both timestamps and perform no row update.

func (*Store) SetHeadless added in v0.10.0

func (s *Store) SetHeadless(update HeadlessUpdate) (FlowRecord, error)

SetHeadless enables or disables headless manual launches for one Flow.

func (*Store) SetIssue

func (s *Store) SetIssue(update IssueUpdate) (FlowRecord, error)

SetIssue validates and persists the issue metadata reported by an agent.

func (*Store) SetMerge

func (s *Store) SetMerge(update MergeUpdate) (FlowRecord, error)

SetMerge validates and persists the merge metadata reported by an agent.

func (*Store) SetPR

func (s *Store) SetPR(update PRUpdate) (FlowRecord, error)

SetPR validates and persists the pull request metadata reported by an agent.

func (*Store) SetPhase

func (s *Store) SetPhase(update PhaseUpdate) (FlowRecord, error)

SetPhase validates and persists one phase update on an existing flow.

The linked-plan sync runs AFTER the phase change commits, so the return contract has four cases:

  • sync succeeded: the committed record, nil.
  • sync failed on a repeat of an already-completed phase: the committed record and nil, because the durable state is correct and the plan write is idempotent. The sync error is DISCARDED, so this recovery path reports success even when it failed again; the signal is the linked plan's own phase status, exactly as on MarkManualMerge's retry.
  • sync failed otherwise: a ZERO record beside the sync error, whether the needs_attention compensation was persisted by the second update or its guard declined to write. MarkManualMerge deliberately differs and returns its compensated record; both halves are pinned by tests.
  • sync failed and the compensation failed too: a zero record beside one error carrying both, with %w on the sync error.

See syncLinkedPlanPhase for the ordering and its accepted windows.

func (*Store) SetPhaseAgentSettings added in v0.10.1

func (s *Store) SetPhaseAgentSettings(update PhaseAgentSettingsUpdate) (FlowRecord, error)

SetPhaseAgentSettings atomically replaces only one phase's agent settings. It intentionally bypasses Flow normalization, readiness derivation, linked plan synchronization, and unrelated phase validation: settings affect only a future launch and legacy records must remain otherwise byte-for-byte stable.

func (s *Store) SetPlanLink(update PlanLinkUpdate) (FlowRecord, error)

SetPlanLink validates and persists the saved plan artifact linked to a Flow.

func (*Store) SetStartMetadata

func (s *Store) SetStartMetadata(update StartMetadataUpdate) (FlowRecord, error)

SetStartMetadata persists branch/worktree/plan metadata discovered while starting a Flow. Empty fields leave existing values unchanged.

type StoreOptions

type StoreOptions struct {
	Root string
	Now  func() time.Time
	// LockTimeout bounds bootstrap lease and SQLite writer acquisition waits.
	LockTimeout time.Duration
	Presets     []Preset
	// AllowDevLiveMigration acknowledges that a development build may advance
	// the schema of the database a *released* build owns. Without it that one
	// case is refused; every other root, and every release build, is unaffected.
	// Surfaced as --allow-dev-live-migration and
	// APPROACH_ALLOW_DEV_LIVE_MIGRATION=1.
	AllowDevLiveMigration bool
	// Role names what this open is allowed to do. The zero value, RoleMigrator,
	// is today's behaviour; every non-test caller names one explicitly and a
	// call-site test enforces that.
	Role Role
	// RootExplicit reports that Root was named by a flag or an environment
	// variable rather than falling back to config or the built-in default. It
	// decides only whether a RoleReader creates a missing root or reports the
	// typo. The zero value keeps today's create-if-absent behaviour.
	RootExplicit bool
	// BackupDir is where a migration writes its pre-migration copy. Empty means
	// <root>/backups/. Surfaced as `approach db migrate --backup-dir`.
	BackupDir string
	// OwnerNonce is the caller's own internal/dblease holder, excluded from the
	// owners scan a migration runs. A long-lived migrator (TUI startup) takes
	// its lease FIRST and keeps it across the migration; without this it would
	// refuse to migrate because of itself.
	OwnerNonce string
}

StoreOptions configures a Store.

type UntrackedOwner added in v0.10.8

type UntrackedOwner struct {
	LaunchID      string                  `json:"launch_id"`
	Role          UntrackedOwnerRole      `json:"role"`
	State         UntrackedOwnerState     `json:"state"`
	Transport     UntrackedOwnerTransport `json:"transport,omitzero"`
	LauncherPID   int                     `json:"launcher_pid,omitempty"`
	LauncherToken string                  `json:"launcher_token,omitempty"`
	ReservedAt    time.Time               `json:"reserved_at"`
	ActivatedAt   time.Time               `json:"activated_at,omitempty"`
	EndedAt       time.Time               `json:"ended_at,omitempty"`
}

UntrackedOwner is the durable phase-untracked worktree owner. Ended owners remain as fenced lifecycle history until the next claim replaces them.

type UntrackedOwnerActivation added in v0.10.8

type UntrackedOwnerActivation struct {
	FlowID    string
	LaunchID  string
	Transport UntrackedOwnerTransport
	// LauncherHandoffComplete clears the temporary launcher fence after the
	// exact transport has started, so later reconciliation probes that identity.
	LauncherHandoffComplete bool
}

type UntrackedOwnerClaim added in v0.10.8

type UntrackedOwnerClaim struct {
	FlowID string
	Owner  UntrackedOwner
}

type UntrackedOwnerRelease added in v0.10.8

type UntrackedOwnerRelease struct {
	FlowID   string
	LaunchID string
}

type UntrackedOwnerReplacement added in v0.10.8

type UntrackedOwnerReplacement struct {
	FlowID           string
	ExpectedLaunchID string
	Owner            UntrackedOwner
}

type UntrackedOwnerRole added in v0.10.8

type UntrackedOwnerRole string
const (
	UntrackedOwnerWorktreeAgent UntrackedOwnerRole = "worktree_agent"
	UntrackedOwnerAutofix       UntrackedOwnerRole = "autofix"
	UntrackedOwnerRepair        UntrackedOwnerRole = "repair"
)

type UntrackedOwnerState added in v0.10.8

type UntrackedOwnerState string
const (
	UntrackedOwnerReserved UntrackedOwnerState = "reserved"
	UntrackedOwnerLive     UntrackedOwnerState = "live"
	UntrackedOwnerEnded    UntrackedOwnerState = "ended"
)

type UntrackedOwnerTransport added in v0.10.8

type UntrackedOwnerTransport struct {
	Kind         UntrackedTransportKind `json:"kind"`
	Socket       string                 `json:"socket,omitempty"`
	Session      string                 `json:"session,omitempty"`
	Window       string                 `json:"window,omitempty"`
	PID          int                    `json:"pid,omitempty"`
	ProcessToken string                 `json:"process_token,omitempty"`
}

UntrackedOwnerTransport identifies the exact transport whose liveness proves whether an owner may be reclaimed. Only the fields used by Kind are set.

type UntrackedTransportKind added in v0.10.8

type UntrackedTransportKind string
const (
	UntrackedTransportLauncher     UntrackedTransportKind = "launcher_process"
	UntrackedTransportRepoTmux     UntrackedTransportKind = "repo_tmux"
	UntrackedTransportEmbeddedTmux UntrackedTransportKind = "embedded_tmux"
	UntrackedTransportDirect       UntrackedTransportKind = "direct_embedded"
)

Jump to

Keyboard shortcuts

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