metadata

package
v0.14.1 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package metadata defines the Projmux resource metadata model: the Project -> Window -> (Pane | Agent) ownership tree, its opaque identity (`metadata.uid`), its stable query key (`metadata.name`), and the atomic name-suffix allocator that backs both.

The package is pure. It performs no file, tmux, or process I/O; every environment-dependent input (clock, uid source, root-directory existence) arrives through the injected Mutator. Persistence and the tmux transport mirror live in internal/integrations/metadata.

Identity boundary: tmux raw identity (`$N`, `@N`, `%N`, `pane_title`, `window_name`) is adapter/status transport and is deliberately absent from this model. tmux ids are resolved to a uid by the adapter, never stored as identity here.

Field spelling in the on-disk registry follows the resource-model contract (`apiVersion`, `schemaVersion`, `metadata`, `displayName`, `ownerRef`, `anchorPaneRef`, `defaultShellPaneRef`, `spec`, `status`) rather than the snake_case spelling used by the older projmux state files. See docs/architecture.md.

Index

Constants

View Source
const (
	// TerminationReasonIntentional is a canonical control action's own record.
	TerminationReasonIntentional = "managed pane was ended by a control action"
	// TerminationReasonNormal is a supervised exit status 0.
	TerminationReasonNormal = "managed process exited with status 0"
	// TerminationReasonKilled is an externally requested hangup with no paired
	// canonical control-action receipt.
	TerminationReasonKilled = "managed process was killed externally"
	// TerminationReasonAbnormal is a supervised non-zero exit or a signal.
	TerminationReasonAbnormal = "managed process exited abnormally"
	// TerminationReasonUnknown is an absence no receipt explains.
	//
	// It is the pre-existing sweep clause, kept verbatim. The sweep applied it
	// to every disappearance including the ones that are now proven; narrowing
	// it to the case it was always literally true of needs no new words.
	TerminationReasonUnknown = "managed pane is no longer live"
)

Termination reason clauses. Each classification gets its own, because status.reason is the one line an operator reads next to the phase, and two classifications sharing a clause would make the phase the only thing separating them -- which is exactly the information the phase collapses.

View Source
const (
	// ConditionMissingRoot marks a Project whose spec.root has disappeared.
	// The Project is never deleted or re-identified while it is set.
	ConditionMissingRoot = "MissingRoot"

	// ConditionMissingRuntime marks a Window or Pane whose uid is mirrored on
	// no live tmux object. It is the recorded *reason* a runtime object went
	// away, and it is deliberately not a deletion: the resource keeps its uid,
	// its name reservation, and its place in the owner tree, exactly the way
	// ConditionMissingRoot preserves a Project whose root vanished.
	//
	// It is never the source of a status read. Status is derived from a live
	// observation taken by the reading invocation (see selector.ObservedStatus);
	// this condition is the durable note the reconciler leaves behind so
	// `describe` can say why an object is offline long after the observation
	// that noticed it has been discarded.
	ConditionMissingRuntime = "MissingRuntime"

	ConditionTrue  = "True"
	ConditionFalse = "False"
)
View Source
const (
	FallbackProjectNameBase = "project"
	FallbackWindowNameBase  = "window"
	FallbackPaneNameBase    = "pane"
	FallbackAgentNameBase   = "agent"
	// FallbackControlSessionNameBase is used when the bound tmux session name
	// sanitizes to nothing usable as a query key.
	FallbackControlSessionNameBase = "control"
)

Fallback name bases used when no better seed exists.

View Source
const (
	AgentProgressPlanCap  = 99
	AgentProgressFilesCap = 999
	AgentProgressItemsCap = 32
	AgentProgressSource   = "provider-control-plane"
)
View Source
const APIVersion = "projmux.io/v1alpha1"

APIVersion is the resource API version string stamped on every resource.

View Source
const AgentInteractionFreshFor = 30 * time.Minute

AgentInteractionFreshFor is the maximum age at which a durable observation may be presented as current after a process restart. Provider hooks refresh active states; an abandoned response must not leave a live badge forever.

View Source
const AnnotationAgentTopic = "projmux.io/agent-topic"

AnnotationAgentTopic is the non-identifying annotation that carries an AI topic. Topics are never a name or a selector input.

View Source
const DeletedPaneMirrorPrefix = "deleted:"

DeletedPaneMirrorPrefix marks a live Pane whose Registry resource was durably deleted before its self-target kill could be queued. It is transport state, not a resource uid: every binding/import path must refuse it so the deleted Pane cannot be minted back under a new identity.

View Source
const ReasonRuntimeUnbound = "RuntimeUnbound"

ReasonRuntimeUnbound is the ConditionMissingRuntime reason. It records what was observed -- no live tmux object mirrors this uid -- and nothing about why, because nothing about why is observable: a window or pane that is gone leaves no exit status behind for a later inventory read to recover.

View Source
const SchemaVersion = 2

SchemaVersion is the current registry envelope version. A registry file carrying a higher value is rejected fail-closed; see schema.go.

Variables

View Source
var (
	// ErrNameConflict is an explicit --name / rename collision. It never gets
	// an implicit suffix; the operation fails with zero mutations.
	ErrNameConflict = errors.New("name is already in use")
	// ErrNameExhausted means the automatic suffix space for a base is full.
	ErrNameExhausted = errors.New("automatic name suffix space is exhausted")
	// ErrInvalidName marks a name that cannot be a stable query key.
	ErrInvalidName = errors.New("invalid resource name")
	// ErrInvalidRoot marks a root that is not an existing absolute directory.
	ErrInvalidRoot = errors.New("invalid project root")
	// ErrRootConflict marks a rebind onto a root already bound to another
	// Project uid.
	ErrRootConflict = errors.New("project root is already bound to another project")
	// ErrNotFound marks an unresolvable uid.
	ErrNotFound = errors.New("resource not found")
	// ErrInvalidPhase marks an Agent phase value or transition that is not in
	// the closed lifecycle model.
	ErrInvalidPhase = errors.New("invalid agent phase")
	// ErrInvalidRegistry marks a registry that violates a structural invariant.
	ErrInvalidRegistry = errors.New("invalid resource registry")
	// ErrSchemaTooNew marks a registry envelope newer than this build. It is
	// handled fail-closed: the caller refuses to read it as valid and performs
	// no write at all.
	ErrSchemaTooNew = errors.New("resource registry schema version is newer than supported")
	// ErrSchemaUnsupported marks an envelope version with no migration path.
	ErrSchemaUnsupported = errors.New("unsupported resource registry schema version")
)

Sentinel causes. Every metadata failure wraps exactly one of these so callers classify with errors.Is instead of string matching.

Functions

func AgentNameBase

func AgentNameBase(explicit, provider string) string

AgentNameBase applies the Agent naming order: explicit --name, normalized provider id, then "agent" when the provider is unknown.

func AgentPanePromotionRefusal added in v0.13.0

func AgentPanePromotionRefusal(reg *Registry, windowUID, paneUID string, observed LegacyPane) string

AgentPanePromotionRefusal returns the reason a runtime Agent marker cannot change one direct Window-owned shell Pane into an Agent-owned managed Pane.

defaultShellPaneRef is Registry ownership authority, not a runtime hint. A marker can link an additional Pane that projmux previously launched, but it cannot re-parent the canonical default shell that ref preserves. Anchor-only promotion semantics belong to a separate contract and remain unchanged. Refusing here is deliberately zero-write: no Agent is minted, no reservation changes scope, and the Pane uid, owner, role, and Window refs remain exact.

func CanTransitionAgent

func CanTransitionAgent(from, to AgentPhase) bool

CanTransitionAgent reports whether from -> to is a permitted transition.

func ControlSessionNameBase added in v0.13.0

func ControlSessionNameBase(session string) string

ControlSessionNameBase derives the ControlSession name base from the tmux session name it is bound to.

The session name is the only seed there is: a control session owns no root to take a basename from, and inventing one from $HOME would be the very path-derived identity this kind exists to avoid.

func DerivePaneDisplayTitle

func DerivePaneDisplayTitle(agent, topic, command, rawTitle string) string

DerivePaneDisplayTitle computes the secondary, derived pane title from the Agent topic, a known interactive shell command, or the raw pane title. It is never a selector, an identity, or a Window name source, and the Pane metadata.name is deliberately not an input.

func EqualTeardownPlans added in v0.13.0

func EqualTeardownPlans(left, right TeardownDecision) bool

EqualTeardownPlans exists for property tests and controller callers that need to compare two delivery orders without depending on serialization.

func IsUsageError

func IsUsageError(err error) bool

IsUsageError reports whether err was caused by invalid user input. The app layer converts these into the CLI usage-error exit code 2.

func LegacyPaneNameSeed

func LegacyPaneNameSeed(pane LegacyPane, defaultShell string) string

LegacyPaneNameSeed derives the one-time Pane name base for a legacy pane: the existing @projmux_pane_label, then the command basename, then the configured shell basename, then "pane".

func LegacyWindowNameSeed

func LegacyWindowNameSeed(_ LegacyWindow) string

LegacyWindowNameSeed returns the stable allocator base for a newly imported Window. Nothing observed from tmux -- window_name, Pane label, provider, command, shell, topic, or title -- is an identity input. The observed window_name is projected separately onto metadata.displayName.

func ManagedPaneNameBase

func ManagedPaneNameBase(agentName string) string

ManagedPaneNameBase is the "<agent-name>-pane" base used for a Pane managed by an Agent.

func MigrateRegistryWithEnvironment added in v0.13.0

func MigrateRegistryWithEnvironment(set MigrationSet, reg Registry, env MigrationEnvironment) (Registry, bool, MigrationReport, error)

MigrateRegistryWithEnvironment lifts reg with an explicit migration set, filesystem and uid inputs, and the complete repair report.

func NeedsTerminationProjection added in v0.13.0

func NeedsTerminationProjection(reg Registry, paneUID string) bool

NeedsTerminationProjection reports whether one absent Pane still has work left for the projection.

It is the pre-transaction dirty check, and its job is to make a repeat pass cost zero write transactions rather than one that happens to change nothing. There are exactly two kinds of outstanding work: evidence that has not been recorded, and an Agent still bound to the dead Pane that can still move. A Pane with its evidence stored and no live binding left is finished.

func NewGeneration added in v0.13.0

func NewGeneration() (string, error)

NewGeneration mints one opaque activation generation.

A generation names one *materialization* of a Pane, not the Pane. The uid survives kill/recreate and resume; the generation does not, and that is the whole point: a receipt carries the generation it was launched with, so a receipt from the process a resume replaced can be recognized as stale instead of being applied to the Pane that now holds the uid.

func NewUID

func NewUID(kind Kind) (string, error)

NewUID mints an opaque Projmux identity for kind. The value is independent of tmux lifecycle, of the resource's name, and of its root path, so it survives snapshot/restore and rebind unchanged.

Entropy comes from crypto/rand; callers that need determinism inject their own generator through Mutator.NewUID.

func NormalizeProvider

func NormalizeProvider(provider string) string

NormalizeProvider maps a provider spelling onto its registered id (codex, claude, antigravity) and returns "" when it is unknown.

func PaneNameBase

func PaneNameBase(command, shell string) string

PaneNameBase applies the shell Pane naming order: command basename, configured shell basename, then "pane".

func ProjectDisplayName

func ProjectDisplayName(root string) string

ProjectDisplayName derives the duplicate-allowed Project display name.

func ProjectNameBase

func ProjectNameBase(root string) string

ProjectNameBase derives the Project name base from its root basename. Project displayName seeds from the same value and may duplicate.

func SanitizeNameBase

func SanitizeNameBase(seed string) string

SanitizeNameBase turns an arbitrary seed into a valid name base. Runes that cannot appear in a name collapse into a single `-`. An empty result means the caller must fall back to the next seed in its priority order.

func ValidAgentActivationReason added in v0.11.1

func ValidAgentActivationReason(reason string) bool

func ValidAgentInteractionKind added in v0.11.1

func ValidAgentInteractionKind(kind AgentInteractionKind) bool

ValidAgentInteractionKind reports whether kind is in the public closed set.

func ValidAgentInteractionSource added in v0.11.1

func ValidAgentInteractionSource(source string) bool

ValidAgentInteractionSource reports whether source is safe durable metadata. Empty remains readable for registries written before sources were introduced, but every new mutation must choose one of the closed values.

func ValidAgentPhase

func ValidAgentPhase(phase AgentPhase) bool

ValidAgentPhase reports whether phase is in the closed phase set.

func ValidAgentProgressActivity added in v0.13.0

func ValidAgentProgressActivity(activity AgentProgressActivity) bool

func ValidTerminationClassification added in v0.13.0

func ValidTerminationClassification(classification TerminationClassification) bool

ValidTerminationClassification reports whether classification is in the closed set.

func ValidTerminationSource added in v0.13.0

func ValidTerminationSource(source TerminationSource) bool

ValidTerminationSource reports whether source is in the closed set.

func ValidateName

func ValidateName(name string) error

ValidateName rejects names that cannot serve as a stable, unambiguous query key. Case is preserved: `Projmux` and `projmux` are distinct names.

func WindowNameBase

func WindowNameBase(explicit, command, shell string) string

WindowNameBase applies the one-time Window naming order: explicit name, initial command basename, configured shell basename, then "window". Agent topic and raw pane title are deliberately excluded as name seeds.

Types

type AdoptionKind

type AdoptionKind string

AdoptionKind is the outcome of matching one live tmux object against the registry.

const (
	// AdoptionRebind means the live object already carries a uid the registry
	// knows and the scope owns. Nothing is adopted and nothing is
	// re-identified; the caller reapplies the binding it already had.
	AdoptionRebind AdoptionKind = "rebind"
	// AdoptionAdopt means the live object carries no uid and the next eligible
	// unbound registry object of the scope, in creation order, takes it.
	AdoptionAdopt AdoptionKind = "adopt"
	// AdoptionUnmatched means the live object carries no uid and the scope has
	// no eligible registry object left. Both write paths create here, but not
	// for the same kinds. The import path mints whichever object is missing,
	// Window or Pane, because its `@projmux_project_path` anchor is what makes
	// minting a whole topology safe. The binding-repair path has no anchor, so
	// it mints a Pane only, and only inside a Window it has already paired --
	// a live window it could not match is still left exactly as it was found.
	AdoptionUnmatched AdoptionKind = "unmatched"
	// AdoptionForeign means the live object carries a uid this registry has
	// never heard of. It is never adopted -- pointing an existing registry
	// object at it would be re-identification on the strength of a blank
	// lookup, which is precisely the heuristic uid merge the contract forbids.
	//
	// It is treated as unmatched by whichever path is allowed to create there,
	// which mints a fresh object for it rather than reusing one. That is not a
	// re-identification: no
	// registry uid changes, a new one is allocated. And it is the only reading
	// that does not strand the machine, because projmux itself produces
	// unknown uids -- a reconcile whose transaction is later rolled back by a
	// pre-create hook refusal has already written its allocated uids onto tmux,
	// and tmux options are not transactional. Refusing those outright would
	// leave the operator's windows permanently unmanageable.
	AdoptionForeign AdoptionKind = "foreign"
	// AdoptionRefused means the pairing is ambiguous in a way that has a real
	// registry object on the other side of it. The caller must neither adopt
	// nor create nor write: a refusal is a decision to leave a live tmux object
	// exactly as it was found, because claiming it would take a binding that
	// belongs to something else.
	AdoptionRefused AdoptionKind = "refused"
)

type AdoptionMatch

type AdoptionMatch struct {
	Kind AdoptionKind
	UID  string
}

AdoptionMatch is one matching decision. UID is set only for AdoptionRebind and AdoptionAdopt.

func (AdoptionMatch) Matched

func (m AdoptionMatch) Matched() bool

Matched reports whether the decision names a registry object the caller should write a binding for.

type Agent

type Agent struct {
	APIVersion string      `json:"apiVersion"`
	Kind       Kind        `json:"kind"`
	Metadata   ObjectMeta  `json:"metadata"`
	Spec       AgentSpec   `json:"spec"`
	Status     AgentStatus `json:"status"`
}

Agent is owned by a Window and owns its current managed Pane.

func (Agent) Clone

func (a Agent) Clone() Agent

Clone returns a deep copy of the Agent.

func (Agent) EffectiveInteraction added in v0.11.1

func (a Agent) EffectiveInteraction(now time.Time) AgentInteraction

EffectiveInteraction returns the current read model without erasing durable history. Non-running lifecycle, missing pane binding, and stale observations are all unknown and therefore cannot retain a response-complete badge.

type AgentActivation added in v0.11.1

type AgentActivation struct {
	State      AgentActivationState `json:"state"`
	ObservedAt time.Time            `json:"observedAt,omitzero"`
	Source     string               `json:"source,omitempty"`
	Reason     string               `json:"reason,omitempty"`
}

AgentActivation is bounded launch metadata. It never contains prompt text, provider credentials, or pane content.

func (AgentActivation) IsZero added in v0.11.1

func (a AgentActivation) IsZero() bool

type AgentActivationState added in v0.11.1

type AgentActivationState string

AgentActivationState separates Pane creation from initial-task activation.

const (
	ActivationNotRequested   AgentActivationState = "not_requested"
	ActivationPending        AgentActivationState = "pending"
	ActivationAcknowledged   AgentActivationState = "acknowledged"
	ActivationUnconfirmed    AgentActivationState = "unconfirmed"
	ActivationReasonTimedOut                      = "provider activation acknowledgement timed out"
	ActivationReasonFailed                        = "provider activation acknowledgement failed"
)

type AgentExit

type AgentExit string

AgentExit classifies why an Agent stopped owning its managed Pane.

const (
	// AgentExitNormal is a normal managed-Pane exit. It resolves to Offline.
	AgentExitNormal AgentExit = "normal"
	// AgentExitDeleted is an explicit pane deletion. It resolves to Offline.
	AgentExitDeleted AgentExit = "deleted"
	// AgentExitAbnormal is an abnormal managed-Pane exit. It resolves to Failed.
	AgentExitAbnormal AgentExit = "abnormal"
	// AgentExitLaunchFailure is a failure to launch. It resolves to Failed.
	AgentExitLaunchFailure AgentExit = "launch-failure"
	// AgentExitUnknown is a managed-Pane disappearance no receipt explains. It
	// resolves to Offline.
	//
	// Offline rather than Failed is a deliberate asymmetry. The phase is what an
	// operator reads to decide whether to resume, and an unproven Failed is
	// worse for that decision than an honest Offline: the evidence that the
	// answer is unproven is carried by status.lastTermination, where it can be
	// read without being mistaken for a diagnosis.
	AgentExitUnknown AgentExit = "unknown"
)

func (AgentExit) Phase

func (e AgentExit) Phase() (AgentPhase, bool)

Phase maps an exit classification onto the resulting Agent phase.

type AgentInteraction added in v0.11.1

type AgentInteraction struct {
	Kind       AgentInteractionKind `json:"kind"`
	ObservedAt time.Time            `json:"observedAt,omitzero"`
	Source     string               `json:"source,omitempty"`
}

AgentInteraction is the last durable semantic observation. Readers use Agent.EffectiveInteraction to invalidate it for an Offline/Failed Agent or after the bounded freshness window; history may remain durable without being presented as current state.

func (AgentInteraction) IsZero added in v0.11.1

func (i AgentInteraction) IsZero() bool

type AgentInteractionKind added in v0.11.1

type AgentInteractionKind string

AgentInteractionKind is the provider-neutral closed interaction vocabulary. It is intentionally separate from AgentPhase: interaction describes what a live provider is waiting on, while phase describes whether the Agent has a live managed Pane at all.

const (
	InteractionUnknown          AgentInteractionKind = "unknown"
	InteractionIdle             AgentInteractionKind = "idle"
	InteractionInProgress       AgentInteractionKind = "in_progress"
	InteractionApprovalRequired AgentInteractionKind = "approval_required"
	InteractionInputRequired    AgentInteractionKind = "input_required"
	InteractionResponseComplete AgentInteractionKind = "response_complete"
)

func AgentInteractionKinds added in v0.11.1

func AgentInteractionKinds() []AgentInteractionKind

AgentInteractionKinds returns the closed semantic set in neutral order.

type AgentInteractionSource added in v0.11.1

type AgentInteractionSource string

AgentInteractionSource is the closed provenance vocabulary for durable semantic observations. It is intentionally not caller supplied: arbitrary strings here would turn status metadata into a prompt/credential sink.

const (
	InteractionSourceManual          AgentInteractionSource = "manual"
	InteractionSourceCompatibilityAI AgentInteractionSource = "compatibility-ai"
	InteractionSourceProviderHook    AgentInteractionSource = "provider-hook"
	InteractionSourceProviderControl AgentInteractionSource = "provider-control-plane"
	InteractionSourceLifecycle       AgentInteractionSource = "lifecycle"
)

func AgentInteractionSources added in v0.11.1

func AgentInteractionSources() []AgentInteractionSource

type AgentLinkKind

type AgentLinkKind string

AgentLinkKind is the outcome of linking one live agent pane to an Agent.

const (
	// AgentLinkNone means the pane carries no agent authorship marker, or the
	// registry state refused the link. Nothing was written.
	AgentLinkNone AgentLinkKind = "none"
	// AgentLinkRebound means the Pane was already owned by an Agent. Only the
	// reverse pointer, status.paneRef, was repaired.
	AgentLinkRebound AgentLinkKind = "rebound"
	// AgentLinkAttached means an existing Agent recording the same provider
	// conversation took the Pane.
	AgentLinkAttached AgentLinkKind = "attached"
	// AgentLinkMinted means no Agent existed for this conversation and one was
	// created.
	AgentLinkMinted AgentLinkKind = "minted"
)

type AgentLinkage

type AgentLinkage struct {
	Kind AgentLinkKind
	// AgentUID is set for every kind except AgentLinkNone.
	AgentUID string
	// Promoted reports whether the Pane moved from Window-owned shell to
	// Agent-owned managed.
	Promoted bool
}

AgentLinkage reports one linkage decision.

func (AgentLinkage) Linked

func (l AgentLinkage) Linked() bool

Linked reports whether the decision named an Agent.

type AgentPaneAuthority added in v0.13.0

type AgentPaneAuthority string

AgentPaneAuthority is the closed authorship table for one observed Pane. Presentation/provider markers are observations; only the distinct canonical launch receipt authorizes a Window-shell promotion.

const (
	AgentPaneAuthorityNoMarker  AgentPaneAuthority = "no-marker"
	AgentPaneAuthorityHookOnly  AgentPaneAuthority = "hook-only"
	AgentPaneAuthorityLaunch    AgentPaneAuthority = "launch-authorship"
	AgentPaneAuthorityAmbiguous AgentPaneAuthority = "ambiguous"
)

func ResolveAgentPaneAuthority added in v0.13.0

func ResolveAgentPaneAuthority(observed LegacyPane) AgentPaneAuthority

ResolveAgentPaneAuthority classifies the complete launch/provider marker pair without consulting command, title, cwd, or names.

type AgentPhase

type AgentPhase string

AgentPhase is the closed Agent lifecycle state set.

const (
	// PhasePending is an Agent whose managed Pane has not started yet.
	PhasePending AgentPhase = "Pending"
	// PhaseRunning is an Agent with a live managed Pane.
	PhaseRunning AgentPhase = "Running"
	// PhaseOffline follows a normal managed-Pane exit or an explicit pane
	// deletion. The Agent survives as a resumable resource.
	PhaseOffline AgentPhase = "Offline"
	// PhaseFailed follows a launch failure or an abnormal exit.
	PhaseFailed AgentPhase = "Failed"
)

func AgentPhases

func AgentPhases() []AgentPhase

AgentPhases returns the closed phase set in lifecycle order.

type AgentProgress added in v0.13.0

type AgentProgress struct {
	TurnRef         string                `json:"turnRef"`
	Activity        AgentProgressActivity `json:"activity,omitempty"`
	PlanCompleted   uint8                 `json:"planCompleted,omitempty"`
	PlanInProgress  uint8                 `json:"planInProgress,omitempty"`
	PlanTotal       uint8                 `json:"planTotal,omitempty"`
	PlanTruncated   bool                  `json:"planTruncated,omitempty"`
	ChangedFiles    uint16                `json:"changedFiles,omitempty"`
	FilesTruncated  bool                  `json:"filesTruncated,omitempty"`
	ActiveItemCount uint8                 `json:"activeItemCount,omitempty"`
	StartedAt       time.Time             `json:"startedAt,omitzero"`
	ObservedAt      time.Time             `json:"observedAt,omitzero"`
	Source          string                `json:"source"`
}

AgentProgress is the current exact-turn projection. It intentionally has no history and no fields capable of retaining provider content. TurnRef is not an independent identity: write sites must prove it is the exact turn in the current Pane activation binding before committing this value.

func (AgentProgress) IsZero added in v0.13.0

func (p AgentProgress) IsZero() bool

type AgentProgressActivity added in v0.13.0

type AgentProgressActivity string

AgentProgressActivity is the provider-neutral, content-free activity vocabulary for one active Agent turn. Providers may only select one of these values from a wire discriminator; names, commands, paths, and payload text never participate in the selection.

const (
	ProgressPlanning   AgentProgressActivity = "planning"
	ProgressCommand    AgentProgressActivity = "command"
	ProgressFileChange AgentProgressActivity = "file-change"
	ProgressTool       AgentProgressActivity = "tool"
	ProgressWebSearch  AgentProgressActivity = "web-search"
	ProgressDelegation AgentProgressActivity = "delegation"
	ProgressImage      AgentProgressActivity = "image"
	ProgressReview     AgentProgressActivity = "review"
	ProgressCompaction AgentProgressActivity = "compaction"
	ProgressOther      AgentProgressActivity = "other"
)

func AgentProgressActivities added in v0.13.0

func AgentProgressActivities() []AgentProgressActivity

AgentProgressActivities returns the closed activity set in presentation order. Empty is reserved for a progress value with no current safe item.

type AgentSessionObservation

type AgentSessionObservation struct {
	Provider       string
	SessionID      string
	ThreadID       string
	TranscriptPath string
}

AgentSessionObservation is the raw identifier set one provider hook handed over. It is the ingest-side input shape; folding it onto the right union member is this package's job so the app layer never encodes provider shape knowledge of its own.

type AgentSessionRef

type AgentSessionRef struct {
	// Provider is the normalized provider id and is the union discriminator.
	// It always matches the populated member.
	Provider string `json:"provider"`
	// ObservedAt is when the reporting hook was ingested. It is an observation
	// timestamp about projmux, not a timestamp the provider supplied.
	ObservedAt time.Time `json:"observedAt"`

	Claude      *ClaudeSessionRef      `json:"claude,omitempty"`
	Codex       *CodexSessionRef       `json:"codex,omitempty"`
	Antigravity *AntigravitySessionRef `json:"antigravity,omitempty"`
}

AgentSessionRef is the durable pointer from an Agent to the provider conversation that Agent belongs to.

It lives in status, not spec, because nothing declares it. A provider hook reports it after the fact and the Agent keeps whatever was last observed — exactly the contract status.paneRef already has. The difference between the two is the question each answers: status.paneRef is the *current* managed Pane binding and is cleared the moment that Pane is released, while this ref answers "which conversation is this Agent" and deliberately survives ReleaseAgentPane, so an Offline Agent still knows what it was.

This is also not a duplicate of the tmux pane option `@projmux_ai_session_id`. That option is a *live routing index*: hook ingest scans the live pane list and matches on it to decide which pane an incoming event belongs to, so following pane lifetime is correct for it. This field is the durable conversation pointer and must outlive the Pane.

The shape is a per-provider discriminated union rather than one flat string or one shared bag of ids. Providers do not agree on what identifies a conversation: Claude reports a session id plus a transcript path, Codex reports a thread id and a session id, Antigravity reports a conversation id. Flattening them would either assert a false equivalence between a Codex thread id and a Claude session id, or leave a reader holding an opaque string with no way to tell which spelling it has. Provider is the discriminator and exactly one member is populated.

func NewAgentSessionRef

func NewAgentSessionRef(obs AgentSessionObservation, observedAt time.Time) (*AgentSessionRef, bool)

NewAgentSessionRef folds one hook observation onto the provider member that owns it.

It reports false — rather than returning a half-populated ref — when the provider is not a known provider id or when the observation carries no usable conversation identifier at all. A hook that fires before the provider has a conversation id is normal, and recording an empty pointer would be worse than recording nothing.

func (*AgentSessionRef) Clone

func (r *AgentSessionRef) Clone() *AgentSessionRef

Clone returns a deep copy. Every member is a pointer, so a shallow copy would let two registry snapshots alias the same conversation record.

func (*AgentSessionRef) ConversationID

func (r *AgentSessionRef) ConversationID() string

ConversationID returns the identifier that names the conversation for the populated provider. It is the one value a human reads to recognize which conversation an Agent belongs to.

func (*AgentSessionRef) Empty

func (r *AgentSessionRef) Empty() bool

Empty reports whether the ref points at no conversation at all.

func (*AgentSessionRef) Fields

func (r *AgentSessionRef) Fields() [][2]string

Fields returns the populated provider's identifier set as ordered display key/value pairs. The keys differ per provider on purpose: that difference is the whole reason the shape is a union rather than one flat record.

func (*AgentSessionRef) SameConversation

func (r *AgentSessionRef) SameConversation(other *AgentSessionRef) bool

SameConversation reports whether two refs point at the same conversation. ObservedAt is deliberately excluded: it records when projmux last saw the conversation, not which conversation it is, so a re-observation of the same ids is not a change and must not trigger a registry write.

func (*AgentSessionRef) Summary

func (r *AgentSessionRef) Summary() string

Summary renders the provider-qualified conversation pointer for a one-line human projection.

type AgentSpec

type AgentSpec struct {
	Provider  string         `json:"provider,omitempty"`
	Workspace AgentWorkspace `json:"workspace,omitzero"`
}

AgentSpec records the normalized provider id the Agent was created for.

type AgentStatus

type AgentStatus struct {
	Phase       AgentPhase       `json:"phase"`
	PaneRef     string           `json:"paneRef,omitempty"`
	SessionRef  *AgentSessionRef `json:"sessionRef,omitempty"`
	Interaction AgentInteraction `json:"interaction,omitzero"`
	Activation  AgentActivation  `json:"activation,omitzero"`
	Progress    AgentProgress    `json:"progress,omitzero"`
	Reason      string           `json:"reason,omitempty"`
	// LastTermination mirrors the receipt recorded against the Agent's current
	// managed Pane, so the evidence survives the Pane resource a canonical
	// delete removes.
	LastTermination  *TerminationEvidence `json:"lastTermination,omitempty"`
	LastTransitionAt time.Time            `json:"lastTransitionAt"`
}

AgentStatus tracks the lifecycle phase, the current managed Pane uid, and the durable pointer to the provider conversation the Agent belongs to.

PaneRef and SessionRef have deliberately different lifetimes. PaneRef is the *current* binding and is cleared by ReleaseAgentPane, DeletePane, and every non-Running transition. SessionRef is the durable conversation pointer and is never cleared by any of them: an Offline Agent that has lost its Pane still knows which conversation it is.

SessionRef is an optional pointer with omitempty. That is the whole read-compatibility story for registry files written before it existed: an absent key decodes to nil, a nil ref re-encodes to an absent key, and the document round-trips byte-identically. It was additive inside schemaVersion 1 and needed no migration step — bumping the envelope would have made every already installed build reject the file fail-closed with ErrSchemaTooNew, which is a hard downgrade break bought for nothing.

One conversation may be pointed at by more than one Agent. That is NOT prevented, and the registry treats it as legal state; see the note on NewAgentSessionRef and the Agent section of docs/agent-workflow.md for the reasoning.

type AgentWorkspace added in v0.11.1

type AgentWorkspace struct {
	CWD                     string   `json:"cwd"`
	AdditionalWritableRoots []string `json:"additionalWritableRoots,omitempty"`
}

AgentWorkspace is the provider-neutral effective filesystem contract of one Agent launch. Project ownership remains on the owning Window; these paths only describe where the provider process is allowed to work.

func (AgentWorkspace) IsZero added in v0.11.1

func (w AgentWorkspace) IsZero() bool

IsZero lets old registry documents omit the additive workspace block.

type AntigravitySessionRef

type AntigravitySessionRef struct {
	ConversationID string `json:"conversationId"`
	TranscriptPath string `json:"transcriptPath,omitempty"`
}

AntigravitySessionRef is Antigravity's conversation identity as its hook reports it. Antigravity carries a single conversation id and reports it in both the thread and session slots of the shared hook seam.

type AssetDisposition added in v0.13.0

type AssetDisposition string

AssetDisposition makes every external-asset cell explicit.

const (
	AssetPreserve      AssetDisposition = "preserve"
	AssetNotApplicable AssetDisposition = "not-applicable"
)

type BindingMatcher

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

BindingMatcher pairs the live tmux objects seen during one reconciliation pass with registry objects.

It is pass-scoped, not session-scoped, and that is load bearing: a registry Window may be the runtime of exactly one live tmux window, so once a walk has claimed it -- by rebinding it or by adopting it -- no later walk in the same pass may claim it again, even from a different session that resolves to the same Project.

The zero value is not usable; build one with NewBindingMatcher.

func NewApprovedOrphanBindingMatcher added in v0.13.0

func NewApprovedOrphanBindingMatcher(runtime RuntimeObservation) *BindingMatcher

NewApprovedOrphanBindingMatcher permits only an exact unknown mirrored uid to reach AdoptionForeign. Blank D2 objects remain refused.

func NewBindingMatcher

func NewBindingMatcher(runtime RuntimeObservation) *BindingMatcher

NewBindingMatcher builds a matcher over one pre-pass live-tmux observation.

An empty observation is the fail-closed reading: nothing is known to be bound elsewhere, so adoption is decided purely by scope, ordinal, and what this pass has already claimed. That is the same tolerance the rest of reconcile extends to a tmux server that is down -- it can never invent a binding, only decline to protect one that no longer exists.

func NewRepairBindingMatcher

func NewRepairBindingMatcher(runtime RuntimeObservation) *BindingMatcher

NewRepairBindingMatcher builds the fail-closed matcher used by explicit resource reconciliation. Unknown live uids are diagnostic evidence, not an invitation to mint a replacement identity.

func (*BindingMatcher) Claim

func (b *BindingMatcher) Claim(uid string)

Claim marks a registry uid as paired for the rest of the pass.

The import path calls it for every object it mints. Without that, a Window created for the third live tmux window of a session would still be an unclaimed candidate when the fourth is matched, and the fourth would adopt the Window that was just created for the third.

func (*BindingMatcher) Claimed

func (b *BindingMatcher) Claimed(uid string) bool

Claimed reports whether this pass already paired a registry uid.

Agent runtime linkage reads it for the same reason the pane and window walks keep the set at all: one registry object is the runtime of at most one live tmux object, so an Agent a previous pane of this pass already attached to is no longer a candidate for the next one. Agent uids share this set with Window and Pane uids because uids are globally unique across kinds, which the registry's own Validate enforces.

func (*BindingMatcher) MatchPane

func (b *BindingMatcher) MatchPane(reg *Registry, windowUID, observedUID string) AdoptionMatch

MatchPane decides which registry Pane of windowUID a live tmux pane is the runtime of. It is only ever called for a Window that was itself matched: a live tmux window nobody adopted contributes none of its panes.

func (*BindingMatcher) MatchWindow

func (b *BindingMatcher) MatchWindow(reg *Registry, projectUID, observedUID string) AdoptionMatch

MatchWindow decides which registry Window of projectUID a live tmux window is the runtime of. observedUID is the `@projmux_window_uid` the live window already carries, empty when it carries none.

type BootstrapPane

type BootstrapPane struct {
	// Name is an explicit pane name. Empty means automatic naming.
	Name string
	// Command is the one-time name-derivation source for the Pane.
	Command string
	// CWD is the declared pane working directory.
	CWD string
	// Labels are creation-time key/value classification metadata. They are a
	// creation option, never a selector input for the resource being created.
	Labels map[string]string
}

BootstrapPane declares one Pane of a Project startup topology.

type BootstrapWindow

type BootstrapWindow struct {
	Name    string
	Command string
	// Labels are creation-time key/value classification metadata.
	Labels map[string]string
	Panes  []BootstrapPane
}

BootstrapWindow declares one Window of a Project startup topology. Every Window owns an initial Pane; an empty Panes list gets one default shell Pane.

type ClaudeSessionRef

type ClaudeSessionRef struct {
	SessionID      string `json:"sessionId"`
	TranscriptPath string `json:"transcriptPath,omitempty"`
}

ClaudeSessionRef is Claude's conversation identity as its hook reports it.

TranscriptPath is stored as a path only. Nothing in projmux reads the transcript contents to populate this ref, and nothing may start: parsing another tool's conversation store is permanently out of scope. Only what the hook hands over is recorded.

type CodexActivationBinding added in v0.13.0

type CodexActivationBinding struct {
	ThreadID string `json:"threadId"`
	TurnID   string `json:"turnId,omitempty"`
}

CodexActivationBinding is the content-free native identity returned by the local app-server. TurnID is optional for an interactive create/resume that has not started a turn yet.

type CodexActivationObservation added in v0.13.0

type CodexActivationObservation struct {
	AgentUID   string
	PaneUID    string
	Generation string
	ThreadID   string
	TurnID     string
}

CodexActivationObservation is the complete identity a native app-server create/resume or later exact event is allowed to commit. It is deliberately separate from AgentSessionObservation: turn identity is scoped to one Pane generation and never becomes a durable conversation pointer.

type CodexSessionRef

type CodexSessionRef struct {
	ThreadID  string `json:"threadId,omitempty"`
	SessionID string `json:"sessionId,omitempty"`
}

CodexSessionRef is Codex's conversation identity as its hook reports it.

Codex also reports a turn id. It is deliberately absent: a turn id addresses one turn inside the conversation and changes on every hook event, so it does not point at the conversation. Storing it would give the Agent a durable field that is stale the moment it is written.

type Condition

type Condition struct {
	Type             string    `json:"type"`
	Status           string    `json:"status"`
	Reason           string    `json:"reason,omitempty"`
	Message          string    `json:"message,omitempty"`
	FirstObservedAt  time.Time `json:"firstObservedAt"`
	LastTransitionAt time.Time `json:"lastTransitionAt"`
}

Condition records an observed resource condition with its first-observed timestamp preserved across repeat observations.

type ControlSession added in v0.13.0

type ControlSession struct {
	APIVersion string               `json:"apiVersion"`
	Kind       Kind                 `json:"kind"`
	Metadata   ObjectMeta           `json:"metadata"`
	Spec       ControlSessionSpec   `json:"spec"`
	Status     ControlSessionStatus `json:"status,omitzero"`
}

ControlSession is the app-owned control session as a Registry root.

It is the second thing a Window may be owned by, and the only root kind that carries no filesystem path. The tmux session it is bound to is named in spec.session and is matched exactly, never by basename, cwd, or a name heuristic: an operator's session that happens to be called `home` on a server projmux does not own is not this resource, and the marker read in internal/core/resourcegraph is what decides that at observation time.

It deliberately has no status.session projection either. A Project's session projection exists so an offline Project remembers which session name it will come back as; a control session's whole identity *is* that name, so a second copy of it in status would be a field that can disagree with spec.

func (ControlSession) Clone added in v0.13.0

func (c ControlSession) Clone() ControlSession

Clone returns a deep copy of the ControlSession.

type ControlSessionBinding added in v0.13.0

type ControlSessionBinding struct {
	ControlSession ControlSession
	// Reused is true when the exact session name already had a ControlSession.
	Reused      bool
	Windows     []ImportedWindow
	Panes       []ImportedPane
	OperationID string
	Created     []string
}

ControlSessionBinding is the outcome of one control-session bind.

Windows and Panes report every object that was bound to a live tmux object, created or not, because the adapter owes all of them a mirror write. Created reports only what the transaction minted, so a rollback removes exactly what this operation brought into existence and never an adopted object that predates it. Both properties are the ImportResult contract, restated here so the two paths can never drift into different adapter expectations.

type ControlSessionObservation added in v0.13.0

type ControlSessionObservation struct {
	// Session is the exact tmux session name. It is the control session's
	// identity, matched verbatim, never by basename or cwd.
	Session string
	// Windows are the session's live windows in window_index ascending order,
	// which is the ordinal adoption aligns against.
	Windows []ControlSessionWindow
}

ControlSessionObservation is one live observation of an app-owned control session: the exact session name plus its windows and panes in tmux order.

It is a value, not a reader, for the same reason resourcegraph.Inventory is: the binding decision stays pure and a test can state a machine state directly instead of scripting tmux output.

The caller is responsible for having proved the session is a control session before it builds one of these. Two facts decide that and neither is observable from this struct: the server carries `@projmux_app=1`, and the exact session's `@projmux_session_role` is exactly `control`. See internal/app/control_session.go for the writer-side guard and internal/core/resourcegraph for the reader-side one.

type ControlSessionPane added in v0.13.0

type ControlSessionPane struct {
	// UID is the `@projmux_pane_uid` the live pane already carries, empty when
	// it carries none.
	UID string
	// Name is the `@projmux_pane_label` mirror, the highest-priority name seed
	// for a minted Pane.
	Name string
	// Command is `pane_current_command`, the one-time name-derivation source.
	Command string
	// Title is `pane_title`, a derived display source only.
	Title string
	// CWD is `pane_current_path`. It is recorded verbatim; a control session has
	// no root to fall back to, so an unreadable cwd stays empty rather than
	// borrowing $HOME.
	CWD string
}

ControlSessionPane is one observed pane of a control session window.

type ControlSessionSpec added in v0.13.0

type ControlSessionSpec struct {
	Session string `json:"session"`
}

ControlSessionSpec names the tmux session this control session is bound to.

There is no Root field and there must never be one. See KindControlSession.

type ControlSessionStatus added in v0.13.0

type ControlSessionStatus struct {
	Conditions []Condition `json:"conditions,omitempty"`
}

ControlSessionStatus carries the observed conditions of one control session.

It is `omitzero` for the same read-compatibility reason WindowStatus is: a control session that has never carried a condition serializes without the key, so the block was additive when introduced in schemaVersion 1.

type ControlSessionWindow added in v0.13.0

type ControlSessionWindow struct {
	// DisplayName is the tmux window_name. It is projected onto the
	// duplicate-allowed metadata.displayName and is never a name seed.
	DisplayName      string
	RuntimeSessionID string
	RuntimeID        string
	// UID is the `@projmux_window_uid` the live window already carries, empty
	// when it carries none.
	UID string
	// Panes are the window's live panes in pane order.
	Panes []ControlSessionPane
}

ControlSessionWindow is one observed window of a control session.

type CreateAgentOptions

type CreateAgentOptions struct {
	// Name is an explicit --name. A collision fails with ErrNameConflict.
	Name string
	// Provider is the raw provider spelling; it is normalized to codex,
	// claude, or antigravity, and falls back to the "agent" name base when
	// unknown.
	Provider    string
	DisplayName string
	Labels      map[string]string
	Annotations map[string]string
	Workspace   AgentWorkspace
	Activation  AgentActivationState
	OperationID string
}

CreateAgentOptions is the input to offline Agent creation.

type ExternalAssetOutcome added in v0.13.0

type ExternalAssetOutcome struct {
	RootDirectory AssetDisposition
	GitMetadata   AssetDisposition
	Worktrees     AssetDisposition
	SnapshotBytes AssetDisposition
}

ExternalAssetOutcome is the non-Registry boundary of a root decision.

type ImportOrigin

type ImportOrigin string

ImportOrigin records how one reported Window or Pane came to be reported.

The distinction matters to exactly two callers and for opposite reasons. The tmux adapter treats all three identically -- every reported object gets its binding written through the one mirror write path, because a created object has no binding yet and an adopted or rebound one has a binding that may have been wiped. The transaction ledger treats only ImportCreated as created: an adopted object existed before this operation, so rolling the operation back must not delete it.

const (
	// ImportCreated is a Window or Pane this import minted.
	ImportCreated ImportOrigin = "created"
	// ImportAdopted is a pre-existing registry object this import paired with a
	// live tmux object that carried no uid.
	ImportAdopted ImportOrigin = "adopted"
	// ImportRebound is a pre-existing registry object whose live tmux object
	// still carried its uid. Reported so its binding is reapplied.
	ImportRebound ImportOrigin = "rebound"
)

type ImportResult

type ImportResult struct {
	Project       Project
	ProjectReused bool
	Windows       []ImportedWindow
	Panes         []ImportedPane
	Agents        []ImportedAgent
	OperationID   string
	Created       []string
}

ImportResult is the outcome of one legacy session import.

Windows and Panes report every object the import bound to a live tmux object, created or not, because the adapter owes all of them a mirror write. Created reports only what the transaction minted, so a rollback still removes exactly what this operation brought into existence and never an adopted object that predates it.

type ImportedAgent

type ImportedAgent struct {
	UID         string
	Name        string
	PaneUID     string
	WindowIndex int
	PaneIndex   int
}

ImportedAgent reports one Agent created by a legacy import.

type ImportedPane

type ImportedPane struct {
	UID         string
	Name        string
	WindowIndex int
	PaneIndex   int
	// Origin distinguishes a minted Pane from an adopted or rebound one.
	Origin ImportOrigin
}

ImportedPane reports one Pane a legacy import bound to a live tmux pane.

type ImportedWindow

type ImportedWindow struct {
	UID         string
	Name        string
	SourceIndex int
	// NeedsAutomaticRenameOff is true for every managed Window, so a
	// focused-Pane change can never overwrite the Window name.
	NeedsAutomaticRenameOff bool
	// Origin distinguishes a minted Window from an adopted or rebound one.
	Origin ImportOrigin
}

ImportedWindow reports one Window a legacy import bound to a live tmux window, together with the transport work the tmux adapter still owes it.

type InputError

type InputError struct {
	Op     string
	Detail string
	Cause  error
}

InputError is a typed metadata error caused by invalid user input. It carries the operation, a human-readable detail, and the sentinel cause.

func (*InputError) Error

func (e *InputError) Error() string

Error renders "<op>: <detail>" with the sentinel cause appended when the detail does not already state it.

func (*InputError) MetadataUsageError

func (e *InputError) MetadataUsageError() bool

MetadataUsageError marks this error as a usage error for IsUsageError.

func (*InputError) Unwrap

func (e *InputError) Unwrap() error

Unwrap exposes the sentinel cause to errors.Is.

type Kind

type Kind string

Kind is the closed set of Projmux resource kinds. A persistent tmux Session is intentionally absent: it is a 1:1 runtime projection of a Project stored in Project.status.session and owns no uid, name, or ownerRef of its own.

const (
	KindProject Kind = "Project"
	KindWindow  Kind = "Window"
	KindPane    Kind = "Pane"
	KindAgent   Kind = "Agent"
	// KindControlSession is the app-owned control session -- the Home session
	// `projmux shell` opens -- as an OWNER of Windows and Panes.
	//
	// It is a fifth root kind rather than a Project with a special role for one
	// reason that no flag can reproduce: a control session owns no filesystem
	// path, and the type below has no field to put one in. Every path-based
	// surface projmux has (managed roots, trust, rebind, cwd defaults,
	// ProjectByRoot) reads Project.Spec.Root, so a control session cannot leak
	// into any of them by omission, by a forgotten filter, or by a later
	// refactor -- the leak would not compile. $HOME is therefore permanently
	// incapable of becoming a Project or a managed root through this kind.
	KindControlSession Kind = "ControlSession"
)

func Kinds

func Kinds() []Kind

Kinds returns the closed kind set in declaration order.

func UIDKind

func UIDKind(uid string) (Kind, bool)

UIDKind reports the kind encoded in a uid prefix. It is a debugging aid; ownership and lookups always go through the registry, never through the prefix.

type LegacyPane

type LegacyPane struct {
	Label    string
	Provider string
	// LaunchAuthorship is the raw canonical Projmux launch receipt. Only the
	// exact value "1" together with a provider authorizes topology promotion.
	// It is deliberately independent of hook/provider presentation markers.
	LaunchAuthorship string
	// Topic is carried for the derived display title only. It is never a name
	// seed for a Pane, a Window, or an Agent.
	Topic   string
	Command string
	Title   string
	CWD     string
	// UID is the `@projmux_pane_uid` the live tmux pane already carries, empty
	// when it carries none. It is a binding, never a name seed: adoption reads
	// it to tell "already ours" from "blank", and refuses either way to
	// re-identify anything.
	UID string
	// SessionID and ThreadID are the provider conversation identifiers the AI
	// routes wrote onto the live pane. They are read for exactly one decision --
	// whether an Agent that already records the same conversation in
	// status.sessionRef is the one this live pane belongs to -- and are never a
	// name seed, a selector, or a uid.
	SessionID string
	ThreadID  string
}

LegacyPane is one observed pre-v2 tmux pane. Label is the existing @projmux_pane_label value, which is the migration seed for the Pane *name*; in v2 vocabulary it is a name, not a label. metadata.labels stays reserved for key/value classification.

type LegacySession

type LegacySession struct {
	Session string
	Root    string
	Windows []LegacyWindow
}

LegacySession is one observed pre-v2 tmux session anchored at a project root.

type LegacyWindow

type LegacyWindow struct {
	Name string
	// RuntimeSessionID and RuntimeID are the exact live $N/@N owner binding.
	RuntimeSessionID string
	RuntimeID        string
	// AutomaticRename is the observed window-scoped automatic-rename value.
	AutomaticRename bool
	// UID is the `@projmux_window_uid` the live tmux window already carries,
	// empty when it carries none. Same role as LegacyPane.UID.
	UID   string
	Panes []LegacyPane
}

LegacyWindow is one observed pre-v2 tmux window.

type Migration

Migration lifts one registry document from an envelope version to the next.

type MigrationEnvironment added in v0.13.0

type MigrationEnvironment struct {
	DirectoryExists func(string) (bool, error)
	NewUID          func(Kind) (string, error)
}

MigrationEnvironment supplies the only machine-dependent inputs a schema repair may use. Keeping them injected leaves this package pure and gives the production adapter and byte-fixed golden tests the exact same algorithm.

type MigrationRepair added in v0.13.0

type MigrationRepair struct {
	Action          string `json:"action"`
	Kind            Kind   `json:"kind"`
	UID             string `json:"uid"`
	Field           string `json:"field"`
	From            string `json:"from,omitempty"`
	To              string `json:"to,omitempty"`
	InformationLoss bool   `json:"informationLoss"`
}

MigrationRepair is one stable, operator-reportable change made while an old envelope is lifted. InformationLoss distinguishes replacement of stale declared data from additive repair.

type MigrationReport added in v0.13.0

type MigrationReport struct {
	FromVersion int
	ToVersion   int
	Repairs     []MigrationRepair
}

MigrationReport records every repair made by a migration in Registry order.

func (MigrationReport) InformationLossCount added in v0.13.0

func (r MigrationReport) InformationLossCount() int

InformationLossCount returns the number of repairs that replaced declared legacy information rather than only adding a missing invariant field.

func (MigrationReport) String added in v0.13.0

func (r MigrationReport) String() string

String renders a deterministic report suitable for an operator-facing migration result or archived test evidence.

type MigrationSet

type MigrationSet map[int]Migration

MigrationSet maps a source envelope version onto the step that lifts it to the next version.

func ProductionMigrationSet added in v0.13.0

func ProductionMigrationSet() MigrationSet

ProductionMigrationSet returns a copy of the shipped migration registry. Integration tests extend this copy with a pre-v1 fixture step without replacing the production v1 repair they are meant to exercise.

type Mutator

type Mutator struct {
	// Now supplies operation timestamps. Defaults to time.Now.
	Now func() time.Time
	// NewUID mints opaque identities. Defaults to the crypto/rand generator.
	NewUID func(Kind) (string, error)
	// DirExists reports whether a path is an existing directory. It has no
	// default: root lifecycle operations require the caller to supply the
	// filesystem probe explicitly.
	DirExists func(string) (bool, error)
}

Mutator applies metadata operations to a registry. Every environment dependency is injected so the core stays free of I/O and tests stay deterministic.

func (Mutator) AddPane

func (m Mutator) AddPane(reg *Registry, windowUID string, declared BootstrapPane, defaultShell, operationID string) (Pane, error)

AddPane creates one offline shell Pane inside an existing Window.

func (Mutator) AddWindow

func (m Mutator) AddWindow(reg *Registry, projectUID string, declared BootstrapWindow, defaultShell, operationID string) (Window, []Pane, error)

AddWindow creates one offline Window plus its initial Pane below an existing Project. No tmux window is created.

func (Mutator) AddWindowToManagedRoot added in v0.13.0

func (m Mutator) AddWindowToManagedRoot(reg *Registry, ownerKind Kind, ownerUID string, declared BootstrapWindow, defaultShell, defaultCWD, operationID string) (Window, []Pane, error)

AddWindowToManagedRoot creates one offline Window plus its initial Pane below an exact Project or ControlSession owner. Generated Window intents use this owner-explicit entrypoint; the public AddWindow contract remains Project-only.

func (Mutator) AdoptWindowDefaultShell added in v0.13.0

func (m Mutator) AdoptWindowDefaultShell(reg *Registry, windowUID, paneUID string) (Pane, bool, error)

AdoptWindowDefaultShell installs paneUID as a Window's optional default shell only when the ref is empty. The candidate must be a direct Window-owned shell Pane; the role-agnostic anchor is never inferred or changed.

func (Mutator) AttachAgentPane

func (m Mutator) AttachAgentPane(reg *Registry, agentUID string, declared BootstrapPane, operationID string) (Pane, error)

AttachAgentPane creates the managed Pane owned by an Agent and moves the Agent to Running. The Pane name uses the "<agent-name>-pane" base inside the Agent's owner scope.

func (Mutator) Begin

func (m Mutator) Begin(reg *Registry, operationID string) *Transaction

Begin opens a transaction against reg with the supplied operation id.

func (Mutator) BindCodexActivation added in v0.13.0

func (m Mutator) BindCodexActivation(reg *Registry, obs CodexActivationObservation) (bool, error)

BindCodexActivation commits the first native binding for one exact running Agent materialization. It also records the returned thread as the Agent's durable Codex sessionRef. A create may fill an empty ref; a resume may only reuse the already stored same thread.

func (Mutator) BindControlSession added in v0.13.0

func (m Mutator) BindControlSession(reg *Registry, observed ControlSessionObservation, defaultShell, operationID string, binder *BindingMatcher) (ControlSessionBinding, error)

BindControlSession converts one observed app-owned control session into Registry resources and reattaches the ones that already exist.

It is convergent by construction, which is what makes it safe to run on every `projmux shell` entry:

  • The exact session name reuses the same ControlSession uid. Nothing else merges uids, and no observation ever renames or re-roots an existing one.
  • Windows and Panes resolve through the shared adoption matcher, so a live object that still carries a known uid is rebound, an unmirrored live object adopts the next unbound Registry object of the same owner in creation order, and only a genuinely surplus live object mints a new resource. A mirror write that failed halfway through a previous pass therefore repairs itself instead of duplicating a Window on the next pass.
  • Nothing is deleted, pruned, renamed, or re-identified. A Window whose tmux window is gone keeps its uid and its name reservation exactly as a MissingRoot Project does.

binder carries the adoption decision across a whole reconciliation pass so one Registry Window is never handed to two live tmux windows. A nil binder gets a private one over an empty observation, which is the correct reading for a caller binding a single session in isolation.

func (Mutator) BindProjectSession

func (m Mutator) BindProjectSession(reg *Registry, projectUID, sessionName string, live bool) (Project, error)

BindProjectSession records the 1:1 persistent tmux session projection. It never changes metadata.uid, so a Project keeps its identity across runtime creation, teardown, and recreation.

func (Mutator) ClearTermination added in v0.13.0

func (m Mutator) ClearTermination(reg *Registry, paneUID, operationID string) (bool, error)

ClearTermination removes a receipt this exact operation wrote.

It is the compensating half of a control action that recorded its intent and then failed before carrying it out. The operation id guard is what makes the compensation safe: a receipt written by anything else -- another delete, a supervisor observing a real exit in the meantime -- is left alone, so a failed delete can never erase evidence it did not produce.

func (Mutator) CreateAgent

func (m Mutator) CreateAgent(reg *Registry, windowUID string, opts CreateAgentOptions) (Agent, error)

CreateAgent creates an Agent owned by windowUID in the Pending phase. The managed Pane is attached separately.

func (Mutator) DeleteAgent

func (m Mutator) DeleteAgent(reg *Registry, agentUID string) error

DeleteAgent removes an Agent and its managed Panes.

func (Mutator) DeletePane

func (m Mutator) DeletePane(reg *Registry, paneUID string) error

DeletePane removes one Pane resource. Deleting the managed Pane of a running Agent moves that Agent to Offline; the Agent resource survives.

func (Mutator) DeleteProject

func (m Mutator) DeleteProject(reg *Registry, projectUID string) error

DeleteProject removes a Project and every descendant resource.

func (Mutator) DeleteWindow

func (m Mutator) DeleteWindow(reg *Registry, windowUID string) error

DeleteWindow is the explicit canonical Window delete. It removes a Window and every descendant Pane and Agent while preserving its Project or ControlSession root. A Project with no remaining Window keeps its uid, root, session name, pin, and snapshot ownership as a valid closed identity.

func (Mutator) EnsureWindowDefaultShell added in v0.13.0

func (m Mutator) EnsureWindowDefaultShell(reg *Registry, windowUID, defaultShell, operationID string) (Pane, bool, error)

EnsureWindowDefaultShell returns the Window's existing optional default shell or allocates one direct Window-owned shell without changing a valid anchor. The allocation is intended for a shell-required transaction that already owns its Registry working copy; callers remain responsible for runtime creation and outer rollback.

func (Mutator) ImportLegacySession

func (m Mutator) ImportLegacySession(reg *Registry, legacy LegacySession, defaultShell, operationID string, binder *BindingMatcher) (ImportResult, error)

ImportLegacySession converts one observed pre-v2 tmux session into Projmux resources, and reattaches the ones that already exist.

Name collisions during import are automatic, not explicit, so they receive the lowest free suffix rather than failing: two projects whose roots share a basename become `name` and `name-1`, and two agents of the same provider in one window become `codex` and `codex-1`. An exact saved root that reappears reuses the same Project uid; nothing else merges uids.

binder carries the adoption decision across the whole reconciliation pass so one registry Window is never handed to two live tmux windows. A nil binder gets a private one over an empty observation, which is the right reading for a caller importing a single session in isolation: adoption still happens, and nothing is known to be bound elsewhere.

func (Mutator) ImportOrphanPane

func (m Mutator) ImportOrphanPane(reg *Registry, windowUID string, observed LegacyPane, operationID string) (Pane, error)

ImportOrphanPane mints the shell Pane a live tmux pane has never had, inside a Window that is already paired with a live tmux window.

Adoption alone cannot reach this state. Adoption pairs a live tmux object with a registry object that already exists, and a pane produced by a route that registers nothing -- the non-resource `projmux create agent` bridge is the measured one -- has no registry counterpart to pair with. Those panes stayed permanently unbound, so `projmux delete pane` with no selector kept refusing with "carries no @projmux_pane_uid" in the operator's own active pane. Something has to be created before a uid exists to mirror back.

The name base is FallbackPaneNameBase, uniquified by the registry's own allocator, and deliberately *not* LegacyPaneNameSeed. That seed reads `pane_current_command`, which changes the moment the operator runs something else in the pane, and the product contract is that metadata.name is never derived from a runtime attribute. What the runtime reported goes to status.displayTitle instead -- the duplicate-allowed field that exists for exactly this, and the same field the import path fills.

No Agent is minted, whatever the pane happens to be running. Reading the pane options or its title to decide that a Window owes an Agent resource would be a content heuristic deciding registry topology, which is the judgment bindLegacyPaneTx already records for an adopted pane. Agent phase belongs to its own track.

Nothing existing is touched: no uid is changed, merged, or reassigned, and no Window spec is rewritten. This adds one Pane and stops.

func (Mutator) LinkAgentPane

func (m Mutator) LinkAgentPane(reg *Registry, windowUID, paneUID string, observed LegacyPane, binder *BindingMatcher, operationID string) (AgentLinkage, error)

LinkAgentPane connects one already-bound registry Pane to the Agent resource whose runtime the live tmux pane is.

It is idempotent by construction. The second pass over the same machine finds the Pane already Agent-owned and does nothing but reassert the reverse pointer, so a reconciler that runs on every mutation route converges instead of accumulating Agents.

windowUID is the Window the Pane was bound inside. It is the whole scope: the candidate Agents are that Window's Agents and no others, so an Agent can never be pulled across a Project boundary. The caller reached this Window by pairing it against a live tmux window inside one resolved Project, so the boundary is structural here rather than checked.

func (Mutator) ObservePaneActivationRuntime added in v0.13.0

func (m Mutator) ObservePaneActivationRuntime(reg *Registry, paneUID, generation, runtimeID string) (bool, error)

ObservePaneActivationRuntime records the exact tmux handle one activation generation was materialized onto.

It is guarded by the generation rather than by the Pane uid alone: a handle observed for a generation the registry has already replaced describes a process that no longer holds the Pane, and writing it would make the diagnostic point at the wrong runtime object. A mismatch is a no-op.

func (Mutator) ObserveProjectRoots

func (m Mutator) ObserveProjectRoots(reg *Registry) error

ObserveProjectRoots refreshes the MissingRoot condition for every Project. A missing root never deletes or re-identifies a Project and never releases its name reservations; a returning root recovers the same uid and clears the condition.

func (Mutator) ObserveRuntimeBindings

func (m Mutator) ObserveRuntimeBindings(reg *Registry, observed RuntimeObservation)

ObserveRuntimeBindings refreshes the MissingRuntime condition of every Window and Pane against one live-tmux observation.

This is the reconciler's half of the observation contract. It records *why* a runtime object is gone so the reason survives the invocation that noticed it; the live/offline answer itself is derived per read from the observation and never from what this writes. That split is deliberate: a stored liveness bool is what made Windows and Panes report live against nothing at all.

It is an inventory diff, not an event handler, which is what lets it converge with no hook firing at all: an object whose uid is mirrored on no live tmux object is judged orphan on the very next pass, and a re-bound object clears the condition on the pass after that.

Nothing here deletes, prunes, or re-identifies a resource, and nothing releases a name reservation. A Window or Pane whose tmux object disappeared stays queryable forever, exactly like a MissingRoot Project.

func (Mutator) ObserveWindowDisplayName

func (m Mutator) ObserveWindowDisplayName(reg *Registry, windowUID, displayName string) (Window, error)

ObserveWindowDisplayName projects one live tmux window_name onto the duplicate-allowed, non-identifying metadata.displayName field. It never changes the Window uid, stable name, owner, or name reservation.

func (Mutator) ObserveWindowRuntimeBinding added in v0.13.0

func (m Mutator) ObserveWindowRuntimeBinding(reg *Registry, windowUID, sessionID, runtimeID string) (Window, error)

ObserveWindowRuntimeBinding records the exact live tmux owner pair for one managed Window. The binding is retained when a later inventory reports the Window missing: teardown hooks need the last positive $N/@N observation to pair pane-exited with window-unlinked without consulting current client context after the Window is gone.

func (Mutator) ProjectTermination added in v0.13.0

func (m Mutator) ProjectTermination(reg *Registry, in TerminationProjectionInput) (TerminationProjection, error)

ProjectTermination is the one lifecycle transition of the exit reconciler.

It consumes the receipt the Pane already stores, or records an unknown one when it stores none, and applies the single transition that evidence implies:

  • a shell Pane keeps its logical existence and gains the evidence. A runtime object going away is not a statement about desired topology, and deleting the resource here would make a crashed shell indistinguishable from one the operator asked to remove.
  • an Agent's *current* managed Pane is unbound by status only: the Agent moves to the phase the evidence implies, drops status.paneRef, and keeps both status.sessionRef and the Pane row. Runtime observation is evidence of disappearance, not canonical delete authority.
  • an Agent-owned Pane the Agent no longer binds is evidence only. The Agent has since been relaunched onto another Pane, and touching it here would apply a dead process's exit to a live one.

It is idempotent by construction. The unknown receipt it writes is the thing that makes it so: a second pass over the same disappearance finds that document already stored, RecordTermination reports it a duplicate, and nothing is written. An absence with no stored value would be re-projected forever.

It performs no runtime call of any kind and can start nothing.

func (Mutator) RebindAgentPane added in v0.13.0

func (m Mutator) RebindAgentPane(reg *Registry, agentUID, paneUID string) (Pane, error)

RebindAgentPane makes a retained Agent-owned Pane the Agent's current managed Pane again without changing either UID. Runtime disappearance retains Pane rows as evidence; materialization may therefore reuse the exact row instead of deleting the Window anchor and allocating a replacement identity.

func (Mutator) RebindProjectRoot

func (m Mutator) RebindProjectRoot(reg *Registry, projectUID, newRoot string) (Project, error)

RebindProjectRoot changes only spec.root of exactly one Project. It never moves files and never changes metadata.uid.

func (Mutator) RecordAgentSessionRef

func (m Mutator) RecordAgentSessionRef(reg *Registry, agentUID string, obs AgentSessionObservation) (Agent, bool, error)

RecordAgentSessionRef stores one hook-observed provider session ref on an Agent and reports whether the registry actually changed.

The write is deliberately narrow. It touches status.sessionRef and nothing else: not the phase, not lastTransitionAt, not paneRef. Observing which conversation an Agent belongs to is not a lifecycle event, and letting an ingest hook move an Agent through the phase machine would make the closed transition table answerable by an external tool.

A re-observation of the same conversation returns changed=false so a hook that fires on every turn does not rewrite the registry file each time.

A conversation already claimed by another Agent is NOT refused. Uniqueness is not an invariant this model can honestly hold: the same provider conversation really can be attached twice (a manual resume of the same session id in a second pane already does exactly that today), an Offline Agent keeps its ref forever, and refusing the write would make the registry describe a world that does not exist rather than the one that does. Choosing between several Agents that point at one conversation is a resume-time decision and belongs to the resume materialization Phase, not to this observation write.

func (Mutator) RecordPaneActivation added in v0.13.0

func (m Mutator) RecordPaneActivation(reg *Registry, paneUID string, opts PaneActivationOptions) (Pane, error)

RecordPaneActivation stamps a new activation generation onto one Pane.

Issuing a generation clears any receipt the previous materialization left behind. A Pane that has just been relaunched has no termination evidence, and keeping the old receipt visible would present a dead process's exit status as the current one's.

func (Mutator) RecordTermination added in v0.13.0

func (m Mutator) RecordTermination(reg *Registry, receipt TerminationEvidence) (TerminationOutcome, error)

RecordTermination applies one receipt under the activation generation guard.

The guards, in order, are the whole contract of this transport:

  • the named Pane must still exist;
  • the receipt's generation must be the Pane's *current* generation;
  • a receipt naming an Agent must name the Agent that owns the Pane, and that Agent's current pane binding must still be this Pane, except that a same-generation supervisor receipt may refine the reconcile/unknown evidence that already cleared this exact binding;
  • a receipt the registry already stores verbatim changes nothing;
  • recorded intent is sticky for its generation.

The last one is not an optimization. A canonical delete records intent and then kills the live Pane; the supervisor watching that Pane sees its child die on a signal and reports abnormal. Letting the observation overwrite the intent would turn every deliberate deletion into a crash report.

Nothing here changes an Agent phase, a paneRef, or a Pane's existence. Consuming this evidence is a separate concern with its own review.

func (Mutator) RefineCodexActivation added in v0.13.0

func (m Mutator) RefineCodexActivation(reg *Registry, obs CodexActivationObservation) (bool, error)

RefineCodexActivation updates only the turn of an already exact native binding. Late generations, foreign Panes/Agents, and another thread are normal rejected observations: they return changed=false and write nothing.

func (Mutator) RegisterProject

func (m Mutator) RegisterProject(reg *Registry, opts RegisterProjectOptions) (RegisterProjectResult, error)

RegisterProject registers a Project and creates its offline Window/Pane topology with no tmux involvement. The exact saved root reappearing reuses the same uid; nothing else ever merges uids.

func (Mutator) ReleaseAgentPane

func (m Mutator) ReleaseAgentPane(reg *Registry, agentUID string, exit AgentExit, reason string) (Agent, error)

ReleaseAgentPane removes the managed Pane and moves the Agent to the phase implied by exit. The Agent itself survives as an offline/resumable resource.

func (Mutator) RenameAgent

func (m Mutator) RenameAgent(reg *Registry, agentUID, name string) (Agent, error)

RenameAgent sets only an Agent's stable metadata.name inside its owning Window scope. Provider, topic annotations, lifecycle state, and managed Pane metadata are independent and remain unchanged.

func (Mutator) RenamePane

func (m Mutator) RenamePane(reg *Registry, paneUID, name string) (Pane, error)

RenamePane sets a Pane metadata.name inside its owner scope. The adapter mirrors the new name into the legacy @projmux_pane_label option and never writes the raw tmux pane_title.

func (Mutator) RenameProject

func (m Mutator) RenameProject(reg *Registry, projectUID, name string) (Project, error)

RenameProject sets a Project metadata.name. An explicit collision fails with ErrNameConflict and zero mutations.

func (Mutator) RenameWindow

func (m Mutator) RenameWindow(reg *Registry, windowUID, name string) (Window, error)

RenameWindow sets a Window metadata.name inside its owning Project scope.

func (Mutator) SetAgentActivation added in v0.11.1

func (m Mutator) SetAgentActivation(reg *Registry, agentUID string, state AgentActivationState, source, reason string) (Agent, error)

SetAgentActivation records bounded launch acknowledgement metadata.

func (Mutator) SetAgentInteraction added in v0.11.1

func (m Mutator) SetAgentInteraction(reg *Registry, agentUID string, kind AgentInteractionKind, source string) (Agent, error)

SetAgentInteraction stores one semantic observation on exactly one Agent. Transport vocabulary is normalized by callers before reaching this mutator.

func (Mutator) SetAgentProgress added in v0.13.0

func (m Mutator) SetAgentProgress(reg *Registry, agentUID, currentTurn string, progress AgentProgress) (Agent, bool, error)

SetAgentProgress commits one already-bounded exact-turn projection. The caller supplies the current turn from the same activation binding checked in its transaction; a mismatch is refused without turning progress into a second conversation authority.

func (Mutator) SetAgentTopic added in v0.11.1

func (m Mutator) SetAgentTopic(reg *Registry, agentUID, topic string) (Agent, error)

SetAgentTopic mutates only the non-identifying topic annotation.

func (Mutator) SetPaneDisplayTitle

func (m Mutator) SetPaneDisplayTitle(reg *Registry, paneUID, agent, topic, command, rawTitle string) (Pane, error)

SetPaneDisplayTitle stores the derived secondary pane title.

func (Mutator) TransitionAgent

func (m Mutator) TransitionAgent(reg *Registry, agentUID string, phase AgentPhase, reason string) (Agent, error)

TransitionAgent applies an explicit phase transition.

type NameReservation

type NameReservation struct {
	Scope string `json:"scope,omitempty"`
	Kind  Kind   `json:"kind"`
	Name  string `json:"name"`
	UID   string `json:"uid"`
}

NameReservation is the persisted record of one allocated name. Reservations are the authority for suffix allocation: the allocator never recomputes a suffix from resource scan order.

Scope is "" for the registry-wide Project scope and the owner uid for Window, Pane, and Agent names.

type ObjectMeta

type ObjectMeta struct {
	UID         string            `json:"uid"`
	Name        string            `json:"name"`
	DisplayName string            `json:"displayName,omitempty"`
	Labels      map[string]string `json:"labels,omitempty"`
	Annotations map[string]string `json:"annotations,omitempty"`
	OwnerRef    *OwnerRef         `json:"ownerRef,omitempty"`
	CreatedAt   time.Time         `json:"createdAt"`
}

ObjectMeta is the metadata block shared by every resource.

  • UID is opaque, immutable, and independent of tmux lifecycle.
  • Name is the stable unique-within-scope query key. Project names are unique across the registry; Window/Pane/Agent names are unique within their ownerRef scope.
  • DisplayName may duplicate and is never a selector, ownerRef, or identity.
  • Labels are key/value classification input.
  • Annotations are non-identifying metadata (AI topic, provider context).

func (ObjectMeta) Clone

func (m ObjectMeta) Clone() ObjectMeta

Clone returns a deep copy of the metadata block.

func (ObjectMeta) OwnerUID

func (m ObjectMeta) OwnerUID() string

OwnerUID returns the owning uid, or "" for a root resource.

type OwnerRef

type OwnerRef struct {
	Kind Kind   `json:"kind"`
	UID  string `json:"uid"`
}

OwnerRef points at the owning resource by opaque uid. It never carries a displayName, a tmux target, or any other non-identifying value.

type Pane

type Pane struct {
	APIVersion string     `json:"apiVersion"`
	Kind       Kind       `json:"kind"`
	Metadata   ObjectMeta `json:"metadata"`
	Spec       PaneSpec   `json:"spec"`
	Status     PaneStatus `json:"status"`
}

Pane is owned by a Window (shell role) or by an Agent (agent role).

func (Pane) Clone

func (p Pane) Clone() Pane

Clone returns a deep copy of the Pane.

func (Pane) HasCondition

func (p Pane) HasCondition(conditionType string) (Condition, bool)

HasCondition reports whether a Pane carries conditionType.

type PaneActivation added in v0.13.0

type PaneActivation struct {
	Generation  string    `json:"generation"`
	RuntimeID   string    `json:"runtimeID,omitempty"`
	AgentUID    string    `json:"agentUID,omitempty"`
	OperationID string    `json:"operationID,omitempty"`
	StartedAt   time.Time `json:"startedAt,omitzero"`
	// Codex is the provider-native conversation/turn observed for exactly this
	// materialization. The durable conversation pointer remains on the Agent;
	// this refinement is nested under the generation so replacement clears it.
	Codex *CodexActivationBinding `json:"codex,omitempty"`
}

PaneActivation binds one Pane materialization to an opaque generation.

RuntimeID is recorded for operator diagnostics only. tmux recycles `%N` handles, so the generation -- not the handle -- is what a receipt is matched against.

func (PaneActivation) Clone added in v0.13.0

func (a PaneActivation) Clone() PaneActivation

Clone returns an activation record whose provider binding cannot alias the source Registry snapshot.

func (PaneActivation) IsZero added in v0.13.0

func (a PaneActivation) IsZero() bool

IsZero lets registry documents written before activation generations existed re-encode without the additive block.

type PaneActivationOptions added in v0.13.0

type PaneActivationOptions struct {
	// Generation is the opaque value the launched process will quote back.
	Generation string
	// RuntimeID is the exact tmux pane handle at issue time, for diagnostics.
	RuntimeID string
	// AgentUID is set for an Agent-managed Pane and empty for a shell Pane.
	AgentUID string
	// OperationID labels the create/resume transaction that issued it.
	OperationID string
}

PaneActivationOptions is the input to one generation issue.

type PaneAgentCascadeDeletePlan added in v0.13.0

type PaneAgentCascadeDeletePlan struct {
	Decision      TeardownDecision
	Desired       Registry
	Changed       bool
	PaneUID       string
	AgentUID      string
	DeletedPanes  int
	DeletedAgents int
	Evidence      *TerminationEvidence
}

PaneAgentCascadeDeletePlan is the desired-Registry plan for one qualifying exact pane-exited event. The Pane row is released while its owning Agent and Window identities survive. When it was the last descendant, the plan adds the minimum replacement shell in the same desired graph.

func PlanPaneAgentCascadeDelete added in v0.13.0

func PlanPaneAgentCascadeDelete(registry Registry, event TeardownEvent, now time.Time) (PaneAgentCascadeDeletePlan, error)

PlanPaneAgentCascadeDelete converts one exact lifecycle decision into a schema-valid desired Registry without mutating the source document.

The complete owner chain is revalidated here even when the caller already derived it from a fresh observation. This is the locked generation/owner guard: a late receipt or a resumed Agent cannot delete the binding that replaced the event's materialization.

type PaneRole

type PaneRole string

PaneRole distinguishes a plain shell Pane from a Pane managed by an Agent.

const (
	PaneRoleShell PaneRole = "shell"
	PaneRoleAgent PaneRole = "agent"
)

type PaneSpec

type PaneSpec struct {
	Role    PaneRole `json:"role"`
	CWD     string   `json:"cwd,omitempty"`
	Command string   `json:"command,omitempty"`
}

PaneSpec records the declared pane recipe inputs. Command is the one-time name-derivation source; it is never re-read to rename an existing Pane.

type PaneStatus

type PaneStatus struct {
	DisplayTitle string      `json:"displayTitle,omitempty"`
	Conditions   []Condition `json:"conditions,omitempty"`
	// Activation names the current materialization of this Pane. It is the
	// value a launched supervisor quotes back, and the only thing that
	// separates the running process from the one a resume replaced.
	Activation PaneActivation `json:"activation,omitzero"`
	// LastTermination is the durable receipt of why the *current* generation's
	// managed process stopped. Issuing a new generation clears it.
	LastTermination *TerminationEvidence `json:"lastTermination,omitempty"`
	// Teardown records the bounded exact-socket topology evidence supplied by a
	// qualifying pane-exited hook. It is not inferred from liveness or content.
	// A matching window-unlinked hook consumes it by deleting this Pane's owner
	// chain; issuing a new activation clears it as stale generation state.
	Teardown *PaneTeardownEvidence `json:"teardown,omitempty"`
}

PaneStatus carries the derived secondary display title and the observed conditions of one Pane. DisplayTitle is never a selector, an identity, or a Window name source.

As with WindowStatus there is no stored liveness field: liveness is derived from a live observation at read time, and Conditions only preserves the reason a runtime object went away.

type PaneTeardownEvidence added in v0.13.0

type PaneTeardownEvidence struct {
	SocketIdentity   string                    `json:"socketIdentity"`
	RuntimeSessionID string                    `json:"runtimeSessionID"`
	RuntimePaneID    string                    `json:"runtimePaneID"`
	RuntimeWindowID  string                    `json:"runtimeWindowID"`
	WindowUID        string                    `json:"windowUID"`
	RootKind         Kind                      `json:"rootKind"`
	RootUID          string                    `json:"rootUID"`
	Generation       string                    `json:"generation"`
	Classification   TerminationClassification `json:"classification"`
	ObservedAt       time.Time                 `json:"observedAt"`
}

PaneTeardownEvidence pairs one exact pane-exited event with a later exact window-unlinked event without parsing Pane content or treating absence as authority. Every identity is current-generation and socket scoped.

func (*PaneTeardownEvidence) Clone added in v0.13.0

Clone returns an independent teardown record.

type PaneTeardownEvidencePlan added in v0.13.0

type PaneTeardownEvidencePlan struct {
	Decision TeardownDecision
	Desired  Registry
	Changed  bool
	Evidence PaneTeardownEvidence
}

PaneTeardownEvidencePlan persists the exact causal half of a last-Pane cascade while retaining the complete Registry graph. It performs no parent deletion until the matching window-unlinked event arrives.

func PlanPaneTeardownEvidence added in v0.13.0

func PlanPaneTeardownEvidence(registry Registry, event TeardownEvent, now time.Time) (PaneTeardownEvidencePlan, error)

PlanPaneTeardownEvidence records the exact clean last-Pane half while retaining the complete Window subtree. The matching window-unlinked event is the only consumer allowed to turn this receipt into Window deletion.

type Project

type Project struct {
	APIVersion string        `json:"apiVersion"`
	Kind       Kind          `json:"kind"`
	Metadata   ObjectMeta    `json:"metadata"`
	Spec       ProjectSpec   `json:"spec"`
	Status     ProjectStatus `json:"status"`
}

Project is the canonical Projmux root resource.

func (Project) Clone

func (p Project) Clone() Project

Clone returns a deep copy of the Project.

func (Project) HasCondition

func (p Project) HasCondition(conditionType string) (Condition, bool)

HasCondition reports whether a Project carries conditionType.

type ProjectCascadeDeletePlan added in v0.13.0

type ProjectCascadeDeletePlan struct {
	ProjectUID          string
	Root                string
	Desired             Registry
	Changed             bool
	DeletedProjects     int
	DeletedWindows      int
	DeletedPanes        int
	DeletedAgents       int
	DeletedReservations int
	ExternalAssets      ExternalAssetOutcome
	ReopenIdentity      ReopenIdentity
}

ProjectCascadeDeletePlan is a pure desired-Registry plan. The source Registry is never mutated; applying the composite write is a later phase.

func PlanProjectCascadeDelete added in v0.13.0

func PlanProjectCascadeDelete(registry Registry, projectUID string, now time.Time) (ProjectCascadeDeletePlan, error)

PlanProjectCascadeDelete removes one Project graph on a clone and returns the schema-valid desired Registry without touching filesystem or snapshot data.

type ProjectDescendantUIDOutcome added in v0.13.0

type ProjectDescendantUIDOutcome string
const (
	ProjectDescendantUIDsPreserved ProjectDescendantUIDOutcome = "preserved"
	ProjectDescendantUIDsCreated   ProjectDescendantUIDOutcome = "created"
	ProjectDescendantUIDsReplaced  ProjectDescendantUIDOutcome = "replaced"
	ProjectDescendantUIDsRemoved   ProjectDescendantUIDOutcome = "removed"
	ProjectDescendantUIDsAbsent    ProjectDescendantUIDOutcome = "absent"
)

type ProjectFreshReplacementPlan added in v0.13.0

type ProjectFreshReplacementPlan struct {
	Operation     ProjectLifecycleOperation
	OldProjectUID string
	NewProjectUID string
	Preimage      Registry
	Desired       Registry
	WriteSet      []ProjectStartupWrite
}

ProjectFreshReplacementPlan is the complete Registry preimage and desired result for one Fresh commit. The caller commits Desired atomically; any plan or store failure retains Preimage byte-for-byte.

func PlanProjectFreshReplacement added in v0.13.0

func PlanProjectFreshReplacement(registry Registry, projectUID string, opts RegisterProjectOptions, mutator Mutator) (ProjectFreshReplacementPlan, error)

PlanProjectFreshReplacement removes one exact Project graph from a clone and registers a new canonical Project/Window/shell graph for the same root. It never mutates registry, filesystem, Git/worktree state, or snapshots.

type ProjectLifecycleAction added in v0.13.0

type ProjectLifecycleAction string

ProjectLifecycleAction is the complete user-intent vocabulary at this boundary. Keeping one action on one plan makes stop, ordinary Window close, Project unregister, and Fresh replacement impossible to conflate.

const (
	ProjectLifecycleStop          ProjectLifecycleAction = "stop"
	ProjectLifecycleContinue      ProjectLifecycleAction = "continue"
	ProjectLifecycleFresh         ProjectLifecycleAction = "fresh"
	ProjectLifecycleDeleteProject ProjectLifecycleAction = "delete-project"
)

type ProjectLifecycleOperation added in v0.13.0

type ProjectLifecycleOperation string

ProjectLifecycleOperation is the single mutation class carried by an executable plan. close-window is produced only by the causal Window plan; the four Project actions can therefore be compared without sharing effects.

const (
	ProjectLifecycleOperationNone          ProjectLifecycleOperation = "none"
	ProjectLifecycleOperationStop          ProjectLifecycleOperation = "stop"
	ProjectLifecycleOperationContinue      ProjectLifecycleOperation = "continue"
	ProjectLifecycleOperationFresh         ProjectLifecycleOperation = "fresh"
	ProjectLifecycleOperationDeleteProject ProjectLifecycleOperation = "delete-project"
	ProjectLifecycleOperationCloseWindow   ProjectLifecycleOperation = "close-window"
)

type ProjectLifecyclePlan added in v0.13.0

type ProjectLifecyclePlan struct {
	State          ProjectLifecycleState
	Action         ProjectLifecycleAction
	Operation      ProjectLifecycleOperation
	Available      bool
	ProjectUID     ProjectUIDOutcome
	DescendantUIDs ProjectDescendantUIDOutcome
	AtomicWriteSet []ProjectStartupWrite
	ExternalAssets ExternalAssetOutcome
	Reason         string
}

ProjectLifecyclePlan is one cell of the retained/zero/deleted × Stop/Continue/Fresh/delete table. AtomicWriteSet names Registry/runtime effects; a no-op is explicit rather than represented by a blank cell.

func DecideProjectLifecycle added in v0.13.0

func DecideProjectLifecycle(state ProjectLifecycleState, action ProjectLifecycleAction, preconditions ProjectLifecyclePreconditions) ProjectLifecyclePlan

DecideProjectLifecycle returns the single lifecycle/startup state table. Ordinary clean Window close is intentionally not an action in this table: it is owned by the causal Window-close plan and never appears in these write sets.

type ProjectLifecyclePreconditions added in v0.13.0

type ProjectLifecyclePreconditions struct {
	UsableSnapshot bool
}

ProjectLifecyclePreconditions carries external evidence required by one cell without widening the closed three-state table. A usable snapshot is relevant only to deleted+Continue; runtime absence is deliberately not a precondition because it never grants Project identity authority.

type ProjectLifecycleState added in v0.13.0

type ProjectLifecycleState string

ProjectLifecycleState is the desired Project shape at the lifecycle/startup boundary. Runtime liveness is deliberately absent: a missing tmux server is observation, never Project-delete authority.

const (
	ProjectLifecycleRetainedWindows ProjectLifecycleState = "retained-window"
	ProjectLifecycleZeroWindows     ProjectLifecycleState = "zero-window"
	ProjectLifecycleDeleted         ProjectLifecycleState = "deleted"
)

type ProjectOpenAction added in v0.13.0

type ProjectOpenAction string

ProjectOpenAction is the user's one-step startup choice.

const (
	ProjectOpenContinue ProjectOpenAction = "continue"
	ProjectOpenFresh    ProjectOpenAction = "open-fresh"
)

type ProjectOpenPlan added in v0.13.0

type ProjectOpenPlan struct {
	Available         bool
	Source            ProjectOpenSource
	AtomicWriteSet    []ProjectStartupWrite
	Reason            ProjectOpenReason
	NewProjectUID     bool
	AdditionalConfirm bool
	ExternalAssets    ExternalAssetOutcome
}

ProjectOpenPlan is one total startup state-table cell.

func DecideProjectOpen added in v0.13.0

func DecideProjectOpen(state ProjectReopenState, action ProjectOpenAction) ProjectOpenPlan

DecideProjectOpen is the compatibility projection used by the older live/closed/deleted-with-snapshot UI classifier. It adds runtime/source labels only; Fresh and Continue identity outcomes and write sets always come from DecideProjectLifecycle. Unavailable Continue never silently falls back to Fresh.

type ProjectOpenReason added in v0.13.0

type ProjectOpenReason string

ProjectOpenReason makes unavailable and invalid cells non-ambiguous.

const (
	ProjectOpenReasonAttachLive        ProjectOpenReason = "attach-live-project"
	ProjectOpenReasonMaterializeClosed ProjectOpenReason = "materialize-closed-project"
	ProjectOpenReasonRestoreSnapshot   ProjectOpenReason = "restore-usable-snapshot"
	ProjectOpenReasonNoSnapshot        ProjectOpenReason = "no-usable-snapshot"
	ProjectOpenReasonFreshReplace      ProjectOpenReason = "replace-existing-project"
	ProjectOpenReasonFreshCreate       ProjectOpenReason = "create-fresh-project"
	ProjectOpenReasonInvalid           ProjectOpenReason = "invalid-state-or-action"
)

type ProjectOpenSource added in v0.13.0

type ProjectOpenSource string

ProjectOpenSource is the authority from which topology is opened.

const (
	ProjectOpenSourceLiveRuntime      ProjectOpenSource = "live-runtime"
	ProjectOpenSourceRegistryTopology ProjectOpenSource = "registry-topology"
	ProjectOpenSourceSnapshot         ProjectOpenSource = "snapshot"
	ProjectOpenSourceRoot             ProjectOpenSource = "filesystem-root"
	ProjectOpenSourceNone             ProjectOpenSource = "none"
)

type ProjectReopenState added in v0.13.0

type ProjectReopenState string

ProjectReopenState is the closed startup state table.

const (
	ProjectReopenLive                   ProjectReopenState = "live"
	ProjectReopenClosed                 ProjectReopenState = "closed"
	ProjectReopenDeletedWithSnapshot    ProjectReopenState = "deleted-with-snapshot"
	ProjectReopenDeletedWithoutSnapshot ProjectReopenState = "deleted-without-snapshot"
)

type ProjectSpec

type ProjectSpec struct {
	Root             string `json:"root"`
	PrimaryWindowRef string `json:"primaryWindowRef"`
}

ProjectSpec holds the absolute filesystem root the Project is bound to and the exact Window that owns its canonical shell Pane.

type ProjectStartupWrite added in v0.13.0

type ProjectStartupWrite string

ProjectStartupWrite is one member of an atomic startup write set.

const (
	ProjectStartupWriteNone                  ProjectStartupWrite = "no-write"
	ProjectStartupWriteStopRuntime           ProjectStartupWrite = "stop-managed-runtime"
	ProjectStartupWriteMaterializeRegistry   ProjectStartupWrite = "materialize-registry-topology"
	ProjectStartupWriteDeleteProjectGraph    ProjectStartupWrite = "delete-existing-project-graph"
	ProjectStartupWriteCreateProject         ProjectStartupWrite = "create-project-with-new-uid"
	ProjectStartupWriteCreateCanonicalWindow ProjectStartupWrite = "create-canonical-window"
	ProjectStartupWriteCreateCanonicalShell  ProjectStartupWrite = "create-canonical-shell"
	ProjectStartupWriteRestoreSnapshotGraph  ProjectStartupWrite = "restore-snapshot-topology"
)

type ProjectStatus

type ProjectStatus struct {
	Session    *SessionProjection `json:"session,omitempty"`
	Conditions []Condition        `json:"conditions,omitempty"`
}

ProjectStatus carries the runtime session projection and observed conditions.

type ProjectUIDOutcome added in v0.13.0

type ProjectUIDOutcome string
const (
	ProjectUIDPreserved ProjectUIDOutcome = "preserved"
	ProjectUIDReplaced  ProjectUIDOutcome = "replaced"
	ProjectUIDCreated   ProjectUIDOutcome = "created"
	ProjectUIDRemoved   ProjectUIDOutcome = "removed"
	ProjectUIDAbsent    ProjectUIDOutcome = "absent"
)

type RegisterProjectOptions

type RegisterProjectOptions struct {
	// Root is the absolute project root. It must already exist.
	Root string
	// Name is an explicit --name. A collision fails with ErrNameConflict and
	// zero mutations; it never receives an implicit suffix.
	Name string
	// DisplayName defaults to the root basename and may duplicate.
	DisplayName string
	Labels      map[string]string
	Annotations map[string]string
	// Topology is the configured project startup topology. Empty means one
	// default Window named after the configured shell basename with one
	// initial shell Pane.
	Topology []BootstrapWindow
	// DefaultShell is the configured shell path; its basename seeds default
	// Window and Pane names.
	DefaultShell string
	// SessionName optionally records the persistent tmux session projection.
	SessionName string
	// OperationID labels the transaction ledger.
	OperationID string
}

RegisterProjectOptions is the input to offline Project registration.

type RegisterProjectResult

type RegisterProjectResult struct {
	Project     Project
	Windows     []Window
	Panes       []Pane
	Reused      bool
	OperationID string
	Created     []string
}

RegisterProjectResult reports the resources an offline registration created.

type Registry

type Registry struct {
	APIVersion    string    `json:"apiVersion"`
	SchemaVersion int       `json:"schemaVersion"`
	UpdatedAt     time.Time `json:"updatedAt"`
	Projects      []Project `json:"projects,omitempty"`
	// ControlSessions is `omitempty`, which is the whole no-migration story for
	// this slice: a registry written before control sessions existed decodes the
	// absent key to a nil slice and re-encodes it as an absent key, so the
	// document round-trips byte-identically and schemaVersion stays 1. Bumping
	// the envelope would make every already installed build reject the file
	// fail-closed with ErrSchemaTooNew -- a hard downgrade break bought for
	// nothing.
	ControlSessions  []ControlSession  `json:"controlSessions,omitempty"`
	Windows          []Window          `json:"windows,omitempty"`
	Panes            []Pane            `json:"panes,omitempty"`
	Agents           []Agent           `json:"agents,omitempty"`
	NameReservations []NameReservation `json:"nameReservations,omitempty"`
}

Registry is the whole persisted resource set plus its name reservations. Slice order is insertion order and is preserved verbatim so serialization is deterministic without a sort pass.

func MigrateRegistry

func MigrateRegistry(reg Registry) (Registry, bool, error)

MigrateRegistry lifts reg to the current schema version using the production migration set.

func NewRegistry

func NewRegistry() Registry

NewRegistry returns an empty registry stamped with the current envelope.

func (*Registry) Agent

func (r *Registry) Agent(uid string) (*Agent, bool)

Agent returns the Agent with uid.

func (*Registry) AgentsOf

func (r *Registry) AgentsOf(windowUID string) []Agent

AgentsOf returns the Agents owned by windowUID in insertion order.

func (Registry) Clone

func (r Registry) Clone() Registry

Clone returns a deep copy of the registry so a failed mutation can never leak a partially applied change back to the caller.

func (*Registry) ControlSession added in v0.13.0

func (r *Registry) ControlSession(uid string) (*ControlSession, bool)

ControlSession returns the ControlSession with uid.

func (*Registry) ControlSessionBySession added in v0.13.0

func (r *Registry) ControlSessionBySession(session string) (*ControlSession, bool)

ControlSessionBySession returns the ControlSession bound to an exact tmux session name.

Matching is exact and never heuristic, for the same reason ProjectByRoot's is: the session name is the identity, so a trimmed, cased, or prefixed variant is a different session and must not merge onto this uid.

func (Registry) Normalize

func (r Registry) Normalize() Registry

Normalize returns the registry with the current envelope stamped and timestamps canonicalized to UTC.

func (*Registry) Pane

func (r *Registry) Pane(uid string) (*Pane, bool)

Pane returns the Pane with uid.

func (Registry) PaneInWindow added in v0.13.0

func (r Registry) PaneInWindow(windowUID, paneUID string) (*Pane, bool)

PaneInWindow reports whether paneUID belongs to windowUID through the exact Registry owner chain. It does not consult names, cwd, command, or runtime order, and it does not require an Agent Pane to be the Agent's current pane; callers that need the stricter anchor invariant use WindowAnchor.

func (*Registry) PanesOf

func (r *Registry) PanesOf(ownerUID string) []Pane

PanesOf returns the Panes owned by ownerUID in insertion order. The owner is a Window for shell panes and an Agent for managed panes.

func (*Registry) Project

func (r *Registry) Project(uid string) (*Project, bool)

Project returns the Project with uid.

func (*Registry) ProjectByName

func (r *Registry) ProjectByName(name string) (*Project, bool)

ProjectByName returns the Project with the registry-unique name.

func (*Registry) ProjectByRoot

func (r *Registry) ProjectByRoot(root string) (*Project, bool)

ProjectByRoot returns the Project bound to an exact cleaned root path. Root matching is exact and never heuristic: basename, git origin, inode, and scan order are deliberately not consulted.

func (Registry) Validate

func (r Registry) Validate() error

Validate checks every structural invariant of the registry: envelope spelling, uid uniqueness, in-scope name uniqueness, ownerRef integrity, anchor/default-shell validity, Agent phase membership, and reservation consistency.

func (*Registry) Window

func (r *Registry) Window(uid string) (*Window, bool)

Window returns the Window with uid.

func (Registry) WindowAnchor added in v0.13.0

func (r Registry) WindowAnchor(windowUID string) (*Pane, bool)

WindowAnchor resolves the required role-agnostic anchor of windowUID.

It is deliberately stricter than a bare Pane lookup: a direct shell must be owned by the Window, while an Agent Pane must be owned by an Agent of that Window and remain that Agent's managed Pane. Consumers use this helper before selecting a runtime target so a dangling or cross-Window ref can never turn into an inferred sibling Pane.

func (Registry) WindowDefaultShell added in v0.13.0

func (r Registry) WindowDefaultShell(windowUID string) (*Pane, bool)

WindowDefaultShell resolves the optional direct Window-owned shell. Empty is a valid absence and is therefore reported as (nil, false).

func (*Registry) WindowsOf

func (r *Registry) WindowsOf(projectUID string) []Window

WindowsOf returns the Windows owned by projectUID in insertion order.

type ReopenIdentity added in v0.13.0

type ReopenIdentity string

ReopenIdentity states which Registry identity a subsequent open may use.

const (
	ReopenIdentitySameProjectUID ReopenIdentity = "same-project-uid"
	ReopenIdentityNewProjectUID  ReopenIdentity = "new-project-uid"
	ReopenIdentityNotApplicable  ReopenIdentity = "not-applicable"
)

type RootTeardownAction added in v0.13.0

type RootTeardownAction string

RootTeardownAction states what happens at the owner-root boundary.

const (
	RootTeardownRetainProject        RootTeardownAction = "retain-project"
	RootTeardownDeleteProject        RootTeardownAction = "delete-project"
	RootTeardownRetainControlSession RootTeardownAction = "retain-control-session"
)

type RuntimeObservation

type RuntimeObservation struct {
	// Windows is the set of Window uids a live tmux window still mirrors.
	Windows map[string]bool
	// Panes is the set of Pane uids a live tmux pane still mirrors.
	Panes map[string]bool
}

RuntimeObservation is one live-tmux inventory of mirrored Projmux uids.

It is the machine half of a registry-versus-machine diff: a uid the registry holds but this observation does not is an object whose tmux window or pane is gone. It is a value taken once per process invocation and thrown away, never a cache and never persisted, which is what makes closing a pane visible to the very next query without any hook firing.

The empty observation means "nothing is bound", which is the fail-closed reading: it can only ever downgrade a resource to offline, never invent a live one.

func (RuntimeObservation) BoundPane

func (o RuntimeObservation) BoundPane(uid string) bool

BoundPane reports whether a live tmux pane still mirrors uid.

func (RuntimeObservation) BoundWindow

func (o RuntimeObservation) BoundWindow(uid string) bool

BoundWindow reports whether a live tmux window still mirrors uid.

There is deliberately no generic Bound(kind, uid) accessor. Only Window and Pane have a tmux object of their own: a Project's runtime is a tmux session with no @projmux uid, and an Agent has no tmux object at all. A kind-dispatch accessor would have to answer something for those two, and every available answer is a lie waiting to be trusted.

func (RuntimeObservation) Clone

Clone returns a deep copy so a resolver can never observe its snapshot changing under it.

type SchemaAction

type SchemaAction int

SchemaAction is the read decision for one on-disk registry envelope.

const (
	// SchemaCurrent means the envelope already matches SchemaVersion.
	SchemaCurrent SchemaAction = iota
	// SchemaMigrate means a known older envelope must be migrated forward
	// before it can be used.
	SchemaMigrate
	// SchemaReject means the envelope cannot be read. The caller must fail
	// closed and perform no write at all.
	SchemaReject
)

func ClassifySchemaVersion

func ClassifySchemaVersion(version int) (SchemaAction, error)

ClassifySchemaVersion decides how to read an envelope version using the production migration set.

func ClassifySchemaVersionWith

func ClassifySchemaVersionWith(set MigrationSet, version int) (SchemaAction, error)

ClassifySchemaVersionWith decides how to read an envelope version against an explicit migration set. A nil set means the production set.

Everything that is not the current version and not covered by a registered migration step is rejected fail-closed: the caller must not quarantine, reset, truncate, or otherwise write the file. A newer version would destroy state a newer build owns; an unversioned or otherwise unknown document is not proven to be a projmux registry at all. Downgrade writes are unsupported.

func (SchemaAction) String

func (a SchemaAction) String() string

String renders the action for diagnostics.

type SessionProjection

type SessionProjection struct {
	Name string `json:"name"`
	Live bool   `json:"live"`
}

SessionProjection is the 1:1 runtime projection of a persistent tmux session onto its Project. Auto-attach ephemeral sessions are never recorded here; they live only in runtime inventory, outside the Project hierarchy.

type SnapshotProjectionPlan added in v0.13.0

type SnapshotProjectionPlan struct {
	ProjectUID      string
	Desired         Registry
	Changed         bool
	ReplacedWindows int
	ReplacedPanes   int
	ReplacedAgents  int
	PreservedUIDs   int
	DeletedWindows  int
	DeletedPanes    int
	DeletedAgents   int
	LostSessionRefs int
}

SnapshotProjectionPlan is the pure, scoped replacement of one Project's descendants. Desired is safe to commit as one Registry transaction.

func PlanSnapshotProjection added in v0.13.0

func PlanSnapshotProjection(registry Registry, targetProjectUID string, snap sessionstate.Snapshot, now time.Time, newUID func(Kind) (string, error)) (SnapshotProjectionPlan, error)

PlanSnapshotProjection translates a v1 session snapshot into the desired Registry subtree of targetProjectUID. It performs no I/O and never mutates registry or snapshot.

type StateError

type StateError struct {
	Op     string
	Detail string
	Cause  error
}

StateError is a typed metadata error caused by inconsistent persisted state or an unavailable resource. It is not a usage error: it maps to exit 1.

func (*StateError) Error

func (e *StateError) Error() string

Error renders "<op>: <detail>".

func (*StateError) Unwrap

func (e *StateError) Unwrap() error

Unwrap exposes the sentinel cause to errors.Is.

type TeardownAction added in v0.13.0

type TeardownAction string

TeardownAction is the bounded Registry action an authority decision permits.

const (
	TeardownRetain          TeardownAction = "retain"
	TeardownDeletePaneAgent TeardownAction = "delete-pane-agent"
	TeardownDeleteWindow    TeardownAction = "delete-window"
	TeardownRefuse          TeardownAction = "refuse"
)

type TeardownDecision added in v0.13.0

type TeardownDecision struct {
	Action         TeardownAction
	RootAction     RootTeardownAction
	Reason         TeardownReason
	ExternalAssets ExternalAssetOutcome
	ReopenIdentity ReopenIdentity
}

TeardownDecision is one total decision-table cell.

func AggregateTeardownEvents added in v0.13.0

func AggregateTeardownEvents(events []TeardownEvent) TeardownDecision

AggregateTeardownEvents folds the two causal event kinds for one exact owner chain. It is intentionally insensitive to event delivery order.

func DecideTeardownEvent added in v0.13.0

func DecideTeardownEvent(event TeardownEvent) TeardownDecision

DecideTeardownEvent evaluates one event without mutating Registry or runtime state. A window-unlinked event is never sufficient by itself: aggregation must pair it with the exact causal pane-exited event.

type TeardownEvent added in v0.13.0

type TeardownEvent struct {
	Kind                  TeardownEventKind
	Classification        TerminationClassification
	Generation            TeardownGeneration
	Observation           TeardownObservation
	Chain                 TeardownOwnerChain
	LiveSiblingPane       bool
	LiveSiblingRootWindow bool
}

TeardownEvent is one bounded input to the pure authority kernel.

type TeardownEventKind added in v0.13.0

type TeardownEventKind string

TeardownEventKind is the closed set of runtime topology events accepted by the automatic teardown authority kernel. Provider commands, pane contents, prompts, shell history, and transcripts are deliberately not inputs.

const (
	TeardownEventPaneExited     TeardownEventKind = "pane-exited"
	TeardownEventWindowUnlinked TeardownEventKind = "window-unlinked"
)

func TeardownEventKinds added in v0.13.0

func TeardownEventKinds() []TeardownEventKind

TeardownEventKinds returns the closed event vocabulary.

type TeardownGeneration added in v0.13.0

type TeardownGeneration string

TeardownGeneration classifies an event's generation guard.

const (
	TeardownGenerationCurrent TeardownGeneration = "current"
	TeardownGenerationStale   TeardownGeneration = "stale"
)

type TeardownObservation added in v0.13.0

type TeardownObservation string

TeardownObservation says what the final inventory pass proved about the exact tmux server named by an event. Only ExactSocket is positive authority; every other value is an explicit fail-closed observation, not an absence.

const (
	TeardownObservationExactSocket      TeardownObservation = "exact-socket"
	TeardownObservationUnavailable      TeardownObservation = "unavailable"
	TeardownObservationEmpty            TeardownObservation = "empty"
	TeardownObservationNoServer         TeardownObservation = "no-server"
	TeardownObservationPermissionDenied TeardownObservation = "permission-denied"
	TeardownObservationSiblingSocket    TeardownObservation = "sibling-socket"
	TeardownObservationForeignHost      TeardownObservation = "foreign-host"
)

func TeardownObservations added in v0.13.0

func TeardownObservations() []TeardownObservation

TeardownObservations returns the closed final-observation vocabulary.

type TeardownOwnerChain added in v0.13.0

type TeardownOwnerChain struct {
	SocketIdentity string
	SessionHandle  string
	PaneHandle     string
	WindowHandle   string
	PaneUID        string
	WindowUID      string
	RootKind       Kind
	RootUID        string
	Generation     string
}

TeardownOwnerChain is the exact Registry chain an event claims.

type TeardownReason added in v0.13.0

type TeardownReason string

TeardownReason is a closed diagnostic vocabulary for the decision table.

const (
	TeardownReasonInvalidInput          TeardownReason = "invalid-input"
	TeardownReasonStaleGeneration       TeardownReason = "stale-generation"
	TeardownReasonUnavailable           TeardownReason = "observation-unavailable"
	TeardownReasonEmptyObservation      TeardownReason = "empty-observation"
	TeardownReasonNoServer              TeardownReason = "no-server"
	TeardownReasonPermissionDenied      TeardownReason = "permission-denied"
	TeardownReasonSiblingSocket         TeardownReason = "sibling-socket"
	TeardownReasonForeignHost           TeardownReason = "foreign-host"
	TeardownReasonNonCausalTermination  TeardownReason = "non-causal-termination"
	TeardownReasonPaneTeardown          TeardownReason = "pane-teardown"
	TeardownReasonAwaitingPaneExit      TeardownReason = "awaiting-pane-exit"
	TeardownReasonAwaitingWindowUnlink  TeardownReason = "awaiting-window-unlink"
	TeardownReasonLiveSiblingPane       TeardownReason = "live-sibling-pane"
	TeardownReasonWindowTeardown        TeardownReason = "window-teardown"
	TeardownReasonProjectTeardown       TeardownReason = "project-teardown"
	TeardownReasonMixedOwnerChain       TeardownReason = "mixed-owner-chain"
	TeardownReasonConflictingOwnerFacts TeardownReason = "conflicting-owner-facts"
	TeardownReasonStaleOwnerBinding     TeardownReason = "stale-owner-binding"
	// TeardownReasonDeadPaneCleanupRetry is the exact retryable boundary after
	// a clean current-generation decision but before the dead tmux Pane and its
	// managed Pane resource have converged. It never grants authority by itself;
	// the next pass must re-prove the same supervisor evidence, generation,
	// owner chain, socket, and dead mirror.
	TeardownReasonDeadPaneCleanupRetry TeardownReason = "exact-dead-pane-cleanup-retry"
)

type TerminationClassification added in v0.13.0

type TerminationClassification string

TerminationClassification is the closed evidence vocabulary.

The five values are not a severity ladder, they are five different *kinds of proof*. Intentional means a canonical control action said so in writing before it acted. Normal and Abnormal mean a supervisor actually reaped the child and read its wait status. Unknown means nothing proved anything, and it is a legal, expected answer rather than a failure.

const (
	// TerminationIntentional is a canonical control-plane action's own record
	// of its intent. Only TerminationSourceControlAction may carry it.
	TerminationIntentional TerminationClassification = "intentional"
	// TerminationNormal is an observed exit status 0.
	//
	// It is emphatically NOT intent. A provider that exits 0 because the
	// operator typed a quit command and a provider that exits 0 because it
	// finished a batch produce byte-identical wait statuses, so promoting
	// exit 0 to "intentional" would invent evidence nobody produced.
	TerminationNormal TerminationClassification = "normal"
	// TerminationKilled is an observed external hangup. It says the managed
	// process was killed from outside its own exit path, but it deliberately
	// says nothing about whose intent caused that kill. A matching canonical
	// control-action receipt remains the only authority that can say
	// intentional.
	TerminationKilled TerminationClassification = "killed"
	// TerminationAbnormal is an observed non-zero exit status or a death by
	// signal.
	TerminationAbnormal TerminationClassification = "abnormal"
	// TerminationUnknown is an explicitly evidence-free record: the runtime
	// object is gone and no receipt explains why.
	//
	// It is a value rather than an absence precisely so that an absence is
	// never re-read as normality. Only TerminationSourceReconcile may write it,
	// and writing it is what makes the reconciliation idempotent: the second
	// pass over the same disappearance finds the same document already stored
	// and changes nothing.
	TerminationUnknown TerminationClassification = "unknown"
)

func ClassifyProcessExit added in v0.13.0

func ClassifyProcessExit(exitCode int, signal string) TerminationClassification

ClassifyProcessExit maps one reaped wait status onto observed evidence.

signal is the empty string for a child that exited on its own. A signalled child is abnormal regardless of the code the platform reports alongside it, and exit status 0 is the only normal outcome.

type TerminationDisposition added in v0.13.0

type TerminationDisposition struct {
	// Exit is the Agent exit classification, meaningful only for an
	// Agent-owned Pane.
	Exit AgentExit
	// Reason is the status.reason clause for this classification.
	Reason string
}

TerminationDisposition is the lifecycle decision one classification implies.

It is a total function of the classification and of nothing else. Not of the role, not of whether an Agent owns the Pane, not of which host answered: those decide *what gets written*, never *what the evidence means*.

func DispositionFor added in v0.13.0

func DispositionFor(classification TerminationClassification) TerminationDisposition

DispositionFor maps one classification onto its lifecycle decision.

An unrecognized classification is treated as unknown rather than refused. A registry written by a build with a wider vocabulary must still converge here, and "this build cannot interpret the evidence" is precisely what unknown means.

type TerminationEvidence added in v0.13.0

type TerminationEvidence struct {
	Source         TerminationSource         `json:"source"`
	Classification TerminationClassification `json:"classification"`
	ObservedAt     time.Time                 `json:"observedAt,omitzero"`
	PaneUID        string                    `json:"paneUID,omitempty"`
	AgentUID       string                    `json:"agentUID,omitempty"`
	Generation     string                    `json:"generation,omitempty"`
	// ExitCode is a pointer so "exited with status 0" and "never exited on its
	// own" are different documents rather than the same zero value.
	ExitCode    *int   `json:"exitCode,omitempty"`
	Signal      string `json:"signal,omitempty"`
	OperationID string `json:"operationID,omitempty"`
}

TerminationEvidence is the minimal durable record of why one managed process stopped.

It is a pointer field with omitempty everywhere it is stored, which is the entire read-compatibility story: a registry written before this field existed decodes to nil, a nil value re-encodes to an absent key, and the document round-trips byte-identically. It was additive inside schemaVersion 1 and needed no migration step -- bumping the envelope would have made every already installed build reject the file fail-closed with ErrSchemaTooNew.

It carries no command text, no pane content, and no provider conversation data. Everything here is either a closed vocabulary value, a uid this build minted, or a numeric wait status.

func (*TerminationEvidence) Clone added in v0.13.0

Clone returns a deep copy, including the exit-code pointer.

func (TerminationEvidence) IsZero added in v0.13.0

func (t TerminationEvidence) IsZero() bool

IsZero reports an entirely empty receipt.

func (*TerminationEvidence) Summary added in v0.13.0

func (t *TerminationEvidence) Summary() string

Summary renders one stored receipt as a single compact operator-facing clause: the classification, the provenance that produced it, and the wait status when one was actually read.

It carries no timestamp. Every surface that renders this already dates it in the unit that surface uses -- describe prints the absolute UTC instant, the columnar reads print a relative age -- and a clause that embedded one form would force the other surface to strip it back out.

type TerminationOutcome added in v0.13.0

type TerminationOutcome struct {
	// Applied reports whether the registry now stores this receipt.
	Applied bool
	// Duplicate marks a receipt the registry already stores verbatim.
	Duplicate bool
	// Stale marks a receipt that lost a guard check.
	Stale bool
	// Reason is the operator-facing diagnostic for a refused receipt.
	Reason string
}

TerminationOutcome is the result of offering one receipt to the registry.

A refused receipt is not an error. Duplicate delivery, a receipt from a replaced generation, and a receipt naming a Pane that has since been deleted are all *expected* on this transport, and the honest answer to each of them is "nothing changed, here is why".

type TerminationProjection added in v0.13.0

type TerminationProjection struct {
	// PaneUID is the projected Pane.
	PaneUID string
	// AgentUID is the Agent released, empty for a shell Pane and for a Pane its
	// Agent no longer binds.
	AgentUID string
	// Classification is the evidence the projection acted on.
	Classification TerminationClassification
	// Phase is the phase the Agent landed in, empty when no Agent moved.
	Phase AgentPhase
	// PaneRetained reports whether the logical Pane resource survived. A shell
	// Pane always survives a runtime disappearance; an Agent's current managed
	// Pane is released with the Agent.
	PaneRetained bool
	// Changed reports whether the registry now differs.
	Changed bool
	// Reason is the diagnostic for a projection that changed nothing.
	Reason string
}

TerminationProjection is what one projection did, or why it did nothing.

type TerminationProjectionInput added in v0.13.0

type TerminationProjectionInput struct {
	// PaneUID is the Pane the caller observed to have no runtime object.
	PaneUID string
	// Generation is an optional guard. When it is set it must equal the Pane's
	// current activation generation, so an event describing a materialization
	// the registry has already replaced projects nothing.
	Generation string
	// ObservedAt is when the absence was observed. A zero value means now.
	ObservedAt time.Time
}

TerminationProjectionInput names one absent Pane for projection.

type TerminationSource added in v0.13.0

type TerminationSource string

TerminationSource is the closed provenance vocabulary of one receipt.

It is deliberately not caller-supplied free text. Provenance decides whether a receipt may claim intent, and an open string here would let anything that can write the registry manufacture "the operator asked for this".

const (
	// TerminationSourceSupervisor is the managed process supervisor that
	// actually reaped the child and read its wait status.
	TerminationSourceSupervisor TerminationSource = "supervisor"
	// TerminationSourceControlAction is a canonical control-plane lifecycle
	// action recording its own intent before it mutates anything live.
	TerminationSourceControlAction TerminationSource = "control-action"
	// TerminationSourceReconcile is the exit reconciliation pass reporting that
	// it re-observed the exact host, found a managed runtime object gone, and
	// found no receipt that explains it.
	//
	// It is the only source that may claim TerminationUnknown, and unknown is
	// the only thing it may claim. That pairing is what keeps "nobody proved
	// anything" from being writable by the two sources that *do* observe
	// something: a supervisor that reaped a child and a control action that
	// stated its intent both know more than this, and a build that let either
	// of them file an unknown receipt could erase real evidence with it.
	TerminationSourceReconcile TerminationSource = "reconcile"
)

type Transaction

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

Transaction records the resources one operation created so a post-create failure can be rolled back in reverse order. It never touches resources that existed before the operation started or that another operation created.

A pre-create failure records nothing, so rolling back is a no-op and the registry is left with zero mutations.

func (*Transaction) Commit

func (t *Transaction) Commit()

Commit closes the transaction successfully and clears the ledger.

func (*Transaction) Created

func (t *Transaction) Created() []string

Created returns the ledger entries in creation order.

func (*Transaction) ID

func (t *Transaction) ID() string

ID returns the operation id recorded for this transaction.

func (*Transaction) Rollback

func (t *Transaction) Rollback()

Rollback removes, in reverse creation order, only the resources this operation created that still carry the same uid. Pre-existing resources and resources created by another operation are never touched.

type Window

type Window struct {
	APIVersion string       `json:"apiVersion"`
	Kind       Kind         `json:"kind"`
	Metadata   ObjectMeta   `json:"metadata"`
	Spec       WindowSpec   `json:"spec"`
	Status     WindowStatus `json:"status,omitzero"`
}

Window is owned by exactly one Project or ControlSession and always owns an initial Pane. The two owner kinds are structurally identical from a Window's point of view -- ownerRef is an opaque uid plus a kind -- so nothing below a Window needs to know which root it hangs from.

func (Window) Clone

func (w Window) Clone() Window

Clone returns a deep copy of the Window.

func (Window) DisplayName

func (w Window) DisplayName() string

DisplayName returns the duplicate-allowed user-facing Window name. Registry files written before Window display names were projected leave the field empty, so those Windows safely fall back to their stable metadata.name until a runtime observation supplies the tmux window_name.

func (Window) HasCondition

func (w Window) HasCondition(conditionType string) (Condition, bool)

HasCondition reports whether a Window carries conditionType.

type WindowRootCascadeDeletePlan added in v0.13.0

type WindowRootCascadeDeletePlan struct {
	Operation           ProjectLifecycleOperation
	Decision            TeardownDecision
	Desired             Registry
	Changed             bool
	PaneUID             string
	AgentUID            string
	WindowUID           string
	RootKind            Kind
	RootUID             string
	ProjectRoot         string
	DeletedProjects     int
	DeletedWindows      int
	DeletedPanes        int
	DeletedAgents       int
	DeletedReservations int
}

WindowRootCascadeDeletePlan is the schema-valid desired Registry produced by one exact pane-exited/window-unlinked pair.

func PlanWindowRootCascadeDelete added in v0.13.0

func PlanWindowRootCascadeDelete(registry Registry, paneEvent, unlinkEvent TeardownEvent, now time.Time) (WindowRootCascadeDeletePlan, error)

PlanWindowRootCascadeDelete consumes one stored exact Pane receipt together with its matching window-unlinked event. It deletes only that Window subtree; Project and ControlSession roots always survive.

type WindowSpec

type WindowSpec struct {
	AnchorPaneRef       string `json:"anchorPaneRef"`
	DefaultShellPaneRef string `json:"defaultShellPaneRef,omitempty"`
	// contains filtered or unexported fields
}

WindowSpec separates the stable role-agnostic Window anchor from the optional direct shell used by shell-requiring compatibility consumers.

func (WindowSpec) CompatibilityShellPaneRef added in v0.13.0

func (s WindowSpec) CompatibilityShellPaneRef() string

CompatibilityShellPaneRef preserves the pre-Phase-1 shell consumer result: prefer the explicit default shell and otherwise fall back to the anchor. It is pure and never changes role, ownership, or Registry bytes.

func (*WindowSpec) UnmarshalJSON added in v0.13.0

func (s *WindowSpec) UnmarshalJSON(data []byte) error

UnmarshalJSON records raw field presence so schemaVersion 2 can distinguish the unpublished primaryPaneRef shape from final-v2 without guessing from values. Mixed authorities remain representable only long enough for the schema normalizer to reject the whole document fail-closed.

type WindowStatus

type WindowStatus struct {
	Conditions       []Condition `json:"conditions,omitempty"`
	RuntimeSessionID string      `json:"runtimeSessionID,omitempty"`
	RuntimeID        string      `json:"runtimeID,omitempty"`
}

WindowStatus carries the observed conditions of one Window.

There is deliberately no stored liveness field here, and there never will be one: a stored bool is exactly the defect this block replaces. Liveness is derived from a live observation at read time; what is stored is only the preserved *reason* an object stopped being bound.

The field is `omitzero` rather than `omitempty` so a Window that has never carried a condition serializes byte-identically to a registry written before this field existed. That kept the addition inside schemaVersion 1: an older build reading a newer file simply ignores a key it does not know, and a newer build reading an older file decodes the absent key to the zero value.

Jump to

Keyboard shortcuts

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