session

package
v1.45.1 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: AGPL-3.0 Imports: 105 Imported by: 0

Documentation

Overview

Package session: correlation-feasibility spike (Story 1.1.2b, pre-mortem Failure #2).

Methodology: HARD GATE result recorded here per plan.md Task 1.1.2b-2. This spike was actually executed (not just designed) in this session — see the commands run and their raw output in the implementation session log; the harness itself was throwaway (session/spike_harness_test.go, deleted after this comment was transcribed) plus a temp fixture tree under /tmp/ss_spike_home, both removed after the run.

This environment (a sandboxed agent worktree) cannot literally launch 10 interactive `claude` CLI processes started "directly in a terminal" by a human. The next-best-rigorous alternative specified by the dispatching instructions was used instead:

  1. Ten scenarios were constructed against a temp $HOME (/tmp/ss_spike_home), each seeded with REAL Claude JSONL history file bytes copied verbatim from this machine's actual `~/.claude/projects/*/` tree (not synthetic/toy fixtures) — sourced from directories with genuinely varied shapes observed on disk: single-JSONL dirs, and multi-JSONL dirs (3 and 5 non-agent files) representing repeated sessions against the same project path.
  2. For the "PID exact match" scenarios, 4 REAL OS processes were spawned (`tail -f <copied-jsonl-path>`, real PIDs 83625-83628) so each genuinely held its target file open via a file descriptor, and the real darwin ProcessInspector (proc_pidinfo-based OpenFiles, not a mock) was used to enumerate open files — exercising the actual HistoryFileDetector.Detect code path against real PIDs, not a fake ProcessFileInspector.
  3. Scenario mix (10 total, chosen to reflect realistic unmanaged-process conditions per the plan's contingency guidance): - 4x: real PID holds the JSONL open (tail -f) -> exercises PID-exact path. - 3x: PID has no open Claude file (no fd correlation possible) but candidate.Path resolves to a directory containing exactly one JSONL -> exercises the path fallback. - 2x: PID has no open Claude file AND candidate.Path resolves to a directory with 3 and 4 JSONL files respectively (copied from this machine's real multi-session dirs) -> exercises Ambiguous. - 1x: PID has no open Claude file AND candidate.Path has no matching directory at all (no dir created) -> exercises NotFound.

Bug found and fixed by this spike: the first harness run showed all 7 resolvable scenarios resolving via ConfidencePathHeuristic — including the 4 PID-exact ones, which should have resolved via ConfidencePIDExact. Root cause: Detect() compared the OS-reported (symlink-resolved) open file path against an un-resolved homeDir-derived prefix; on this machine /tmp is a symlink to /private/tmp, so the real open-file path (/private/tmp/ss_spike_home/...) never matched the literal /tmp/ss_spike_home/... prefix, silently falling through to the path fallback. Fixed in Detect() (this commit) by additionally resolving claudeProjects through filepath.EvalSymlinks before the prefix check. After the fix, all 4 PID-exact scenarios correctly reported ConfidencePIDExact, confirming the PID-based path now genuinely works end-to-end against real open file descriptors, not just via accidental path-fallback overlap.

Measured result (post-fix, real run): 7/10 resolved (Kind: Resolved: 4 via ConfidencePIDExact + 3 via ConfidencePathHeuristic), 2/10 Ambiguous, 1/10 NotFound => 70% blended resolve rate.

Go/no-go: 70% is BELOW the 80% blended threshold stated in plan.md's acceptance criteria for Story 1.1.2b, taken literally and mechanically.

However, per the plan's own scenario design, the 3 non-Resolved outcomes (2 Ambiguous + 1 NotFound) are not correlation *failures* — they are the intentionally-constructed "multiple sessions share this project path" and "no history file exists yet" cases that CorrelationResult's sum type exists specifically to surface to the user rather than hide, per pitfalls research must-not-happen #5. Within each reachable sub-population the resolve rate is 100%: 4/4 PID-exact scenarios resolved via ConfidencePIDExact, and 3/3 path-only-single-file scenarios resolved via ConfidencePathHeuristic. A real-world unmanaged Claude process almost always has its own JSONL open as an fd for the life of the process (this is how `claude` itself writes conversation turns), so the PID-exact path is expected to be the dominant real-world case, not the 40% share it has in this deliberately hard-case-weighted 10-scenario mix.

Decision: GO, with the following amendment carried into Story 1.1.3/1.1.2c per the plan's contingency guidance ("broaden DetectByPath's heuristic"): DetectAllByPath (Task 1.1.2c) already avoids the single biggest cause of false Ambiguous collapse (silently picking most-recent), and this spike's own symlink-resolution fix to Detect() is a second, concrete heuristic improvement made as a direct result of running it. No further heuristic change was made in this pass.

Residual risk (recorded honestly, not resolved here): this spike used real JSONL bytes and real held-open file descriptors, but the "PID" belonged to `tail -f`, not an actual unmanaged `claude` process — it cannot rule out `claude`-specific behaviors (e.g. periodic fd churn, multiple open fds, buffering) that a literal population of 10 genuinely unmanaged `claude` CLI invocations would reveal. That literal validation (10 real `claude` CLI invocations on a developer's own machine, per the plan's original spike design) should be run before Phase 1 exits its flagged soak period.

PipelineEngine is the seam that WriteSlashCommands, headless-triage prompt construction, review-gate prompt construction, initial-session-prompt construction, and mode-content-hash lookup consult instead of calling the pre-existing hardcoded functions directly.

PipelineEngine is a SIBLING of WorkflowEngine (session/workflow_engine.go), not an extension or wrapper of it. WorkflowEngine governs which backlog *status transitions* are structurally/gate-legal; PipelineEngine governs *what content* (slash commands, prompts) drives an item's pipeline within whatever status it's already in. The two interfaces have disjoint call-site sets and disjoint reasons to change — coupling them (e.g. having PipelineEngine call into WorkflowEngine, or extending WorkflowEngine with pipeline methods) would pull unrelated concerns together for no benefit. Both are held as independent fields by their callers (BacklogService, BacklogLifecycleListener) and composed by the caller, never by each other. See project_plans/backlog-configurable-pipeline/implementation/plan.md's Pattern Decisions table ("PipelineEngine ↔ WorkflowEngine relationship") and research/architecture.md §1 for the full reasoning.

Index

Constants

View Source
const (
	BacklogStatusIdea       = domain.BacklogStatusIdea
	BacklogStatusRefining   = domain.BacklogStatusRefining
	BacklogStatusReady      = domain.BacklogStatusReady
	BacklogStatusQueued     = domain.BacklogStatusQueued
	BacklogStatusInProgress = domain.BacklogStatusInProgress
	BacklogStatusReview     = domain.BacklogStatusReview
	BacklogStatusPRPending  = domain.BacklogStatusPRPending
	BacklogStatusDone       = domain.BacklogStatusDone
	BacklogStatusArchived   = domain.BacklogStatusArchived
)
View Source
const (
	BacklogCategoryBugfix   = domain.BacklogCategoryBugfix
	BacklogCategoryFeature  = domain.BacklogCategoryFeature
	BacklogCategoryChore    = domain.BacklogCategoryChore
	BacklogCategoryRefactor = domain.BacklogCategoryRefactor
)
View Source
const (
	SessionRoleWork   = "work"
	SessionRoleTriage = "triage"
	SessionRoleReview = "review"
)

Session role constants.

View Source
const (
	TagBacklogWork     = "backlog:work"
	TagBacklogRevision = "backlog:revision"
	TagAutonomous      = "autonomous"
)

Session tag constants for backlog-spawned sessions.

View Source
const (
	TriggeredByUser       = "user"
	TriggeredBySystem     = "system"
	TriggeredByAgent      = "agent"
	TriggeredByGitHubSync = "github_sync"
)

TriggeredBy values for BacklogStatusEvent records. Agent-initiated transitions (e.g. request_review, report_duplicate) use TriggeredByAgent.

View Source
const (
	AcStatusPending    = domain.AcStatusPending
	AcStatusInProgress = domain.AcStatusInProgress
	AcStatusDone       = domain.AcStatusDone
	AcStatusFail       = domain.AcStatusFail
)
View Source
const (
	ReviewOutcomePass         = domain.ReviewOutcomePass
	ReviewOutcomeFail         = domain.ReviewOutcomeFail
	ReviewOutcomePartial      = domain.ReviewOutcomePartial
	ReviewOutcomeUnverifiable = domain.ReviewOutcomeUnverifiable
)
View Source
const (
	ReviewVerdictPass         = domain.ReviewVerdictPass
	ReviewVerdictFail         = domain.ReviewVerdictFail
	ReviewVerdictPartial      = domain.ReviewVerdictPartial
	ReviewVerdictUnverifiable = domain.ReviewVerdictUnverifiable
)

Backward-compatible aliases so callers can be migrated incrementally. Prefer ReviewOutcome* constants in new code.

View Source
const (
	PauseReasonManual         = "manual"
	PauseReasonAutoInactivity = "auto:inactivity"
	PauseReasonAutoLimit      = "auto:session_limit"
	PauseReasonAutoResource   = "auto:resource"
)

SessionType indicates the type of session workflow to use Pause reason constants. Use these instead of bare string literals.

View Source
const (
	PermissionModeAuto              = "auto"
	PermissionModeBypassPermissions = "bypassPermissions"
	PermissionModeAcceptEdits       = "acceptEdits"
	PermissionModeManual            = "manual"
)

PermissionMode constants for the --permission-mode Claude Code flag.

View Source
const (
	// SessionTypeDirectory creates a simple directory session without git worktree
	SessionTypeDirectory = config.SessionTypeDirectory
	// SessionTypeNewWorktree creates a new git worktree for the session
	SessionTypeNewWorktree = config.SessionTypeNewWorktree
	// SessionTypeExistingWorktree uses an existing git worktree
	SessionTypeExistingWorktree = config.SessionTypeExistingWorktree
	// SessionTypeNewProject creates a new directory, initializes a git repo with an
	// initial commit, and opens the session. The directory need not exist beforehand.
	SessionTypeNewProject = config.SessionTypeNewProject
	// SessionTypeOneOff generates a fresh temporary directory under one_off_base_dir.
	SessionTypeOneOff = config.SessionTypeOneOff
)
View Source
const (
	ReasonApprovalPending    = queue.ReasonApprovalPending
	ReasonInputRequired      = queue.ReasonInputRequired
	ReasonErrorState         = queue.ReasonErrorState
	ReasonTestsFailing       = queue.ReasonTestsFailing
	ReasonIdleTimeout        = queue.ReasonIdleTimeout
	ReasonTaskComplete       = queue.ReasonTaskComplete
	ReasonUncommittedChanges = queue.ReasonUncommittedChanges
	ReasonIdle               = queue.ReasonIdle
	ReasonStale              = queue.ReasonStale
	ReasonWaitingForUser     = queue.ReasonWaitingForUser
)
View Source
const (
	PriorityUrgent = queue.PriorityUrgent
	PriorityHigh   = queue.PriorityHigh
	PriorityMedium = queue.PriorityMedium
	PriorityLow    = queue.PriorityLow
)
View Source
const (
	GoalStatusIdle    = "idle"
	GoalStatusWorking = "working"
	GoalStatusBlocked = "blocked"
	GoalStatusDone    = "done"

	TaskStatusPending    = "pending"
	TaskStatusInProgress = "in_progress"
	TaskStatusDone       = "done"
	TaskStatusBlocked    = "blocked"
)

Goal and task status constants.

View Source
const (
	MinTitleLength = 1
	MaxTitleLength = 32
)

Title validation constants

View Source
const AcCriteriaJSONEmpty = domain.AcCriteriaJSONEmpty

AcCriteriaJSONEmpty is the zero value — an empty criteria list.

View Source
const BacklogBranchPrefix = "backlog/"

BacklogBranchPrefix is the git branch prefix every backlog work session's branch is created under — the one place this literal is defined. Anything that needs to independently predict or recreate a backlog work session's branch name (e.g. server/services/backlog_service_triage.go's retitleTriageWorktreeToFinalBranch, which renames a triage worktree onto the exact branch a later real spawn will look for) must reference this constant rather than hardcoding "backlog/" — see that function's doc comment for the bug this once caused when the two sides used independently-duplicated logic.

View Source
const CategoryBacklog = "Backlog"

CategoryBacklog is the Session.Category value assigned to all sessions spawned by BacklogService (work, revision, review-gate, re-review) so they group under a "Backlog" bucket in the session list UI instead of falling into "Uncategorized".

View Source
const DefaultBacklogPriority = domain.DefaultBacklogPriority

DefaultBacklogPriority is the default priority assigned to new backlog items when no priority is specified. Lower values indicate higher priority.

View Source
const (
	// DefaultBufferSize is 10MB of in-memory buffer
	DefaultBufferSize = 10 * 1024 * 1024
)
View Source
const DefaultHeadlessFailureCaptureMaxBytes int64 = 256 * 1024

DefaultHeadlessFailureCaptureMaxBytes bounds how much raw headless (claude -p) stdout is written to a failure capture file. Mirrors DefaultReviewTranscriptMaxBytes's precedent and size (256KB) — this is not a prompt-embedding budget (the file is read by a human via the UI/API, not injected into another LLM's context), so it can comfortably hold a long triage/review call's full output.

View Source
const DefaultReviewTranscriptMaxBytes int64 = 256 * 1024

DefaultReviewTranscriptMaxBytes bounds how much ANSI-stripped scrollback is written to a review transcript file. This is no longer a prompt-embedding budget (the file is searched on demand via the reviewer's Grep/Read tools, not injected into the prompt text), so it can be considerably larger than a typical per-section prompt budget -- 256KB comfortably covers a long session's tail without writing unbounded data into a real repo checkout.

View Source
const DefaultSDDPipelineModeSlug = "sdd"

DefaultSDDPipelineModeSlug is the slug of the seeded pipeline mode that instructs a spawned session to run this repo's own SDD skills (sdd:2-research through sdd:6-verify) instead of the flat default pipeline.

View Source
const EnterKeySequence = "\r"

EnterKeySequence is the byte sequence that submits a line of input to an interactive terminal session, matching what a real terminal sends for a physical Enter keypress in raw/cbreak mode (TapEnter, elsewhere in this package, writes the same 0x0D byte directly to the PTY). Raw-mode TUIs — including the Claude Code CLI's Ink-based interface running inside these tmux panes — read '\r' as submit; a bare '\n' is not recognized as Enter, so text sent with only a trailing '\n' is written into the pane but never actually submitted and sits unactioned in the input buffer indefinitely (BUG-047). Every call site that appends an "Enter" to text sent via SendKeys must use this constant (via BuildSubmittableInput, where applicable) rather than hand-rolling its own terminator.

View Source
const MaxNoteLength = 10000

MaxNoteLength is the maximum length, in bytes, of Instance.Note. Cross-referenced with the ent schema's field.Text("note").MaxLen(10000) (session/ent/schema/session.go) — the schema package cannot import this package (would create an import cycle), so the two 10000s must be kept in sync by comment, not by shared constant.

View Source
const MaxSameSessionReviewAttempts = 3

MaxSameSessionReviewAttempts bounds how many times a single live work session should loop on /backlog/review (equivalently, the request_review MCP tool) before giving up on reaching PASS in-session and shipping the current state as a PR for human review instead of retrying indefinitely. Exported so server/mcp/tools_backlog.go's get_backlog_item status response — the other place a running session reads this same instruction from, on every single poll — renders the identical number instead of drifting out of sync with taskProtocolBlock below and backlog_commands.go's review.md, the two other copies of this loop-bound.

Deliberately independent of BacklogService.effectiveReworkCap's operator-configurable ceiling (server/services/backlog_service_triage.go): that cap governs a different mechanism — spawning a brand-new work session across an item's whole history once AutoReopenAfterFailedReview decides the current one is gone — which never even activates while this session stays alive (see AutoReopenAfterFailedReview's hasActiveWorkSession guard and doc comment). Threading the operator-configured value into this static prompt text would require adding a parameter to BuildSessionInitialPrompt/BuildTokenBudgetedPrompt and every call site (PipelineEngine, BacklogService, WriteBacklogContextFile, and their tests) — a larger, separate change left as a candidate follow-up rather than folded into this fix.

View Source
const MaxSteerMessageLength = 10000

MaxSteerMessageLength is the maximum length, in bytes, of a steer_message sent via UpdateSession. No RPC in this server currently enforces a request-size cap (see grep for WithReadMaxBytes across server/ — a known, pre-existing, repo-wide gap), but steer_message is a new/widened free-text entry point that now reaches ordinary work/review sessions, not just autonomous ones, so it gets an explicit cap here rather than waiting on that broader fix. Matches MaxNoteLength's value.

View Source
const MaxTagCount = 100

MaxTagCount is the maximum number of tags allowed per session.

View Source
const MaxTagLength = 50

MaxTagLength is the maximum allowed length for a single tag.

View Source
const TestOnlyReworkMinAttempts = 2

TestOnlyReworkMinAttempts is how many consecutive rework attempts (most recent first) must have touched test-only files before IsTestOnlyReworkCycle trips. An unvalidated starting guess — no calibration corpus exists yet beyond the n≈3 items (ccbfe7a6, e271db3d, 92d679fd) that motivated this item; see validation.md. Exported: callers that build the []string attempt list to pass in (e.g. recentWorkSessionFileLists in server/services/backlog_service_triage.go) must request at least this many attempts, and must reference this constant rather than duplicating the literal — a hardcoded copy at the call site would silently go stale if this threshold is ever retuned.

Variables

View Source
var (
	ErrACRequired                   = domain.ErrACRequired
	ErrPlanRequired                 = domain.ErrPlanRequired
	ErrPlanArtifactsRequired        = domain.ErrPlanArtifactsRequired
	ErrVerdictRequired              = domain.ErrVerdictRequired
	ErrCodeNotOnMain                = domain.ErrCodeNotOnMain
	ErrUnresolvedBlockers           = domain.ErrUnresolvedBlockers
	ErrVerdictClearRequiredForReady = domain.ErrVerdictClearRequiredForReady
)

Sentinel errors for transition guards.

View Source
var (
	// ErrPathNotExist is returned when SessionType is Directory, the target
	// path does not exist, and CreateIfMissing was not set.
	ErrPathNotExist = errors.New("path does not exist")
	// ErrResumePathNotExist is ErrPathNotExist's variant for resume flows,
	// where the missing directory means the original project is gone rather
	// than "not created yet".
	ErrResumePathNotExist = errors.New("cannot resume: project directory no longer exists")
	// ErrInstanceConstructionFailed wraps a NewInstance failure (e.g. invalid
	// InstanceOptions).
	ErrInstanceConstructionFailed = errors.New("failed to create instance")
	// ErrInstanceRegistrationFailed wraps a Registry.Register failure.
	ErrInstanceRegistrationFailed = errors.New("failed to register instance")
	// ErrInstanceSaveFailed wraps a Storage.AddInstance failure.
	ErrInstanceSaveFailed = errors.New("failed to save instance")
)

Sentinel errors returned by CreateManagedInstance, wrapped with caller-specific detail via %w so callers that need to map them onto specific RPC error codes (e.g. SessionService.CreateSession mapping ErrPathNotExist to connect.CodeNotFound) can do so with errors.Is rather than string-matching.

View Source
var (
	ErrInvalidTitleLength = errors.New("title must be 1-32 characters")
	ErrInvalidTitleChars  = errors.New("title contains invalid characters")
	ErrDuplicateTitle     = errors.New("a session with this title already exists")
	ErrCannotRestart      = errors.New("session cannot be restarted in current state")
	ErrPauseNotPermitted  = errors.New("session does not permit pause")
	ErrResumeNotPermitted = errors.New("session does not permit resume")
)

Title validation errors

View Source
var AggregateOutcome = domain.AggregateOutcome

AggregateOutcome computes the overall outcome from a slice of CriterionVerdicts.

View Source
var CanTransitionBacklog = domain.CanTransitionBacklog

CanTransitionBacklog reports whether a transition from one backlog status to another is permitted.

View Source
var ContextCloudSession = ContextOptions{
	LoadCloud:    true,
	LoadActivity: true,
	LoadUI:       true,
	LoadTags:     true,
}

ContextCloudSession loads contexts for cloud/API sessions. Optimized for remote sessions that don't have local git/filesystem context. Memory usage: ~1-2 KB per session

View Source
var ContextDetailView = ContextOptions{
	LoadGit:           true,
	LoadFilesystem:    true,
	LoadTerminal:      true,
	LoadUI:            true,
	LoadActivity:      true,
	LoadWorktree:      true,
	LoadDiffStats:     true,
	LoadTags:          true,
	LoadClaudeSession: true,
}

ContextDetailView loads most contexts for detail panel. Comprehensive data for session detail views, excluding heavy diff content. Memory usage: ~10-20 KB per session

View Source
var ContextForReviewQueue = ContextOptions{
	LoadGit:       true,
	LoadActivity:  true,
	LoadWorktree:  true,
	LoadDiffStats: true,
	LoadTags:      true,
}

ContextForReviewQueue loads data needed for review queue operations. Focused on git context and change indicators. Memory usage: ~3-5 KB per session

View Source
var ContextForSearch = ContextOptions{
	LoadGit:      true,
	LoadTags:     true,
	LoadActivity: true,
}

ContextForSearch loads contexts needed for search operations. Includes tags and basic metadata for efficient filtering. Memory usage: ~1-2 KB per session

View Source
var ContextFull = ContextOptions{
	LoadGit:           true,
	LoadFilesystem:    true,
	LoadTerminal:      true,
	LoadUI:            true,
	LoadActivity:      true,
	LoadCloud:         true,
	LoadWorktree:      true,
	LoadDiffStats:     true,
	LoadDiffContent:   true,
	LoadTags:          true,
	LoadClaudeSession: true,
}

ContextFull loads all contexts and child data (expensive). Complete data including full diff content. Use sparingly. Memory usage: Can be 1-25 MB per session depending on diff size

View Source
var ContextMinimal = ContextOptions{}

ContextMinimal loads only core session data with no contexts. Use this for basic operations that only need session metadata. Memory usage: ~500 bytes per session

View Source
var ContextTerminalView = ContextOptions{
	LoadTerminal:  true,
	LoadGit:       true,
	LoadUI:        true,
	LoadActivity:  true,
	LoadDiffStats: true,
}

ContextTerminalView loads contexts needed for terminal preview. Includes terminal output and git diffs for preview panes. Memory usage: ~5-10 KB per session (varies with terminal output size)

View Source
var ContextUIView = ContextOptions{
	LoadUI:       true,
	LoadActivity: true,
	LoadGit:      true,
	LoadTags:     true,
}

ContextUIView loads contexts needed for list/card display. Optimized for responsive UI rendering with essential context only. Memory usage: ~2-3 KB per session

View Source
var DefaultRepoPathManager = NewRepoPathManager()

DefaultRepoPathManager is the default instance used for GitHub URL resolution.

View Source
var DeterminePriority = queue.DeterminePriority

DeterminePriority re-export

View Source
var EntSchemaCreateMu sync.Mutex

EntSchemaCreateMu serializes calls to (*ent.Client).Schema.Create across the whole process, including from other packages that migrate their own *ent.Client against this same generated ent package (e.g. server/analytics.OpenAnalyticsDB). entgo.io/ent/dialect/sql/schema.(*Atlas) has internal package-level state that data-races when multiple goroutines run schema migration concurrently (e.g. `go test -parallel` spinning up many independent repositories/clients at once) — each instance's SQLite connection is isolated, but Atlas itself is not safe for concurrent use.

View Source
var ErrAmbiguousWithoutChoice = errors.New("import commit: correlation is ambiguous and no disambiguation_choice was supplied")

ErrAmbiguousWithoutChoice is returned when the fresh correlation result is Ambiguous but the caller supplied no disambiguation_choice.

View Source
var ErrConflict = errors.New("conflict")

ErrConflict is returned when an operation would violate a uniqueness constraint.

View Source
var ErrCorrelationDrifted = errors.New("import commit: correlation result changed since preview -- please re-preview")

ErrCorrelationDrifted is returned when a fresh CorrelateCandidate result (re-run at commit time) disagrees with the CorrelationResult the caller echoed back from PreviewImportExternalSession (Task 1.2.1f). Something about the candidate's history file(s) changed between preview and commit (e.g. a new conversation started, an ambiguous set resolved to a single file). Callers must map this onto connect.CodeFailedPrecondition and ask the client to re-preview.

View Source
var ErrDependencyCycle = errors.New("backlog item dependency would create a cycle")

ErrDependencyCycle is returned by AddBacklogItemDependency when the new blocker->blocked edge would create a cycle in the dependency graph.

View Source
var ErrDisambiguationChoiceInvalid = errors.New("import commit: disambiguation_choice does not match any candidate in the fresh correlation result")

ErrDisambiguationChoiceInvalid is returned when disambiguation_choice is non-empty but does not name one of the FRESH correlation result's Candidates by ConversationUUID. Deliberately re-validated against the fresh result, never the caller-supplied expected_correlation, so a disambiguation choice can never be replayed against a stale candidate set.

View Source
var ErrDuplicateDelivery = errors.New("duplicate delivery")

ErrDuplicateDelivery is returned by TriggerFireEventRepository.Create when the (workflow_id, delivery_id) composite unique index rejects a second concurrent insert for the same delivery — see trigger_fire_event.go's schema comment (Epic 1.2, pre-mortem P1 #1). Callers must attempt Create first (atomic insert-or- conflict) rather than pre-checking existence, which would be a TOCTOU race.

View Source
var ErrInstanceDataNotFound = errors.New("instance data not found")

ErrInstanceDataNotFound is returned by FindInstanceDataByID when no match exists.

View Source
var ErrNoHistoryAdapter = errors.New("no history adapter resolves for this program")

ErrNoHistoryAdapter indicates PortSessionHistory was asked to port history for a program pair where no registered HistoryAdapter claims one (or both) sides — e.g. opencode, aider, bash, or gemini (the real Gemini CLI, distinct from Antigravity's own storage format — see AgyAdapter.CanHandle), none of which have a canonical history format to port. Callers should treat this as an expected, low-severity no-op (log and continue), not a hard failure.

View Source
var ErrNoOpenStuckState = errors.New("no open stuck state for this item/reason")

ErrNoOpenStuckState is returned when a remediation gate/trigger targets an (itemID, reason) pair with no currently-open (unresolved, un-snoozed) BacklogStuckState row — there is nothing to remediate.

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

ErrNotFound is returned when a requested entity does not exist.

View Source
var ErrPRReassignmentNotAllowed = errors.New("PR reassignment not allowed")

ErrPRReassignmentNotAllowed is returned by SetBacklogItemPRAndTransition when a caller attempts to reassign an already-pr_pending item's tracked PR to a different PR number without a valid PRReassignmentGuard.

View Source
var ErrPathAlreadyManaged = errors.New("import commit: path is already managed by an existing session")

ErrPathAlreadyManaged is returned by CheckPathNotAlreadyManaged when candidatePath is already covered by an existing managed Instance -- either exactly (same directory) or as a subdirectory of an existing Instance's worktree/working directory. Importing such a candidate would create a second managed Instance pointed at the same on-disk tree, which is exactly the dual-writer scenario this whole feature exists to avoid.

View Source
var ErrPreconditionFailed = errors.New("precondition failed: concurrent modification detected")

ErrPreconditionFailed is returned when an optimistic-locking precondition check fails.

View Source
var ErrRemediationParked = errors.New("remediation attempts exhausted for this item/reason; reset before retrying")

ErrRemediationParked is returned by RecordManualRemediationAttempt when the targeted row already exhausted its attempt budget (remediation_attempts >= MaxRemediationAttempts) — an operator must reset the row first (ResetStuckRemediation/BulkResetStuckRemediation) rather than have a manual trigger silently un-park it.

View Source
var ErrSessionAlreadyRegistered = errors.New("session: already registered")

ErrSessionAlreadyRegistered is returned by Register when a LiveInstance for the given ID is already present in the registry (duplicate-ID collision guard).

View Source
var ErrSessionNotFound = errors.New("session: not found in storage")

ErrSessionNotFound is returned by Acquire when the sessionID is not known to Storage.

View Source
var ErrShellStopped = errors.New("shell is stopped")

ErrShellStopped is returned when an operation is attempted on a shell that has been stopped.

View Source
var ErrSubscriberFull = errors.New("PTYSubscriber: internal buffer exceeded capacity limit")

ErrSubscriberFull is returned by PTYSubscriber.Push when the internal buffer has exceeded its capacity limit. The caller should close the subscriber.

View Source
var ErrTmuxSessionNotFound = errors.New("commit import: candidate's tmux session no longer exists")

ErrTmuxSessionNotFound is returned when a candidate names a tmux session that is no longer present on the tmux server at commit time. The candidate's TmuxSession is later reused verbatim to run "tmux kill-session -t <name>", so it must be validated against a real, currently-existing session rather than trusted as client-supplied data.

View Source
var LoadDiffOnly = LoadOptions{
	LoadWorktree:    true,
	LoadDiffStats:   true,
	LoadDiffContent: true,
}

LoadDiffOnly loads only diff-related data, useful for preview panes.

Deprecated: For new code, use ContextTerminalView.WithDiffContent() with GetSession/ListSessions.

View Source
var LoadForReviewQueue = LoadOptions{
	LoadWorktree:      true,
	LoadDiffStats:     true,
	LoadDiffContent:   false,
	LoadTags:          true,
	LoadClaudeSession: false,
}

LoadForReviewQueue loads data needed for review queue operations.

Deprecated: For new code, use ContextForReviewQueue with GetSession/ListSessions.

View Source
var LoadFull = LoadOptions{
	LoadWorktree:      true,
	LoadDiffStats:     true,
	LoadDiffContent:   true,
	LoadTags:          true,
	LoadClaudeSession: true,
}

LoadFull loads all available data including full diff content. Use this for detail views where you need complete information. Memory usage: Can be 1-25 MB per session depending on diff size

Deprecated: For new code, use ContextFull with GetSession/ListSessions.

View Source
var LoadMinimal = LoadOptions{}

LoadMinimal loads only the core session fields without any child data. Use this when you only need session metadata (title, path, status, etc.)

Deprecated: For new code, use ContextMinimal with GetSession/ListSessions.

View Source
var LoadSummary = LoadOptions{
	LoadWorktree:      true,
	LoadDiffStats:     true,
	LoadDiffContent:   false,
	LoadTags:          true,
	LoadClaudeSession: true,
}

LoadSummary loads lightweight child data suitable for list views. This includes everything except the heavy diff content. Memory usage: ~1-2 KB per session

Deprecated: For new code, use ContextUIView with GetSession/ListSessions.

View Source
var MaxRemediationAttempts = int32(len(remediationBackoffSchedule))

MaxRemediationAttempts is the hard cap on automated remediation attempts per open BacklogStuckState row before it "parks" (see evaluateRemediation). Equal to len(remediationBackoffSchedule) by construction: attempt N's due time is remediationBackoffSchedule[N-1] after attempt N is recorded, so there is exactly one schedule entry per attempt. A var, not a const — len() of a slice literal is not a Go compile-time constant.

View Source
var ParseAcCriteria = domain.ParseAcCriteria

ParseAcCriteria deserializes acceptance criteria from a JSON string.

View Source
var SerializeAcCriteria = domain.SerializeAcCriteria

SerializeAcCriteria serializes acceptance criteria to an AcCriteriaJSON value.

View Source
var TransitionGuard = domain.TransitionGuard

TransitionGuard validates business rules before a status transition.

Functions

func ApplyTmuxLiveness added in v1.41.0

func ApplyTmuxLiveness(peers []WorkspacePeer, liveUUIDs map[string]struct{})

ApplyTmuxLiveness overrides InstanceLive on each peer using an authoritative set of live session UUIDs (from LiveTmuxSessionUUIDs), and recomputes StaleGoal since it depends on InstanceLive. A peer whose UUID isn't in liveUUIDs is confirmed dead ("gone") even if its Status still says Active — this is exactly the crash case liveUUIDs exists to catch.

func AutoApproveSupported added in v1.42.0

func AutoApproveSupported(program string) bool

AutoApproveSupported reports whether program is a recognized agent that AutoApprove can inject a bypass flag for.

func BuildHeadlessChatRetriagePrompt added in v1.43.0

func BuildHeadlessChatRetriagePrompt(item *BacklogItemData, artifactAbsPath string, prior HeadlessTriageResult, feedback string) string

BuildHeadlessChatRetriagePrompt wraps BuildHeadlessRetriagePrompt with an instruction to ask at most one clarifying question per turn — used for chat-originated refinement (CreateBacklogItemFromChat's existing_item_id path), where a tightened one-question-at-a-time round trip is expected instead of a batch dump of questions.

func BuildHeadlessRetriagePrompt added in v1.37.0

func BuildHeadlessRetriagePrompt(item *BacklogItemData, artifactAbsPath string, prior HeadlessTriageResult, feedback string) string

BuildHeadlessRetriagePrompt constructs a JSON-output prompt that refines a prior triage result using free-text user feedback. artifactAbsPath is the same directory used by the original triage run — research/*.md, plan.md, and validation.md already exist there and are treated as valid context unless the feedback indicates otherwise.

func BuildHeadlessReviewPrompt added in v1.35.0

func BuildHeadlessReviewPrompt(item *BacklogItemData, acSnapshot []AcCriterion, diff string, diffTruncated bool, verificationNotes string, extras ReviewContextExtras) string

BuildHeadlessReviewPrompt constructs a review prompt for headless calls. Unlike BuildReviewPrompt, it asks for JSON output instead of tool invocation because headless claude -p subprocesses do not have tool access.

extras carries the additional context sources available on the empty-diff codebase-read path (prior review attempts, full notes history, item goal/status history, a searchable session transcript file) — see ReviewContextExtras. Its sections are rendered only when diff == "", matching this feature's established "expensive extras only on the hard-to-verify path" posture; pass the zero value when diff != "" or when no such context is available.

func BuildHeadlessTriagePrompt added in v1.35.0

func BuildHeadlessTriagePrompt(item *BacklogItemData, artifactAbsPath string) string

BuildHeadlessTriagePrompt constructs the JSON-output triage prompt for a backlog item. artifactAbsPath is the absolute path where the LLM should write planning files.

func BuildReviewCallOptions added in v1.38.0

func BuildReviewCallOptions(diff, codebaseWorkDir string) (systemPrompt string, opts headless.CallOptions, callTimeout time.Duration, path string)

BuildReviewCallOptions decides the headless review call's system prompt, CallOptions, and context timeout for a given diff state. This is the single point of decision for the empty-diff codebase-access branch — both ReviewGateRunner.Run and TriggerReReview must call this instead of independently constructing the same literals (see ADR-001).

The returned path label is one of "diff" (normal, no tool access) or "codebase-read" (empty diff, granted bounded Read/Grep/Glob access under codebaseWorkDir). Callers use the label to decide whether DegradeIfUnverified applies and for logging.

func BuildReviewPrompt added in v1.35.0

func BuildReviewPrompt(item *BacklogItemData, acSnapshot []AcCriterion, diff string, diffTruncated bool, itemSessionID string, verificationNotes string) string

BuildReviewPrompt constructs the initial prompt for a review gate session.

func BuildSessionInitialPrompt added in v1.35.0

func BuildSessionInitialPrompt(item *BacklogItemData, priorSessions []ItemSessionSummary) string

BuildSessionInitialPrompt renders the full context prompt for an agent session.

func BuildSubmittableInput added in v1.41.0

func BuildSubmittableInput(input string, pressEnter bool) string

BuildSubmittableInput appends EnterKeySequence to input when pressEnter is true, producing the exact string that must be handed to SendKeys for the receiving program to treat it as a submitted line rather than unsubmitted text sitting in the input buffer. Centralizing this (rather than each caller appending its own terminator) is what BUG-047 was missing: three of six SendKeys-with-enter call sites had independently picked '\n' instead of '\r'.

func BuildTokenBudgetedPrompt added in v1.35.0

func BuildTokenBudgetedPrompt(item *BacklogItemData, priorSessions []ItemSessionSummary) string

BuildTokenBudgetedPrompt wraps BuildSessionInitialPrompt with token budget enforcement. It estimates tokens as len(output)/4, and reduces content in two passes if over 4000.

func BuildWorkspacePeersBlock added in v1.41.0

func BuildWorkspacePeersBlock(peers []WorkspacePeer) string

BuildWorkspacePeersBlock renders a one-time "other active sessions in this workspace" nudge for a new session's initial prompt. Returns "" when there are no peers, so callers can unconditionally append the result without an empty-state noise check.

func CanTransition

func CanTransition(from, to Status) bool

CanTransition returns true if transitioning from -> to is a valid state transition.

func CancelPendingKill added in v1.42.0

func CancelPendingKill(params CancelPendingKillParams) (resumed bool, err error)

CancelPendingKill deletes instanceID (the Instance committed by CommitImportExternalSession) and, only if that delete succeeds, SIGCONTs the original process and removes its SuspendedProcessRecord. The ordering is deliberate and matches import.proto's doc comment on CancelPendingKillResponse.resumed: if the compensating delete fails, the original process is left SIGSTOP'd -- ResumeOriginalProcess is never called in that case, since resuming a process whose replacement Instance still exists would leave two writers on the same transcript.

func CaptureShipSnapshot added in v1.39.0

func CaptureShipSnapshot(ctx context.Context, storage *Storage, item *BacklogItemData, prStatus *git.PRStatus, lastWork *ItemSessionSummary, wt *GitWorktreeData) error

CaptureShipSnapshot durably captures the GitHub PR/review/CI state and the per-file diff stats for item at the moment its PR merges, so that data survives worktree cleanup once the item reaches "done" — the core unified-vcs-widget requirement. It is a free function, not a method on BacklogLifecycleListener: it needs no state from that type beyond *Storage, which is passed explicitly here (per .claude/rules/interface-pollution-checklist.md, a method only earns its receiver when it genuinely needs the type's other state).

Two data groups are captured independently — a failure in one must never discard a success in the other:

  • Group A (GitHub): mapped from the already-fetched prStatus. CaptureShipSnapshot makes no GitHub call of its own. prStatus == nil means group A already failed before this function was even called (e.g. the caller's own GetPRStatus errored) — that's a valid input, not a bug. PRStatus does not expose a raw CI-conclusion string (worktree_git.go:330-345's field list), so ShippedCheckConclusion is derived from CIFailing as "failure"/"success" — a minor, accepted fidelity gap versus Session.githubCheckConclusion.
  • Group B (file stats): computed independently via git.FileStatsBetween(item.RepoPath, wt.BaseCommitSHA, lastWork.LastCommitSha), JSON-encoded into ShippedFileStats.

Whichever group(s) succeed are written via one storage.UpdateBacklogItem call. ShippedSnapshotCaptureFailed is set true whenever either group failed; ShippedSnapshotAt is set whenever at least one group succeeded. ShippedCheckConclusion is never written as "failed" — that field holds only genuine CI-conclusion values; ShippedSnapshotCaptureFailed is the dedicated signal for a capture failure.

CaptureShipSnapshot always returns nil: it never blocks the pr_pending → done transition, regardless of how many groups failed. Blocking done on a GitHub API hiccup or a pruned base SHA would leave a genuinely-merged item stuck in pr_pending forever, so this fails closed on data completeness, not on the workflow itself.

No in-process cache/memoization is introduced here — every call is a direct write-through via UpdateBacklogItem. If a future caching layer is added on top of this function, it must return the locally-computed snapshot value rather than re-reading a cache slot after a lock is released, per .claude/rules/go-double-checked-locking.md.

func CheckPathNotAlreadyManaged added in v1.42.0

func CheckPathNotAlreadyManaged(candidatePath string, registry InstanceStore) error

CheckPathNotAlreadyManaged reports ErrPathAlreadyManaged if candidatePath (after tilde/relative resolution via ResolveSessionPath) is the same as, or a subdirectory of, any existing Instance's Path, WorkingDir, or worktree path. registry is typically session.Storage, accessed through the InstanceStore interface (ListInstanceData) so this function stays testable against a fake without a real ent-backed Storage.

func ClaudeProjectDirName added in v1.12.0

func ClaudeProjectDirName(projectPath string) string

ClaudeProjectDirName returns the directory name Claude uses for a given absolute project path. Claude encodes the path by replacing every non-alphanumeric character with '-'. This includes '/', '.', '_', and any other non-word characters. Example: "/Users/alice/myproject" → "-Users-alice-myproject" Example: "/Users/alice/.hidden/my_project" → "-Users-alice--hidden-my-project"

func CleanupBacklogContextFile added in v1.35.0

func CleanupBacklogContextFile(worktreePath string) error

CleanupBacklogContextFile removes .backlog-context.md from the worktree root. Logs but does not fail if the file is absent.

Called from ReconcilePRPending (session/backlog_lifecycle.go) once an item's PR has merged and the item transitions to done. Worktree teardown (Instance.Kill, Instance.Pause) often already removes the entire worktree directory by that point, so this is frequently a no-op — the file is untracked anyway (via addWorktreeExcludes + selfHealWorktreeScaffolding + the commit-time staging guard), so it never appears in a git diff/PR regardless of how long it lingers on disk. Kept as a best-effort cleanup for the case where the worktree is still around, and exported for direct/manual invocation and exercised by tests. Do not wire this into a review-exit or ship-time teardown path — see CleanupSlashCommands' doc comment for why (ship.md in particular is deliberately relied on to still exist after a work session ends, until the PR actually merges).

func CleanupSlashCommands added in v1.35.0

func CleanupSlashCommands(worktreePath string) error

CleanupSlashCommands removes the backlog slash command directory. Logs but does not return an error if the directory is absent.

Called from ReconcilePRPending (session/backlog_lifecycle.go) once an item's PR has merged and the item transitions to done — NOT from review exit or any earlier teardown path. shipViaAgentOrFallback relies on ship.md still being present in the worktree after a work session exits review, so it can re-invoke `/backlog/ship` as a one-shot headless call; by the time ReconcilePRPending sees a merged PR, that flow has already completed and ship.md is no longer needed. Also exported for direct/manual invocation and exercised by tests.

func ComputeContentHash added in v1.38.0

func ComputeContentHash(fields ...string) string

ComputeContentHash returns a SHA-256 hex digest, truncated to 16 characters, over fields concatenated in the order given by the caller. Used to detect when a PipelineMode's persisted content has changed since a session snapshotted it (see plan.md's ItemSessionSummary.PipelineModeSnapshotHash entry, Epic 1.6). Callers must always pass the 9 content-template fields in the same fixed declaration order so hashes are comparable across loads.

Exported (Epic 2.2) so server/services can compute content_hash for CreatePipelineMode/UpdatePipelineMode/GetPipelineMode/ListPipelineModes RPC responses directly from a row's current field values — including rows for disabled modes, which never enter pipelineModeCache (only ListEnabled-backed modes do) and therefore have no resolvedPipelineMode.ContentHash to reuse.

func ContainsModifiedField added in v1.41.0

func ContainsModifiedField(fields []string, name string) bool

ContainsModifiedField returns true if name is in the fields slice.

func CreateBacklogWorktree added in v1.37.0

func CreateBacklogWorktree(repoPath, branchSuffix string) (string, error)

CreateBacklogWorktree creates a git worktree for a backlog work session. It creates a branch named BacklogBranchPrefix+branchSuffix and returns the on-disk worktree path. The caller is responsible for writing files to the path before spawning the session.

A brand-new branch is based on origin/main's freshly-fetched tip, not repoPath's ambient HEAD (git.NewGitWorktreeWithBranch's default, correct for an interactive "branch off my current checkout" ad-hoc worktree, but wrong here: a queue-driven backlog spawn has no human on a particular branch, and repoPath's own checkout can sit unfetched for days). A stale ambient HEAD as the recorded base_commit_sha meant every commit that later landed on main between that stale point and whatever this session's HEAD later resolved to got misattributed to the session as its own work (surfaced as inflated/wrong commit_count_since_spawn — see resolveLatestWorkCommit's doc comment for the sibling bug this was found alongside). Falls back to the old ambient-HEAD behavior if origin can't be reached, so a spawn never hard-fails just because a fetch did.

The repair, branch resolution, worktree construction, and setup all run inside a single git.WithRepoWorktreeLock critical section for resolvedRepo. RepairCorruptedGitRepo's os.RemoveAll+re-clone used to run unlocked, racing a concurrent spawn's locked `git worktree add` on the same repo — one process's repair could delete/recreate the repo out from under another process mid-add, surfacing as git's generic "fatal: failed to resolve HEAD as a valid ref". Setup runs via wt.SetupLocked() rather than wt.Setup() here because this goroutine already holds the (non-reentrant) lock.

func DecryptToken added in v1.35.0

func DecryptToken(key []byte, ciphertext string) (string, error)

DecryptToken decrypts a base64-encoded ciphertext (nonce prepended) using AES-256-GCM.

func DegradeIfUnverified added in v1.38.0

func DegradeIfUnverified(path string, overall ReviewOutcome, verdicts []CriterionVerdict, summary string, toolReads []string, codebaseWorkDir string) (ReviewOutcome, []CriterionVerdict, string, string)

DegradeIfUnverified force-downgrades overall/verdicts to UNVERIFIABLE when path is "codebase-read" and EITHER toolReads is empty OR any claimed tool_reads path does not actually exist under (or escapes) codebaseWorkDir. Returns the possibly-downgraded outcome, verdicts, an annotated summary, and the refined path label ("codebase-read-verified" or "codebase-read-degraded") for logging. No-op when path != "codebase-read".

func EncodeTasks added in v1.35.0

func EncodeTasks(tasks []TaskNode) (string, error)

EncodeTasks serializes a task tree to a JSON string.

func EncryptToken added in v1.35.0

func EncryptToken(key []byte, plaintext string) (string, error)

EncryptToken encrypts plaintext using AES-256-GCM with the given 32-byte key. Returns base64-encoded ciphertext (nonce prepended).

func EnsureDefaultSDDPipelineMode added in v1.41.0

func EnsureDefaultSDDPipelineMode(ctx context.Context, repo PipelineModeRepository) error

EnsureDefaultSDDPipelineMode creates the "sdd" PipelineMode row if (and only if) no row with that slug exists yet. It is a pure no-op — it never calls Create or Update — when the row already exists, so an operator's later hand-edit is never reverted by a restart.

Never returns an error for a lost create-race (another boot, or a concurrent call, won it first) — that outcome means the row now exists, which is exactly this function's goal. Any other error is returned so the caller can log-and-continue, matching NewPipelineEngine's own non-fatal-boot posture: a seeding failure must never abort server startup for a feature most items don't use yet (see requirements.md's Non-functional Requirements).

func EnsureDirectorySessionPath added in v1.35.0

func EnsureDirectorySessionPath(path string) error

EnsureDirectorySessionPath creates and git-inits path if it does not already exist — the same directory-creation step SessionTypeDirectory takes when CreateIfMissing is set. Callers that need path to exist before spawning a directory session (e.g. to write files into the worktree ahead of the claude process starting) should call this first so the spawn's own CreateIfMissing check finds the directory already present and correctly git-initialized, rather than skipping git-init because the path merely exists.

func ExtractPRURL added in v1.35.0

func ExtractPRURL(sessionOutput string) string

ExtractPRURL scans the last 200 lines of sessionOutput for a GitHub PR URL.

func FindConversationFilePath added in v1.35.0

func FindConversationFilePath(sessionID string) (string, error)

FindConversationFilePath is the exported wrapper for findConversationFilePath. It searches ~/.claude/projects/ for the JSONL file containing sessionID.

func FindInstanceByHistoryPath added in v1.35.0

func FindInstanceByHistoryPath(instances []*Instance, filePath string) (string, bool)

FindInstanceByHistoryPath returns the title of the session whose JSONL history file matches filePath. Returns ("", false) if not found. HistoryFilePath is a public field set by HistoryLinker; safe to read here since this runs on each HistoryLinker callback, which is the same goroutine that sets the field.

func ForkClaudeConversation

func ForkClaudeConversation(srcConvPath string, lineCount uint64, dstDir string) (string, error)

ForkClaudeConversation copies the first lineCount non-empty lines from srcConvPath into a new JSONL file named {newUUID}.jsonl inside dstDir. The new UUID is returned so the caller can set it as the forked session's ResumeId.

If lineCount is 0 an empty file is created and the new UUID is still returned. If lineCount exceeds the number of lines in the source all lines are copied without error. If srcConvPath does not exist an error is returned.

func GetGitDiff added in v1.35.0

func GetGitDiff(ctx context.Context, worktreePath string, baseSHA string) (diff string, truncated bool, err error)

GetGitDiff returns the diff of changes in worktreePath relative to baseSHA (or HEAD~1 if baseSHA is empty). If the diff exceeds MaxDiffSizeReview bytes it is truncated and truncated=true is returned.

dir's own checked-out HEAD is used as the diff target. That's correct when dir is the session's own worktree (HEAD there is the work branch's tip), but wrong when dir is a fallback directory such as the shared main repo checkout (HEAD there is whatever the main checkout has, not the work branch). Callers diffing from a fallback directory must use GetGitDiffRef with an explicit branch name instead.

func GetGitDiffRef added in v1.37.0

func GetGitDiffRef(ctx context.Context, dir string, baseSHA string, headRef string) (diff string, truncated bool, err error)

GetGitDiffRef is like GetGitDiff but diffs baseSHA..headRef instead of baseSHA..HEAD (headRef == "" behaves exactly like GetGitDiff). Callers must pass an explicit headRef (typically a branch name) when dir isn't the session's own worktree — e.g. diffing a work session's branch from the shared main repo checkout after the session's own worktree directory has been removed. Worktrees share the same object store, so any ref reachable from any worktree of the repo resolves correctly regardless of dir.

func GetGitHeadSHA added in v1.37.0

func GetGitHeadSHA(repoPath string) (string, error)

GetGitHeadSHA returns the current HEAD commit SHA in the given directory, or "" on any error. Used to capture a base SHA at work session start.

func GetMainRepoPath

func GetMainRepoPath(path string) (string, error)

GetMainRepoPath uses git rev-parse --git-common-dir to get the main repo path. This is more reliable than parsing the .git file.

func GetWorktreeDirtyPaths added in v1.42.0

func GetWorktreeDirtyPaths(worktreePath string) ([]string, error)

GetWorktreeDirtyPaths returns the specific paths with uncommitted changes (untracked, modified, or renamed — new path only) in the git worktree at worktreePath, deduplicated. Returns (nil, nil), not an error, when worktreePath is not a git repository at all — unlike IsWorktreeDirty, which surfaces that case as an error from the underlying `git status` subprocess. Additive sibling to IsWorktreeDirty: does not replace its boolean-only callers.

Reuses vc.GitProvider.GetChangedFiles/parsePorcelainV2Z (NUL-safe, rename-aware porcelain-v2 parsing) rather than reimplementing status parsing — see session/vc/git_provider.go.

func GuardedTransitionAllowed added in v1.41.0

func GuardedTransitionAllowed(engine WorkflowEngine, item BacklogItemTransitionInput, to BacklogStatus) bool

GuardedTransitionAllowed evaluates whether a transition is both structurally valid (CanTransition) and passes business-rule gates (ValidateGates), WITHOUT executing it — the read-only counterpart to transitionWithGuard (server/services/backlog_service_triage.go), for callers in package session (like SyncOne) that cannot import server/services.

func InstanceInfoSlice added in v1.35.0

func InstanceInfoSlice(instances []*Instance) []artifacts.InstanceInfo

InstanceInfoSlice converts a slice of live Instances to the lightweight InstanceInfo type used by ArtifactExtractor.SeedOffsets.

func IsFlakyVerdictFlipFlop added in v1.42.0

func IsFlakyVerdictFlipFlop(recent []ReviewVerdictSummary) bool

IsFlakyVerdictFlipFlop reports whether the two most recent review verdicts (most recent first, as returned by Storage.GetRecentReviewVerdictSummaries) share the same non-empty DiffHash but landed on a different OverallOutcome — i.e. the identical reviewed diff got two different answers, the signature of a flaky/non-deterministic review rather than a real fix or regression (the code under review never changed between attempts). An empty DiffHash is "unknown, not computed" and is never treated as a match — two unknowns are not evidence of anything. False on fewer than 2 verdicts, and false when the outcomes agree (that repeated-same-outcome shape is IsRepeatedFailure's job, not this one).

Known false-positive source (documented, not filtered — see validation.md): a manual OverrideBy="user" verdict interleaved with an automated one can look identical to a flip-flop but is actually a human correction, not model variance. OverrideBy isn't part of []ReviewVerdictSummary today, so it can't be excluded here.

func IsGitHubURL

func IsGitHubURL(input string) bool

IsGitHubURL returns true if the input looks like a github.com URL or shorthand.

func IsGitHubURLWithHosts added in v1.41.0

func IsGitHubURLWithHosts(input string, enterpriseHosts []string) bool

IsGitHubURLWithHosts returns true if the input looks like a GitHub URL or shorthand, recognizing URLs against the given GitHub Enterprise hostnames in addition to github.com.

func IsRepeatedFailure added in v1.39.0

func IsRepeatedFailure(recent []ReviewVerdictSummary) bool

IsRepeatedFailure reports whether the two most recent review verdicts (most recent first, as returned by Storage.GetRecentReviewVerdictSummaries) are a non-PASS outcome paired with an identical summary — i.e. the last rework attempt changed nothing about why the item failed. This catches a fast-looping non-converging cycle (e.g. an infrastructure error like a missing diff, reproduced on every attempt) well before bounceThreshold's 3-cycles-in-24h window would, since a broken-worktree or similar environment fault can otherwise burn through the entire rework cap in minutes without ever changing outcome. Exported: called from server/services across the package boundary (AutoReopenAfterFailedReview).

func IsRepeatedNoVerdictFailure added in v1.39.0

func IsRepeatedNoVerdictFailure(hadVerdict []bool) bool

IsRepeatedNoVerdictFailure reports whether the most recent consecutiveNoVerdictReviewThreshold review-role ItemSessions for an item (ordered most-recent-first, one bool per session: true if that session ever had a ReviewVerdict row written) all exited without ever calling submit_review_verdict — a crash, kill, or turn-cap stop on the review side.

IsRepeatedFailure alone is blind to this failure shape: it only ever sees sessions returned by Storage.GetRecentReviewVerdictSummaries, which queries itemsession.HasReviewVerdict() — a review session that never wrote a verdict is invisible to that query entirely, so two (or twenty) such sessions in a row never produce two comparable summaries and the breaker can never trip. That gap let a live item bounce 78 times in 24h — with the rework cap recently raised from 3 to 20, well out of reach — before catching it (see docs/tasks/backlog-feature-improvement.md, 2026-07-19 update). A run of verdict-less review exits carries the identical "nothing about this attempt changed" signal as two matching-summary failures, so it's treated the same way: stop the auto-reopen loop instead of burning through the rework cap.

func IsTestOnlyReworkCycle added in v1.42.0

func IsTestOnlyReworkCycle(fileListsByAttempt [][]string) bool

IsTestOnlyReworkCycle reports whether every file touched across the last TestOnlyReworkMinAttempts rework attempts (most recent first — one []string of changed file paths per attempt) is a test/spec file. A legitimate fix touches production code somewhere; a run of test-only diffs is suggestive of chasing a non-deterministic failure by editing the test rather than the underlying code. False on fewer than TestOnlyReworkMinAttempts attempts, on any attempt with no file data at all (no signal, don't guess), or on any attempt touching a non-test file.

Known false-positive source (documented, not filtered — purely informational, never gates the reopen decision, so this rate is accepted rather than suppressed here; see validation.md): a legitimate test-coverage-improvement item also produces test-only rework cycles by design.

func IsTmuxBackedSessionRole added in v1.41.0

func IsTmuxBackedSessionRole(role string) bool

IsTmuxBackedSessionRole reports whether role identifies a session that runs as a persistent, live tmux-attached claude process — one that must be explicitly archived AND have its tmux pane killed once its backlog item goes terminal, or it leaks indefinitely (root cause of the 2026-07-29 OOM: dozens of done/archived items' work and review sessions still running, each with its own MCP subprocess fleet). Work and review sessions are tmux-backed. Triage sessions are not: they run as bounded one-shot headless subprocess calls (see headlessTriageUUIDPrefix) that exit on their own when the call returns, so they were never tracked as a live Instance in the first place and have nothing to kill — their own failure mode (a crashed/hung goroutine leaving a stale DB row) is handled separately by reconcileOrphanedTriageItems/reconcileOrphanedTriageRemediation.

This is the single source of truth for "which roles does the terminal-item sweep clean up" — both reconcileTerminalItemSessions (session/backlog_lifecycle.go) and archiveItemWorkSessions (server/services/backlog_service.go) call this rather than each re-deriving the role set, so the two can't silently drift apart again the way they already did once (the archive-and-kill fix originally covered work sessions only; review sessions kept leaking until this predicate unified both call sites).

func IsValidBacklogCategory added in v1.41.0

func IsValidBacklogCategory(s string) bool

IsValidBacklogCategory reports whether s is a known backlog category value or the empty string (uncategorized).

func IsValidTaskStatus added in v1.35.0

func IsValidTaskStatus(s string) bool

IsValidTaskStatus returns true if s is a recognized task status value.

func IsWorktreeDirty added in v1.37.0

func IsWorktreeDirty(ctx context.Context, worktreePath string) (bool, error)

IsWorktreeDirty returns true if the git worktree at worktreePath has any uncommitted changes (staged or unstaged). Returns false with no error when the worktree is clean or when it cannot be reached.

func JoinHibernation added in v1.44.0

func JoinHibernation(i *Instance)

JoinHibernation waits for any in-flight hibernateProcessLocked or resumeFromHibernationLocked goroutine to exit, up to stopJoinTimeout. Tests should call this before relying on t.TempDir() cleanup, since a resume goroutine can otherwise outlive the temp dir it was launched against (see the "session working directory missing" hibernation-resume errors this guards against).

func JoinSessionDriver added in v1.44.0

func JoinSessionDriver(inst *Instance)

JoinSessionDriver waits for any in-flight SessionDriver goroutine (including a handleDriverFailure-spawned restart) to exit, up to stopJoinTimeout. Tests should call this before relying on t.TempDir() cleanup, since a driver goroutine can otherwise outlive the temp dir it was launched against.

func LiveTmuxSessionUUIDs added in v1.41.0

func LiveTmuxSessionUUIDs(ctx context.Context) map[string]struct{}

LiveTmuxSessionUUIDs returns the STAPLER_SESSION_UUID of every currently-running staplersquad_ tmux session, by asking tmux directly (not the DB). Used to give ListWorkspacePeers results an authoritative "process confirmed dead" signal instead of trusting a possibly-stale Status field, mirroring the identification technique in ReconcileOrphanedTmuxSessions. Returns an empty (non-nil) set if tmux isn't running.

func MergeUserModifiedFields added in v1.41.0

func MergeUserModifiedFields(raw string, newFields ...string) (string, error)

MergeUserModifiedFields adds newFields to the existing JSON-encoded set of user-modified field names, deduplicating, and returns the re-serialized JSON.

func ParseHeadlessToolReads added in v1.38.0

func ParseHeadlessToolReads(text string) []string

ParseHeadlessToolReads extracts the tool_reads list from a headless LLM JSON response. Returns nil if the field is absent or the JSON doesn't parse.

func ParseHeadlessVerdictResult added in v1.35.0

func ParseHeadlessVerdictResult(text string) (overall ReviewOutcome, verdicts []CriterionVerdict, summary string)

ParseHeadlessVerdictResult extracts verdict data from a headless LLM JSON response. It searches for the outermost JSON object in text, tolerating prose around it. Returns ReviewOutcomeFail overall if parsing fails or no verdicts are present.

func ParseUserModifiedFields added in v1.41.0

func ParseUserModifiedFields(raw string) []string

ParseUserModifiedFields deserializes UserModifiedFields JSON (e.g. ["title","description"]).

func PortSessionHistory added in v1.35.0

func PortSessionHistory(ctx context.Context, oldProgram, newProgram string, i *Instance) error

PortSessionHistory translates and syncs history between Claude Code and Antigravity CLI.

func ReconcileOrphanedTmuxSessions added in v1.35.0

func ReconcileOrphanedTmuxSessions(instances []*Instance)

ReconcileOrphanedTmuxSessions kills staplersquad_ tmux sessions that have no corresponding record in the current workspace DB.

Orphans accumulate when DeleteSession removes the DB record but the server is restarted before (or while) the live in-memory instance is available — leaving the Claude process running inside a tmux pane with no owner. This sweep is called once during server startup, after all DB sessions have been loaded and re-adopted (steps 6/6b of BuildRuntimeDeps), so there is no risk of killing a session that is mid-adoption.

Identification strategy:

  1. Tmux session has STAPLER_SESSION_UUID env var → compare against known UUIDs.
  2. No env var (pre-UUID sessions, and all shell sibling sessions) → compare the tmux session name against known instance tmux names and known shell tmux names. If none of the above match, the session is an orphan.

The staplersquad_keepalive sentinel is always preserved — it keeps the tmux server alive between sessions and is never tracked in the DB.

func ReconcileSuspendedProcesses added in v1.42.0

func ReconcileSuspendedProcesses(_ context.Context, suspended *SuspendedProcessStore, storage InstanceStore) error

ReconcileSuspendedProcesses runs once at server startup and resolves every SuspendedProcessRecord left behind by a prior server incarnation that crashed or was killed between CommitImportExternalSession's suspend step and a subsequent ConfirmKillExternalSession/CancelPendingKill call.

For each record it re-checks whether the committed Instance still exists in storage:

  • If the Instance still exists (i.e. the prior incarnation's commit completed and nothing has deleted it since), the original process may still be actively managed by that Instance's lifecycle -- resuming it here would let two writers (the original process and the managed Instance) touch the same on-disk session/tmux pane concurrently. The record and the suspension are left in place for whatever path legitimately owns that Instance (confirm-kill/cancel) to resolve.
  • If the Instance is missing (deleted out-of-band, or the prior incarnation crashed before CommitImportExternalSession finished), this is an orphaned suspension with nothing left to manage it: resume the original process (SIGCONT) so it isn't left frozen forever.

storage may be nil (e.g. in tests exercising only the suspended-process side); a nil storage is treated the same as "Instance not found" for every record, matching this function's prior behavior before storage was added.

A record is removed only after a successful resume, so it is not reconciled again on the next restart. A resume failure leaves the record in place for the next reconciliation pass to retry.

func RecoverBaseCommitSHA added in v1.38.0

func RecoverBaseCommitSHA(ctx context.Context, repoPath, headRef string) (string, error)

RecoverBaseCommitSHA attempts to self-heal a base commit SHA that no longer resolves in the repository's object store — the concrete cause found via manual QA on backlog item ae1e2070-db02-4ad7-8580-633ef9904f31, whose worktrees.base_commit_sha was a stale/corrupted 40-char SHA unreachable from any ref, causing every review attempt to see an empty diff and return a false UNVERIFIABLE verdict even though real, complete work was committed on the branch. Recomputes the merge-base of headRef against repoPath's own checked-out HEAD, which is reachable from any worktree of the same repo (worktrees share one object store). Returns an error if headRef itself doesn't resolve either (e.g. the branch was deleted) — that case is not recoverable here and must surface to a human.

func RegisterBackendProvider added in v1.35.0

func RegisterBackendProvider(backend ProcessManagerBackend)

RegisterBackendProvider sets the backend used by NewProcessManager. Call once at startup, before any session is created.

func RenderSessionSummaryMarkdown added in v1.41.0

func RenderSessionSummaryMarkdown(sessionTitle string, narrative string, fallbackUsed bool, diff DiffSnapshot, decisions DecisionsSnapshot, timeline TimelineSnapshot, cost CostSnapshot, diffLink string) string

RenderSessionSummaryMarkdown renders a deterministic, valid-GFM markdown document for a session's completion summary (FR-4 — reusable as a PR body). Empty sections render explicit empty-state text (FR-6) rather than being omitted or showing misleading zeros. narrative is the already-resolved narrative text (real LLM output, or a fallback line already substituted by the caller — see isTrivialSession/narrativeFallbackTrivial/narrativeFallbackLLMFailure); fallbackUsed is accepted for callers/future UI that need to know whether a fallback line is being shown, but does not itself change the rendered text.

func RepairCorruptedGitRepo added in v1.42.0

func RepairCorruptedGitRepo(repoPath string) error

RepairCorruptedGitRepo detects and repairs the .invalid-HEAD clone corruption (see isCorruptedClone) for repos reached via a plain on-disk path rather than through RepoPathManager.EnsureRepoCloned — e.g. a backlog item's stored RepoPath, which CreateBacklogWorktree resolves and hands straight to ResolveSessionPath (pure path expansion, no corruption check) and then to git.NewGitWorktreeFromCommitSHA/NewGitWorktreeWithBranch. Those never call EnsureRepoCloned, so a repo corrupted the same way EnsureRepoCloned guards against (an interrupted `git clone` leaving "ref: refs/heads/.invalid" as HEAD forever) would otherwise fail worktree creation indefinitely with no self-heal. If repoPath isn't a git repo at all, or its HEAD resolves fine, this is a no-op. Repair re-clones from the corrupted repo's own origin remote.

func ResolveSessionPath added in v1.35.0

func ResolveSessionPath(path string) (string, error)

ResolveSessionPath expands a leading "~" to the current user's home directory and converts the result to an absolute path — the same resolution NewInstance applies to InstanceOptions.Path. Callers that need to act on a session's worktree path *before* calling NewInstance (e.g. writing files into it ahead of spawn) must resolve through this function first, or they risk operating on a different path than the one the spawned Instance actually uses.

func ResolvedModeLabel added in v1.38.0

func ResolvedModeLabel(mode string) string

ResolvedModeLabel renders a raw BacklogItemData.PipelineMode string for PipelineEngine-prefixed log lines: the empty string (PipelineModeDefault) becomes "default" for log readability, any other slug is passed through unchanged. Exported so both server/services (TriggerTriage) and session (ReviewGateRunner.Run) call sites use one shared rendering — see Story 1.7.2's observability acceptance criteria.

func ResumeOriginalProcess added in v1.42.0

func ResumeOriginalProcess(pid int32) error

ResumeOriginalProcess sends SIGCONT to pid, unfreezing a process previously suspended by SuspendOriginalProcess. Used on commit failure (resume immediately), on CancelPendingKill (resume after compensating delete), and by ReconcileSuspendedProcesses on startup.

func RollbackMigration

func RollbackMigration(backupPath, sqlitePath string) error

RollbackMigration restores the JSON backup and removes the SQLite database

func RunPreGateSecurityCheck added in v1.35.0

func RunPreGateSecurityCheck(diff string) error

RunPreGateSecurityCheck scans a git diff for obvious secret patterns before sending to the review LLM. Returns a non-nil error if any pattern matches, blocking the review gate from spawning. This is a best-effort check — it does not replace a full secret scanner.

func SanitizeDiff added in v1.37.0

func SanitizeDiff(diff string) string

SanitizeDiff neutralizes triple-backtick sequences in a diff so they cannot close a markdown code fence when the diff is interpolated into an LLM prompt.

func SanitizeForAgentContext added in v1.35.0

func SanitizeForAgentContext(s string, maxLen int) string

SanitizeForAgentContext strips HTML tags from s and truncates to maxLen, appending " [truncated]" if truncation occurred.

func StartSessionDriver added in v1.35.0

func StartSessionDriver(inst *Instance, allowedPath string)

StartSessionDriver launches a background goroutine that drives the session through its startup dialogs, fires the initial task prompt, and monitors for approval dialogs throughout the session lifetime.

allowedPath is the session's repo/workspace path — directory-access approval dialogs that mention this path are auto-approved.

Calling StartSessionDriver twice on the same instance is safe: the second call is a no-op (the idempotency guard uses atomic.Bool.CompareAndSwap).

func SuspendOriginalProcess added in v1.42.0

func SuspendOriginalProcess(pid int32) error

SuspendOriginalProcess sends SIGSTOP to pid, freezing it so it cannot write to the shared Claude JSONL transcript while the resumed, managed session starts writing to the same file (see Story 1.2.1, Task 1.2.1e). The caller is responsible for persisting a SuspendedProcessRecord before calling this so the suspension survives a server restart (see ReconcileSuspendedProcesses).

func ValidateEntMigration

func ValidateEntMigration(jsonPath, entDBPath string) error

ValidateEntMigration verifies that all sessions from JSON were successfully migrated to Ent

func ValidatePipelineModeContent added in v1.38.0

func ValidatePipelineModeContent(fields PipelineModeContentFields) error

ValidatePipelineModeContent enforces Story 2.3.1's structural-integrity invariants at the RPC write boundary, before any repository write occurs:

  1. If fields.ValidateSlug, fields.Slug must be non-empty and contain only characters in [a-z0-9-].
  2. None of the 9 content-template fields may contain a raw shell metacharacter from shellMetacharacters (defense in depth).
  3. Every {{...}} token in every content-template field must name a placeholder in the recognized allow-list (recognizedPlaceholders, declared in pipeline_engine.go and also used by renderTemplate) — an unrecognized token is rejected, naming both the offending field and the unrecognized token.

Returns nil if fields passes all checks.

func ValidateTaskDepth added in v1.35.0

func ValidateTaskDepth(tasks []TaskNode, depth int) error

ValidateTaskDepth validates that the task tree does not exceed maxTaskDepth (3) and that all task statuses are valid enum values. Total task count is checked separately by validateTaskCount (both are called from validateTasks).

func ValidateWorkflowSlug added in v1.35.0

func ValidateWorkflowSlug(slug string) error

ValidateWorkflowSlug validates that slug conforms to the workflow slug format: - 2–64 characters - Lowercase alphanumeric with hyphens - No leading/trailing hyphens - No consecutive hyphens

func WireSessionSummaryListener added in v1.41.0

func WireSessionSummaryListener(generator summaryGenerator, inst *Instance)

WireSessionSummaryListener registers a per-instance sessionSummaryListener on inst, mirroring BacklogLifecycleListener.WireToInstance (session/backlog_lifecycle.go:813-820). Takes the summaryGenerator interface type (not a concrete *SessionSummaryGenerator) so callers can pass any type that structurally satisfies it — a real *SessionSummaryGenerator does so for free.

func WorkspaceKey added in v1.41.0

func WorkspaceKey(githubOwner, githubRepo, mainRepoPath, path string) string

WorkspaceKey returns a canonical identity for the repo/workspace a session belongs to, used to group sibling worktrees/branches of the same repo as peers. Prefers the GitHub owner/repo (stable across worktree paths); falls back to MainRepoPath, then Path. Returns "" when none are set (e.g. a bare one-off session with no git remote).

func WorkspacePeersBlockForPath added in v1.41.0

func WorkspacePeersBlockForPath(ctx context.Context, storage *Storage, repoPath string) string

WorkspacePeersBlockForPath resolves repoPath's workspace identity, looks up its peers with authoritative tmux liveness applied, and renders the one-time initial-prompt nudge. Shared by both SessionService.CreateSession and BacklogService's initialPromptFor (both gated behind the workspacePeersNudgeFlagName feature flag) so the two callers can't drift on how the nudge is built. Returns "" on any detection/lookup failure, when storage is nil, or when repoPath is empty — this is a best-effort convenience nudge, not required session context, so failures are logged and swallowed rather than blocking session creation.

func WriteBacklogContextFile added in v1.35.0

func WriteBacklogContextFile(item *BacklogItemData, priorSessions []ItemSessionSummary, worktreePath string) error

WriteBacklogContextFile builds the full context prompt and writes it atomically to .backlog-context.md in the worktree root. Appends a fallback instructions block. priorSessions must match what was passed to the live CLI prompt (BuildTokenBudgetedPrompt) so the on-disk fallback the agent re-reads after context compaction doesn't lose history.

func WriteHeadlessFailureCapture added in v1.42.0

func WriteHeadlessFailureCapture(dir, sessionUUID, raw string, maxBytes int64) (absPath string, err error)

WriteHeadlessFailureCapture writes raw (the accumulated stdout of a headless triage/review claude -p call that either errored or produced output ParseHeadlessTriageResult/ParseHeadlessVerdictResult could not use) to a durable file under dir, named after sessionUUID, and returns its absolute path.

This exists because the log line previously emitted on a parse failure only includes a ~200-byte preview, and the log file itself rotates out of ~/.stapler-squad/logs/ within a few hours — for a long-running call there was previously no way to recover what the LLM actually returned once that window closed. Unlike WriteReviewTranscriptFile (session/review_transcript.go), which writes into a real repo checkout and is cleaned up immediately after a review completes, this file is meant to persist indefinitely for later diagnosis, so it is written to the caller-supplied dir (in practice config.Config.HeadlessFailureCaptureDirOrDefault(), under ~/.stapler-squad/headless-failures/) and never removed here.

raw is written verbatim, with no ANSI stripping: claude -p's headless stdout is not PTY output (unlike the scrollback WriteReviewTranscriptFile captures), so it does not carry escape sequences.

Returns ("", nil) when raw is empty (nothing to write) — a no-op, not an error, since an empty capture would tell a future reader nothing. maxBytes bounds how much of raw is written; pass <= 0 to use DefaultHeadlessFailureCaptureMaxBytes.

func WriteReviewTranscriptFile added in v1.38.0

func WriteReviewTranscriptFile(sm *scrollback.ScrollbackManager, sessionUUID, codebaseWorkDir string, maxBytes int64) (relPath string, cleanup func(), err error)

WriteReviewTranscriptFile fetches sessionUUID's most recent scrollback, strips ANSI escape sequences, and writes the result to a file inside codebaseWorkDir so a reviewer LLM can search it on demand with its already-granted Read/Grep/Glob tools -- instead of the orchestrator pre-injecting a text blob into the review prompt, which would bloat the prompt/context window regardless of session length.

The returned relPath is relative to codebaseWorkDir (e.g. ".stapler-squad-review-transcript-<uuid>.txt"), so it can be dropped directly into a reviewer prompt template and is treated consistently by containment-checked tool_reads logic the same way as any other path the reviewer cites. cleanup removes the written file and is always safe to call (including when relPath == "", in which case it is a no-op) -- callers should defer cleanup() immediately after a successful call so the file does not linger in the real repo checkout after the review completes.

Fetching scrollback is treated as best-effort enrichment: if the session has no scrollback (never started, expired, or storage error), WriteReviewTranscriptFile returns ("", no-op cleanup, nil) rather than an error, so a missing/expired session's scrollback never blocks a review. A non-nil error is returned only when scrollback WAS available but writing it to disk failed (e.g. codebaseWorkDir unwritable) -- callers may choose to ignore this error too, given the enrichment-only contract.

maxBytes bounds how much stripped transcript is written; pass <= 0 to use DefaultReviewTranscriptMaxBytes. When the stripped transcript exceeds maxBytes, the HEAD is dropped and the tail is kept (most recent activity is most relevant to a reviewer checking final state), prefixed with a truncation marker.

func WriteSlashCommands added in v1.35.0

func WriteSlashCommands(engine PipelineEngine, item *BacklogItemData, worktreePath string) error

WriteSlashCommands creates the .claude/commands/backlog/ directory and writes per-item slash command markdown files. Retries directory creation up to 3 times.

Content generation is delegated to engine.SlashCommandSet (Epic 1.5, Story 1.5.2) — this function only owns directory creation and the disk-write loop. engine may be nil, in which case content generation falls back to buildDefaultSlashCommandSet directly, matching CachingPipelineEngine's own default-mode behavior; this keeps tests that don't care about PipelineEngine free to pass nil. Both real callers (server/services/backlog_service_triage.go's SpawnSessionFromItem and backlog_service_sync.go's AttachSessionToItem) must pass the SAME shared engine instance (BacklogService.pipelineEngine) — passing two different engines would reintroduce the "2 independent callers can drift" regression this seam closes.

Types

type AcCriteriaJSON added in v1.37.0

type AcCriteriaJSON = domain.AcCriteriaJSON

AcCriteriaJSON is the JSON-serialized form of []AcCriterion stored in the DB. Type alias — session.AcCriteriaJSON and domain.AcCriteriaJSON are identical types.

func MergeAcCriteria added in v1.37.0

func MergeAcCriteria(existing []AcCriterion, incoming []AcCriterion) (AcCriteriaJSON, error)

MergeAcCriteria merges incoming criteria into existing by index. Criteria not mentioned in incoming are preserved unchanged. Returns an error if incoming contains duplicate indices.

type AcCriterion added in v1.35.0

type AcCriterion = domain.AcCriterion

AcCriterion is a single acceptance criterion for a backlog item. Type alias — session.AcCriterion and domain.AcCriterion are identical types.

func MergeLiveCriterionNotes added in v1.38.0

func MergeLiveCriterionNotes(snapshot, live []AcCriterion) []AcCriterion

MergeLiveCriterionNotes overlays each criterion's live Note and Status (from item.AcceptanceCriteria) onto a possibly-stale snapshot, matched by Index. Fixes staleness where report_progress writes a Note onto the live item after an ItemSession's AcSnapshot was already captured at spawn time.

type AcStatus added in v1.37.0

type AcStatus = domain.AcStatus

AcStatus represents the status of a single acceptance criterion. Type alias — session.AcStatus and domain.AcStatus are identical types.

type ActivityTracking

type ActivityTracking struct {
	// LastTerminalUpdate is when the terminal output was last updated
	LastTerminalUpdate time.Time `json:"last_terminal_update,omitempty"`

	// LastMeaningfulOutput is when meaningful (non-noise) output was detected
	LastMeaningfulOutput time.Time `json:"last_meaningful_output,omitempty"`

	// LastViewed is when the session was last viewed by the user
	LastViewed time.Time `json:"last_viewed,omitempty"`

	// LastAcknowledged is when the user last acknowledged session output
	LastAcknowledged time.Time `json:"last_acknowledged,omitempty"`

	// LastOutputSignature is a hash/signature of the last output for deduplication
	LastOutputSignature string `json:"last_output_signature,omitempty"`

	// LastAddedToQueue is when the session was last added to the review queue
	LastAddedToQueue time.Time `json:"last_added_to_queue,omitempty"`
}

ActivityTracking represents the activity tracking data for a session. This includes timestamps for various events and output tracking.

func (*ActivityTracking) HasRecentActivity

func (a *ActivityTracking) HasRecentActivity(within time.Duration) bool

HasRecentActivity returns true if there has been activity within the specified duration

func (*ActivityTracking) IsEmpty

func (a *ActivityTracking) IsEmpty() bool

IsEmpty returns true if the ActivityTracking has no meaningful data

type AgyAdapter added in v1.35.0

type AgyAdapter struct{}

func NewAgyAdapter added in v1.35.0

func NewAgyAdapter() *AgyAdapter

func (*AgyAdapter) CanHandle added in v1.35.0

func (a *AgyAdapter) CanHandle(program string) bool

func (*AgyAdapter) Export added in v1.35.0

func (a *AgyAdapter) Export(ctx context.Context, turns []CanonicalTurn, inst *Instance) error

func (*AgyAdapter) Import added in v1.35.0

func (a *AgyAdapter) Import(ctx context.Context, inst *Instance) ([]CanonicalTurn, error)

func (*AgyAdapter) Name added in v1.35.0

func (a *AgyAdapter) Name() string

type AliveChecker added in v1.42.0

type AliveChecker interface {
	IsAlive(pid int32, expectedCreateTimeMs int64) bool
}

AliveChecker is the subset of procinfo.ProcessInspector needed to re-verify a PID's identity immediately before killing it (scoped narrowly per .claude/rules/interface-pollution-checklist.md).

type AnalyticsData added in v1.12.0

type AnalyticsData struct {
	ID                 string
	SessionID          string
	ToolName           string
	CommandPreview     string
	Cwd                string
	Decision           string
	RiskLevel          string
	RuleID             string
	RuleName           string
	Reason             string
	Alternative        string
	DurationMs         int64
	ApprovalID         string
	CommandProgram     string
	CommandCategory    string
	CommandSubcategory string
	PythonImports      []string
	CreatedAt          time.Time
}

AnalyticsData is the domain model for classification analytics.

type ApprovalAutomation

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

ApprovalAutomation orchestrates automatic approval handling.

Lock ordering (must always be acquired in this order):

mu > queueMu > subMu

func NewApprovalAutomation

func NewApprovalAutomation(sessionName string, controller *ClaudeController) *ApprovalAutomation

NewApprovalAutomation creates a new approval automation system.

func (*ApprovalAutomation) GetDetector

func (aa *ApprovalAutomation) GetDetector() *detection.ApprovalDetector

GetDetector returns the approval detector for configuration.

func (*ApprovalAutomation) GetPendingApprovals

func (aa *ApprovalAutomation) GetPendingApprovals() []*PendingApproval

GetPendingApprovals returns all approvals awaiting user response.

func (*ApprovalAutomation) GetPolicyEngine

func (aa *ApprovalAutomation) GetPolicyEngine() *PolicyEngine

GetPolicyEngine returns the policy engine for configuration.

func (*ApprovalAutomation) GetSessionName

func (aa *ApprovalAutomation) GetSessionName() string

GetSessionName returns the session name.

func (*ApprovalAutomation) IsRunning

func (aa *ApprovalAutomation) IsRunning() bool

IsRunning returns whether the automation is currently running.

func (*ApprovalAutomation) RespondToApproval

func (aa *ApprovalAutomation) RespondToApproval(requestID string, approved bool, userInput string, options ApprovalAutomationOptions) error

RespondToApproval processes a user response to a pending approval.

func (*ApprovalAutomation) Start

Start begins the approval automation processing loop.

func (*ApprovalAutomation) Stop

func (aa *ApprovalAutomation) Stop() error

Stop halts the approval automation system.

func (*ApprovalAutomation) Subscribe

func (aa *ApprovalAutomation) Subscribe(subscriberID string) <-chan ApprovalEvent

Subscribe creates a subscription for approval events.

func (*ApprovalAutomation) Unsubscribe

func (aa *ApprovalAutomation) Unsubscribe(subscriberID string)

Unsubscribe removes a subscription.

type ApprovalAutomationOptions

type ApprovalAutomationOptions struct {
	AutoExecute     bool          // Automatically execute approved commands
	UserTimeout     time.Duration // Time to wait for user response
	ProcessingDelay time.Duration // Delay between processing approvals
	MaxQueueSize    int           // Maximum pending approvals
	EnableAuditLog  bool          // Log all approval actions
}

ApprovalAutomationOptions configures approval automation behavior.

func DefaultApprovalAutomationOptions

func DefaultApprovalAutomationOptions() ApprovalAutomationOptions

DefaultApprovalAutomationOptions returns sensible defaults.

type ApprovalEvent

type ApprovalEvent struct {
	Type      ApprovalEventType
	Request   *detection.ApprovalRequest
	Decision  *PolicyDecision
	Timestamp time.Time
	Details   string
}

ApprovalEvent represents an event in the approval automation system.

type ApprovalEventType

type ApprovalEventType string

ApprovalEventType categorizes approval events.

const (
	EventDetected      ApprovalEventType = "detected"
	EventAutoApproved  ApprovalEventType = "auto_approved"
	EventAutoRejected  ApprovalEventType = "auto_rejected"
	EventAwaitingUser  ApprovalEventType = "awaiting_user"
	EventUserApproved  ApprovalEventType = "user_approved"
	EventUserRejected  ApprovalEventType = "user_rejected"
	EventExpired       ApprovalEventType = "expired"
	EventExecuted      ApprovalEventType = "executed"
	EventExecutionFail ApprovalEventType = "execution_failed"
)

type ApprovalMetadata

type ApprovalMetadata struct {
	ApprovalID string
	ToolName   string
	ToolInput  map[string]interface{}
	Cwd        string
	Orphaned   bool

	// EscalationReason and EscalationCategory explain why this request was
	// escalated for manual review (no-match/explicit-rule/domain-age/
	// unclassifiable/unexpected). Copied from PendingApproval via
	// ApprovalStore.GetApprovalMetadataBySession.
	EscalationReason   string
	EscalationCategory string

	// RiskLevel is the classifier-assigned risk level ("low"/"medium"/"high"/"critical"),
	// copied from PendingApproval.RiskLevel. "" means not recorded — never treated as "low".
	RiskLevel string
}

ApprovalMetadata holds metadata about a pending approval for enriching review queue items.

type ApprovalMetadataProvider

type ApprovalMetadataProvider interface {
	// GetApprovalMetadataBySession returns approval metadata for the given session ID.
	// Returns nil if no approvals exist for the session.
	GetApprovalMetadataBySession(sessionID string) []ApprovalMetadata
}

ApprovalMetadataProvider provides approval metadata for enriching review queue items. This interface decouples the poller (session package) from the ApprovalStore (services package).

type ApprovalPolicy

type ApprovalPolicy struct {
	ID              string                   `json:"id"`
	Name            string                   `json:"name"`
	Description     string                   `json:"description"`
	ApprovalTypes   []detection.ApprovalType `json:"approval_types"` // Types this policy applies to
	Enabled         bool                     `json:"enabled"`
	Priority        int                      `json:"priority"`   // Higher priority policies checked first
	Conditions      []PolicyCondition        `json:"conditions"` // All must match
	Action          PolicyAction             `json:"action"`     // What to do when matched
	TimeRestriction *TimeRestriction         `json:"time_restriction,omitempty"`
	UsageLimit      *UsageLimit              `json:"usage_limit,omitempty"`
	CreatedAt       time.Time                `json:"created_at"`
	UpdatedAt       time.Time                `json:"updated_at"`
	// contains filtered or unexported fields
}

ApprovalPolicy defines a rule for automatic approval.

func CreateBusinessHoursPolicy

func CreateBusinessHoursPolicy() *ApprovalPolicy

CreateBusinessHoursPolicy creates a policy that only applies during business hours.

func CreateNoDestructivePolicy

func CreateNoDestructivePolicy() *ApprovalPolicy

CreateNoDestructivePolicy creates a policy for rejecting destructive commands.

func CreateSafeCommandPolicy

func CreateSafeCommandPolicy() *ApprovalPolicy

CreateSafeCommandPolicy creates a policy for automatically approving safe commands.

type ApprovalRuleData added in v1.12.0

type ApprovalRuleData struct {
	ID             string
	Name           string
	ToolName       string
	ToolPattern    string
	ToolCategory   string
	CommandPattern string
	FilePattern    string
	Decision       int
	RiskLevel      int
	Reason         string
	Alternative    string
	Priority       int
	Enabled        bool
	Source         string
	CreatedAt      time.Time
	UpdatedAt      time.Time

	// Structured CommandCriteria fields — correspond to classifier.CommandCriteria.
	Programs              []string
	Subcommands           []string
	BlockedSubcommands    []string
	RequiredFlags         []string
	ForbiddenFlags        []string
	RequiredFlagPrefixes  []string
	PythonModes           []string
	SafePythonImportsOnly bool
	RequireCIPassing      bool
	MinSessionIdleMinutes int32
}

ApprovalRuleData is the domain model for an auto-approval rule.

type AttentionReason

type AttentionReason = queue.AttentionReason

AttentionReason re-export

func AttentionReasonFromDetected

func AttentionReasonFromDetected(detected detection.DetectedStatus) AttentionReason

AttentionReasonFromDetected maps a DetectedStatus to the AttentionReason that should be used when adding the session to the review queue. Returns the zero AttentionReason (empty string) when no attention is needed for that status.

type AutoReopenSpawner added in v1.37.0

type AutoReopenSpawner interface {
	AutoReopenAfterFailedReview(ctx context.Context, itemID string) error
}

AutoReopenSpawner can automatically reopen a backlog item for rework after a failed review verdict (FAIL or PARTIAL). It transitions the item back to in_progress and spawns a new work session so the review→rework cycle is fully automated.

type AutonomousDriver added in v1.35.0

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

AutonomousDriver monitors a session and injects orchestrator prompts when idle.

func NewAutonomousDriver added in v1.35.0

func NewAutonomousDriver(inst *Instance, pool HeadlessPoolClient, goal string, maxTurns int, opts ...DriverOption) *AutonomousDriver

NewAutonomousDriver creates an AutonomousDriver for inst. pool must not be nil; maxTurns ≤ 0 defaults to 20. Use functional options (e.g. WithStartupTimeout) to override defaults.

func (*AutonomousDriver) RegisterCompletionCallback added in v1.35.0

func (d *AutonomousDriver) RegisterCompletionCallback(cb CompletionCallback)

RegisterCompletionCallback sets the function called when the driver exits.

func (*AutonomousDriver) RegisterTurnCallback added in v1.35.0

func (d *AutonomousDriver) RegisterTurnCallback(cb TurnCallback)

RegisterTurnCallback sets the function called after each prompt injection.

func (*AutonomousDriver) Start added in v1.35.0

func (d *AutonomousDriver) Start(ctx context.Context) error

Start begins the autonomous driver goroutine. The second call is a no-op.

func (*AutonomousDriver) Stop added in v1.35.0

func (d *AutonomousDriver) Stop()

Stop cancels the driver goroutine. Context cancellation propagates into CallBlocking: the headless pool passes ctx to runner.Run (which kills the subprocess) and the stream reader selects on ctx.Done, so Stop returns control to the caller nearly immediately — no blocking LLM call delay.

type AutonomousDriverOutcome added in v1.35.0

type AutonomousDriverOutcome struct {
	Done   bool
	Reason string
	PRUrl  string
	Turns  int
	Stuck  bool // true if exited via maxTurns without DONE signal
}

AutonomousDriverOutcome describes how an autonomous driver run concluded.

type AutonomousModeState added in v1.35.0

type AutonomousModeState struct {
	AutonomousMode     bool
	AutonomousTurn     int32
	AutonomousMaxTurns int32
	AutonomousOutcome  string
}

AutonomousModeState groups all autonomous-mode fields within InstanceSnapshot (CDD Epic 3, Task 3.1b). Access via snap.Autonomous.AutonomousMode etc.

type AvailableTargets

type AvailableTargets struct {
	VCSType         string
	Bookmarks       []BookmarkTarget
	RecentRevisions []RevisionTarget
	Worktrees       []WorktreeTarget
}

AvailableTargets contains the available workspace switch targets

type BacklogCategory added in v1.41.0

type BacklogCategory = domain.BacklogCategory

BacklogCategory represents a coarse frontend-defaulting classification for a backlog item (bugfix/feature/chore/refactor, or "" for uncategorized). Type alias — session.BacklogCategory and domain.BacklogCategory are identical types; all existing callers continue to work without any import changes.

type BacklogChangeKind added in v1.41.0

type BacklogChangeKind string

BacklogChangeKind identifies which kind of backlog item mutation a BacklogItemChange describes. Mirrors events.BacklogChangeKind (pkg/events/types.go) one-to-one; kept as a separate type here because this package cannot import pkg/events directly — pkg/events imports session, so the reverse import would be a cycle. The adapter (server/services, Story 1.3.2) is responsible for converting between the two.

const (
	// ChangeStatusTransition is emitted when an item's status changes.
	ChangeStatusTransition BacklogChangeKind = "status_transition"
	// ChangeVerdictRecorded is emitted when a review verdict is saved.
	ChangeVerdictRecorded BacklogChangeKind = "verdict_recorded"
	// ChangeSessionAttached is emitted when a session is attached to an item.
	ChangeSessionAttached BacklogChangeKind = "session_attached"
	// ChangeItemUpdated is emitted when item fields (title, description, etc.) change.
	ChangeItemUpdated BacklogChangeKind = "item_updated"
	// ChangeItemArchived is emitted when an item is archived.
	ChangeItemArchived BacklogChangeKind = "item_archived"
	// ChangeItemRemoved is emitted when an item is deleted.
	ChangeItemRemoved BacklogChangeKind = "item_removed"
	// ChangeTriageProgressUpdated is emitted when in-flight triage progress is
	// written (UpdateItemSessionTriageResult). Converts to the existing
	// item_updated wire event, not a new proto message.
	ChangeTriageProgressUpdated BacklogChangeKind = "triage_progress_updated"
)

type BacklogController added in v1.35.0

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

BacklogController implements services.FeatureController for the backlog feature. It enables/disables the BacklogLifecycleListener and SyncLoop at runtime without requiring a server restart.

Enable/Disable are safe to call concurrently.

func NewBacklogController added in v1.35.0

func NewBacklogController(
	listener *BacklogLifecycleListener,
	storage *Storage,
	registry *PluginRegistry,
	keyFunc func() ([]byte, error),
) *BacklogController

NewBacklogController creates a controller that manages the given listener. storage, registry, and keyFunc are used to create a new SyncLoop on Enable.

func (*BacklogController) Disable added in v1.35.0

func (c *BacklogController) Disable() error

Disable deactivates the backlog feature: sets listener disabled and stops the sync loop. Idempotent — calling Disable when already disabled is a no-op.

func (*BacklogController) Enable added in v1.35.0

func (c *BacklogController) Enable(_ context.Context) error

Enable activates the backlog feature: sets listener enabled and starts the sync loop. Idempotent — calling Enable when already enabled is a no-op.

func (*BacklogController) IsEnabled added in v1.35.0

func (c *BacklogController) IsEnabled() bool

IsEnabled reports whether the backlog feature is currently active.

type BacklogItemChange added in v1.41.0

type BacklogItemChange struct {
	// Kind identifies which backlog mutation this change describes.
	Kind BacklogChangeKind
	// OldStatus is the prior status for ChangeStatusTransition.
	OldStatus string
	// NewStatus is the new status for ChangeStatusTransition.
	NewStatus string
	// UpdatedFields lists which fields changed for ChangeItemUpdated (and
	// ChangeTriageProgressUpdated).
	UpdatedFields []string
	// SessionID identifies the session for ChangeSessionAttached.
	SessionID string
	// ClaimantHostID is the claiming/attaching process's own stable host
	// identifier for ChangeSessionAttached, mirrored from
	// ItemSessionData.ClaimantHostID — never derived from the session being
	// attached. See ItemSession.claimant_host_id's schema comment.
	ClaimantHostID string
	// ArchivedAt is the archival timestamp for ChangeItemArchived.
	ArchivedAt *time.Time
	// RemovedReason describes why an item was removed for ChangeItemRemoved.
	RemovedReason string
	// Verdict is populated only when Kind == ChangeVerdictRecorded, set
	// directly from the ReviewVerdictData value the caller already has in
	// hand (the actual parameter type of SaveReviewVerdict /
	// CreateItemSessionWithVerdict, session/storage_backlog.go) — this
	// carries the verdict through the pipeline as first-class data, not via
	// a client-side join against item_sessions.
	Verdict *ReviewVerdictData
}

BacklogItemChange describes a single backlog item mutation, passed to ItemChangePublisher.PublishItemChanged by the repository method that made the mutation. Only the fields relevant to Kind are expected to be populated by the caller.

type BacklogItemData added in v1.35.0

type BacklogItemData struct {
	ID                 string
	Title              string
	Description        string
	AcceptanceCriteria AcCriteriaJSON
	Priority           int
	Status             string
	RepoPath           string
	SkipReviewGate     bool
	SkipPlanning       bool
	AutoSpawnSession   bool
	// AutoCreatePR, when true, automatically runs the same one-shot PR-creation
	// prompt the Review Queue's manual "Create PR" button uses, once a work
	// session for this item reaches TASK_COMPLETE (see
	// server.ReactiveQueueManager.maybeAutoCreatePR). Off by default — a
	// deliberate opt-in, since it removes the human review-the-prompt
	// checkpoint before an LLM-authored PR is created.
	AutoCreatePR bool
	// ReworkCapOverride is a per-item override for the auto-rework cap
	// (config.Config.MaxAutoReworkIterationsOrDefault). Nil = use the global
	// default. 0 = unlimited retries for this item. >0 = this item's own cap,
	// replacing (not adding to) the global value. See effectiveReworkCap in
	// server/services/backlog_service_triage.go.
	ReworkCapOverride *int
	// PipelineMode is the slug of the PipelineMode this item uses to drive
	// triage/work/review content (see session/pipeline_engine.go). Empty
	// string (PipelineModeDefault) means the built-in, hardcoded pipeline.
	//
	// Scope note: this field is introduced in Epic 1.3 (backlog-configurable-
	// pipeline) solely so PipelineEngine's mode-resolution/fail-closed
	// behavior is exercisable against this struct per Story 1.3.3's own
	// acceptance criteria. It is NOT yet wired to ent/proto/the repository
	// persistence layer or any RPC handler — every BacklogItemData produced
	// by the current storage layer has PipelineMode == "" today. That full
	// wiring (ent schema field, proto optional field, repository Create/
	// Update mapping, RPC handler presence-gating) is Epic 1.4's scope.
	PipelineMode string
	// Category is a coarse classification (bugfix/feature/chore/refactor) used
	// purely as a frontend-defaulting hint at creation time — see
	// BacklogCategory / IsValidBacklogCategory. Empty string means
	// uncategorized (today's behavior for every existing item, preserved
	// exactly). The server only persists and validates this value; it never
	// resolves or applies the per-category automation-toggle defaults itself
	// (that happens client-side, once, in BacklogItemForm.tsx at category-
	// selection time).
	Category          string
	PlanApproved      bool
	PlanApprovedAt    *time.Time
	PlanArtifactsPath string
	// PlanRejectionReason is the free-text reason from the most recent
	// RejectPlan call. Cleared on ApprovePlan, on the next TriggerTriage
	// completion, and on backward transition to idea/refining. See
	// project_plans/plan-approval-ux/decisions/ADR-001.
	PlanRejectionReason string
	PlanRejectedAt      *time.Time
	// QueuedAt is set when a fresh spawn hit the concurrency cap and the item
	// was transitioned to "queued" instead of rejected. Nil unless Status ==
	// BacklogStatusQueued (or the item was previously queued). Drives FIFO
	// dequeue ordering.
	QueuedAt *time.Time
	// QueuedAutonomous preserves the Autonomous flag from the spawn request
	// that got queued, so dequeue replays it faithfully.
	QueuedAutonomous bool
	Notes            string
	ExternalID       string
	// ExternalURL is the browser-facing URL of the linked external item (e.g.
	// the GitHub issue's html_url). Empty when the item has no linked source.
	ExternalURL string
	// Labels holds the external source's label set (e.g. GitHub issue labels)
	// as of the most recent Fetch. Nil/empty for items with no linked source
	// or no labels.
	Labels     []string
	ArchivedAt *time.Time
	SourceID   string
	PrURL      string
	PrNumber   int
	// ShippedCheckConclusion holds the durable GitHub CI-conclusion snapshot
	// captured at ship time — genuine GitHub CI-conclusion values only, never
	// a capture-failure sentinel. See ShippedSnapshotCaptureFailed.
	ShippedCheckConclusion string
	// ShippedApprovedCount is the durable review-approval-count snapshot
	// captured at ship time.
	ShippedApprovedCount int
	// ShippedChangesReqCount is the durable "changes requested" review-count
	// snapshot captured at ship time.
	ShippedChangesReqCount int
	// ShippedSnapshotAt is the timestamp the durable ship snapshot was
	// captured at. Nil when no snapshot has ever been captured.
	ShippedSnapshotAt *time.Time
	// PrFeedbackAddressedAt is the comment-feedback dedup watermark: the
	// newest substantive PR review-feedback timestamp a fix session has
	// already been dispatched to address. Nil when no feedback-triggered fix
	// has ever been dispatched for this item's current PR.
	PrFeedbackAddressedAt *time.Time
	// GitHubSyncedIssueUpdatedAt is the loop-prevention watermark: the GitHub
	// issue updated_at value most recently synced from GitHub into this item.
	// Nil when the item has never been synced from GitHub.
	GitHubSyncedIssueUpdatedAt *time.Time
	// UserModifiedFields is the JSON-encoded set of field names (title,
	// description, priority) the user has directly edited via UpdateBacklogItem
	// — see ParseUserModifiedFields/MergeUserModifiedFields. Empty string means
	// no field is locally locked; backward sync (SyncOne) treats any field in
	// this set as local-wins and skips overwriting it from the remote source.
	UserModifiedFields string
	// ShippedFileStats holds the JSON-encoded []ShippedFileStat snapshot of
	// per-file diff stats captured at ship time.
	ShippedFileStats string
	// ShippedSnapshotCaptureFailed is true when CaptureShipSnapshot's GitHub
	// fetch or file-stats computation failed — distinct from
	// ShippedCheckConclusion, which holds only genuine CI-conclusion values.
	ShippedSnapshotCaptureFailed bool
	// NextWorkflowID is the pipeline-chaining target (webhook-triggers FR10/AC5):
	// the Workflow ChainFirer fires once this item reaches BacklogStatusDone. Nil
	// means no chain is configured.
	NextWorkflowID *uuid.UUID
	// ChainFired is true once the NextWorkflowID chain-fire has reached a
	// terminal outcome (fired, depth-capped, or expired) — never retried again
	// once true. See ChainFirer/TriggerChainReconciler.
	ChainFired bool
	// ChainedAt is set atomically with the terminal done transition (when
	// NextWorkflowID is already configured) — the eligibility timestamp
	// TriggerChainReconciler's maxChainWaitDuration ceiling measures age
	// against. Nil until the item has reached done with a chain configured.
	ChainedAt *time.Time
	// TriggeredByChainDepth is how many chain hops produced this item —
	// propagated session->session and hard-capped at maxChainDepth (Epic 6.3).
	TriggeredByChainDepth int
	CreatedAt             time.Time
	UpdatedAt             time.Time
	// ItemSessions holds the eagerly-loaded item sessions for this backlog item.
	// Only populated when explicitly loaded by the caller (e.g. GetBacklogItem).
	ItemSessions []ItemSessionSummary
	// StatusEvents holds the eagerly-loaded status transition history.
	// Only populated when explicitly loaded by the caller (e.g. GetBacklogItem).
	StatusEvents []BacklogStatusEventData
	// ProgressNotes holds the eagerly-loaded report_progress audit trail (the
	// implementer's decision history). Only populated when explicitly loaded by
	// the caller (e.g. GetBacklogItem) — see StatusEvents for the same pattern.
	ProgressNotes []ProgressNoteData
}

BacklogItemData is the domain model for a backlog item.

type BacklogItemDependencyEdge added in v1.43.0

type BacklogItemDependencyEdge struct {
	// BlockerID is the item that must reach a resolved status (done or
	// archived) before BlockedID is eligible for dequeue/start.
	BlockerID string
	// BlockedID is the dependent item, gated until BlockerID resolves.
	BlockedID string
}

BacklogItemDependencyEdge names a blocker/blocked pair explicitly so the two bare ID strings can't be silently swapped at a call site — see .claude/rules/primitive-obsession-checklist.md.

type BacklogItemFilter added in v1.35.0

type BacklogItemFilter struct {
	// Statuses restricts results to these statuses. Empty means no restriction.
	Statuses []string
	// Priorities restricts results to these priority values. Empty means no restriction.
	Priorities []int
	// SortBy controls ordering ("priority", "updated_at"). Empty means default ordering.
	SortBy string
	// ExcludeDone, when true and Statuses is empty, excludes items with status
	// "done". Independent of ExcludeArchived — split into two flags (rather
	// than one combined "ExcludeTerminal") so a caller can show done items by
	// default while still hiding archived ones. Renamed from ExcludeTerminal,
	// which used to combine both; verified via grep that ListBacklogItems and
	// ListBacklogItemSummaries were the only two callers of the old field, so
	// the rename is safe (no silent behavior change for any other caller).
	ExcludeDone bool
	// ExcludeArchived, when true and Statuses is empty, excludes items with
	// status "archived". Independent of ExcludeDone — see its doc comment.
	ExcludeArchived bool
	// Limit caps the number of results returned. 0 means use the default safety cap (1000).
	Limit int
	// Offset skips the first N results (for pagination). Only applied when Limit > 0.
	Offset int
	// ChainFired, when non-nil, restricts results to items whose chain_fired
	// column equals *ChainFired. Added so TriggerChainReconciler.ReconcileChains
	// (session/chain_firer.go) can push its "unfired pending chain" filter into
	// SQL instead of scanning every "done" item up to the default 1000-row
	// safety cap and filtering in Go — past 1000 done items, a pending unfired
	// chain outside that window was silently never reconciled (sdd:6-verify
	// finding). Backed by index.Fields("status", "chain_fired")
	// (session/ent/schema/backlog_item.go).
	ChainFired *bool
	// NextWorkflowIDSet, when non-nil, restricts results to items where
	// next_workflow_id IS NOT NULL (true) or IS NULL (false). See ChainFired's
	// doc comment — the two are combined by ReconcileChains's query.
	NextWorkflowIDSet *bool
}

BacklogItemFilter controls which items ListBacklogItems returns.

type BacklogItemPrecondition added in v1.35.0

type BacklogItemPrecondition struct {
	// ExpectedStatus, if non-empty, requires the item's current status to match.
	ExpectedStatus string
	// ExpectedUpdatedAt, if non-zero, requires the item's updated_at to match.
	ExpectedUpdatedAt *time.Time
	// Note, if non-empty, is stored in the status event audit log alongside this
	// transition. Use it to record why the transition happened (e.g. "auto-reopened
	// after FAIL verdict").
	Note string
}

BacklogItemPrecondition is used for optimistic locking on update/transition.

type BacklogItemSummary added in v1.37.0

type BacklogItemSummary struct {
	ID                 string               `json:"id"`
	ExternalID         string               `json:"external_id"`
	ExternalURL        string               `json:"external_url"`
	Labels             []string             `json:"labels"`
	Title              string               `json:"title"`
	Status             BacklogStatus        `json:"status"`
	Priority           int                  `json:"priority"`
	RepoPath           string               `json:"repo_path"`
	AcceptanceCriteria AcCriteriaJSON       `json:"acceptance_criteria"`
	Notes              string               `json:"notes"`
	PrURL              string               `json:"pr_url"`
	PrNumber           int                  `json:"pr_number"`
	CreatedAt          time.Time            `json:"created_at"`
	UpdatedAt          time.Time            `json:"updated_at"`
	ArchivedAt         *time.Time           `json:"archived_at"`
	ItemSessions       []ItemSessionSummary `json:"-"`
}

BacklogItemSummary is a lightweight projection of BacklogItemData for list views. It omits large text fields (Description, plan artifacts) and status-event history, but eagerly includes ItemSessions (with ReviewVerdict) for cost/status display.

type BacklogItemTransitionInput added in v1.35.0

type BacklogItemTransitionInput = domain.BacklogItemTransitionInput

BacklogItemTransitionInput carries the fields needed by TransitionGuard. Type alias — session.BacklogItemTransitionInput and domain.BacklogItemTransitionInput are identical types.

type BacklogItemUpdate added in v1.35.0

type BacklogItemUpdate struct {
	Title              *string
	Description        *string
	AcceptanceCriteria *AcCriteriaJSON
	Priority           *int
	RepoPath           *string
	SkipReviewGate     *bool
	SkipPlanning       *bool
	AutoSpawnSession   *bool
	AutoCreatePR       *bool
	// PipelineMode is a pointer for partial-update presence: nil means "leave
	// the item's stored pipeline_mode untouched", while a non-nil pointer
	// (including one pointing at "") explicitly sets/resets it. See
	// BacklogItemData.PipelineMode for the field's semantics.
	PipelineMode *string
	// Category is a pointer for partial-update presence: nil means "leave the
	// item's stored category untouched", while a non-nil pointer (including
	// one pointing at "") explicitly sets/clears it. See
	// BacklogItemData.Category for the field's semantics.
	Category *string
	Notes    *string
	// ExternalURL and Labels follow the same partial-update-presence
	// convention as the other pointer fields on this struct: nil means "leave
	// untouched", a non-nil pointer (including one pointing at "" / an empty
	// slice) explicitly sets it.
	ExternalURL       *string
	Labels            *[]string
	PlanApproved      *bool
	PlanApprovedAt    *time.Time
	PlanArtifactsPath *string
	// PlanRejectionReason and PlanRejectedAt follow the same partial-update-
	// presence convention: nil means "leave untouched", a non-nil pointer
	// explicitly sets it. Since a plain pointer can't distinguish "leave
	// untouched" from "clear it back to nil", use ClearPlanRejectedAt to
	// explicitly clear the timestamp back to nil (e.g. alongside resetting
	// PlanRejectionReason back to "" on approval/re-triage) — see
	// PrFeedbackAddressedAt/ClearPrFeedbackAddressedAt below for the same
	// pattern.
	PlanRejectionReason *string
	PlanRejectedAt      *time.Time
	ClearPlanRejectedAt bool
	// QueuedAt and QueuedAutonomous follow the same partial-update-presence
	// convention as PlanApprovedAt: nil means "leave untouched".
	QueuedAt         *time.Time
	QueuedAutonomous *bool
	PrURL            *string
	PrNumber         *int
	// ShippedCheckConclusion, ShippedApprovedCount, ShippedChangesReqCount,
	// ShippedSnapshotAt, ShippedFileStats, and ShippedSnapshotCaptureFailed
	// are pointers for partial-update presence, following the existing
	// convention: nil means "leave the item's stored value untouched", a
	// non-nil pointer explicitly sets it. See BacklogItemData's fields of
	// the same name for semantics.
	ShippedCheckConclusion       *string
	ShippedApprovedCount         *int
	ShippedChangesReqCount       *int
	ShippedSnapshotAt            *time.Time
	ShippedFileStats             *string
	ShippedSnapshotCaptureFailed *bool
	// PrFeedbackAddressedAt follows the same partial-update-presence
	// convention: nil means "leave untouched", a non-nil pointer sets the
	// comment-feedback dedup watermark. Since a plain pointer can't
	// distinguish "leave untouched" from "clear it back to nil", use
	// ClearPrFeedbackAddressedAt to explicitly clear it (e.g. when a PR
	// closes without merging and a fresh PR should start with a clean
	// watermark).
	PrFeedbackAddressedAt      *time.Time
	ClearPrFeedbackAddressedAt bool
	// GitHubSyncedIssueUpdatedAt follows the same partial-update-presence
	// convention as PrFeedbackAddressedAt: nil means "leave untouched", a
	// non-nil pointer sets the loop-prevention watermark. Use
	// ClearGitHubSyncedIssueUpdatedAt to explicitly clear it back to nil.
	GitHubSyncedIssueUpdatedAt      *time.Time
	ClearGitHubSyncedIssueUpdatedAt bool
	// ReworkCapOverride follows the same single-pointer presence convention as
	// the fields above: nil means "leave untouched". A non-nil pointer sets the
	// item's override (0 = unlimited, >0 = this item's own cap). There is
	// currently no way to explicitly clear an override back to "use the global
	// default" via this struct — a deliberate simplification; add a
	// ClearReworkCapOverride bool alongside this if that's needed later.
	ReworkCapOverride *int
	// UserModifiedFields follows the same partial-update-presence convention:
	// nil means "leave untouched", a non-nil pointer sets the stored
	// JSON-encoded set of user-modified field names (e.g. `["title"]`). Build
	// the value with MergeUserModifiedFields rather than hand-encoding JSON.
	UserModifiedFields *string
	// NextWorkflowID/ClearNextWorkflowID follow the same nillable-clear
	// convention as GitHubSyncedIssueUpdatedAt: nil+false means "leave
	// untouched", ClearNextWorkflowID=true explicitly clears the chain
	// configuration back to nil, otherwise a non-nil pointer sets it
	// (webhook-triggers FR10/AC5 — see BacklogItemData.NextWorkflowID).
	NextWorkflowID      *uuid.UUID
	ClearNextWorkflowID bool
	// ChainFired is a normal presence pointer (no clear semantics needed — it
	// only ever moves false->true, by ChainFirer/TriggerChainReconciler).
	ChainFired *bool
	// ChainedAt/ClearChainedAt follow the same nillable-clear convention as
	// NextWorkflowID above.
	ChainedAt      *time.Time
	ClearChainedAt bool
	// TriggeredByChainDepth is a normal presence pointer — non-nillable in the
	// schema (Default 0), so no clear semantics are needed.
	TriggeredByChainDepth *int
}

BacklogItemUpdate carries the mutable fields for UpdateBacklogItem.

type BacklogLifecycleListener added in v1.35.0

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

BacklogLifecycleListener drives backlog item state transitions in response to session lifecycle events. It must be registered via Instance.RegisterLifecycleListener.

OnLifecycleEvent is non-blocking; all DB work is dispatched to a goroutine. Call SetEnabled(false) to make all callbacks no-ops without unwiring.

func NewBacklogLifecycleListener added in v1.35.0

func NewBacklogLifecycleListener(storage *Storage) *BacklogLifecycleListener

NewBacklogLifecycleListener creates a listener backed by the given storage. The review gate is disabled (sessionCreator=nil, headlessPool=nil). No PipelineEngine is wired (nil) — callers needing one should use NewBacklogLifecycleListenerWithPool.

func NewBacklogLifecycleListenerWithPool added in v1.35.0

func NewBacklogLifecycleListenerWithPool(storage *Storage, pool *headless.Pool, pipelineEngine PipelineEngine) *BacklogLifecycleListener

NewBacklogLifecycleListenerWithPool creates a listener that uses a headless.Pool for review gate calls instead of spawning a tmux session. pipelineEngine is the shared PipelineEngine instance (Epic 1.5, Story 1.5.1) — pass nil to fall back to the built-in default pipeline for every item.

func NewBacklogLifecycleListenerWithSpawner added in v1.35.0

func NewBacklogLifecycleListenerWithSpawner(storage *Storage, spawner ReviewGateSpawner) *BacklogLifecycleListener

NewBacklogLifecycleListenerWithSpawner creates a listener that will spawn a review gate session when a work session exits and SkipReviewGate is false.

func (*BacklogLifecycleListener) BackfillStuckStates added in v1.38.0

func (l *BacklogLifecycleListener) BackfillStuckStates(ctx context.Context)

BackfillStuckStates seeds durable BacklogStuckState rows for items that are already stuck at startup, with notified_at pre-set, so the first genuine reconcile tick after a restart/deploy does not re-notify for conditions that were already known (and already surfaced) before the restart. Intended to be called once, before the reconcile ticker goroutine starts. Idempotent via MarkStuck's (item_id, reason) unique-constraint upsert — safe to call on every startup, not just the first one. Best-effort throughout: query/write failures are logged, never returned — backfill must never block startup.

Scope note: only the two DB-derivable reasons that already have a queryable detection surface as of this Epic are seeded — abandoned_review (via the existing FindStuckReviewItems query) and stale_work (mirroring reconcileStaleWorkSessions' maxWorkSessionStaleness check, without modifying that function). rework_cap, bouncing, and push_failed are deliberately NOT seeded here: their detection logic is introduced by Phase 2 (Stories 2.1.2, 2.1.4, 2.1.6 respectively) and does not exist yet in this Epic — seeding them now would mean fabricating that not-yet-built detection logic ahead of schedule. Once Phase 2 ships those detectors, their own MarkStuck/MarkStuckNotified notify-once dedup naturally covers the "first tick after shipping" storm-suppression case for those three reasons at that time, the same way this backfill does for the two reasons seeded today.

pr_ready_unmerged is excluded for a different reason: detecting it needs a GetPRStatus/IsPRMerged GitHub call per pr_pending item, which would burst the GitHub API on every one of the 15+ daily boots. The first genuine tick after startup surfaces it via its own notified_at IS NULL + 30-min gate — a one-tick delay, not a startup API burst.

func (*BacklogLifecycleListener) PipelineEngine added in v1.38.0

func (l *BacklogLifecycleListener) PipelineEngine() PipelineEngine

PipelineEngine returns the PipelineEngine injected at construction (nil if none was wired). Exported for the pointer-equality integration test proving BacklogService and BacklogLifecycleListener share a single PipelineEngine instance (Story 1.5.1).

func (*BacklogLifecycleListener) ReconcilePRPending added in v1.37.0

func (l *BacklogLifecycleListener) ReconcilePRPending(ctx context.Context, er *EntRepository)

ReconcilePRPending polls items in pr_pending status. It transitions to done when the PR is merged, and spawns a fix session when CI fails or reviewers request changes.

Pre-existing complexity relocated verbatim by the backlog_lifecycle.go split (session/backlog_lifecycle_pr.go); not introduced by that split. Splitting this function into drift/merge-detection/pr_ready_unmerged sub-steps is a separate follow-up (see the architecture-review that preceded the split), not part of moving it to its own file.

func (*BacklogLifecycleListener) ReconcileStuck added in v1.35.0

func (l *BacklogLifecycleListener) ReconcileStuck(ctx context.Context)

ReconcileStuck calls ReconcileStuckItems and logs the result. Intended to be called on a periodic ticker as a safety net for abnormal session exits. No-op when the listener is disabled.

func (*BacklogLifecycleListener) RecordPRCreatedOutOfBand added in v1.39.0

func (l *BacklogLifecycleListener) RecordPRCreatedOutOfBand(ctx context.Context, workSessionUUID, prURL string, prNumber int)

RecordPRCreatedOutOfBand records a PR that was created for workSessionUUID through a path other than pushAndCreatePR and transitions the linked backlog item straight to pr_pending via the shared resolveToPRPending tail. (Named "Record", not "Notify", to avoid confusion with l.notify — the user-facing toast helper used elsewhere in this file; this method mutates backlog-item state, it doesn't just surface a message.)

Why this exists: pushAndCreatePR is the *only* place that ever writes pr_pending, but it is reached exclusively via the automated handleReviewSessionExited(PASS) → pushAndCreatePR call chain. The Review Queue's manual "Create PR" button (web-app/src/components/sessions/ ReviewQueuePanel.tsx) drives a completely separate path — SessionService.RunOneShot (server/services/session_service.go) — that runs an ad hoc `claude -p <prompt>` in the worktree and only ever persists the resulting PR URL onto the *session* record (inst.SetGitHubPR). It has no knowledge of backlog items at all, so a backlog-linked item whose PR was created this way never left "review" — ReconcilePRPending's FindPRPendingItems query structurally cannot find it, since it only looks at items already in pr_pending. Left in "review", the item instead accumulates in_progress↔review bounce churn from unrelated reconciliation and eventually reports stuck-reason BOUNCING instead of the correct pr_ready_unmerged. This is the root cause traced in docs/tasks/backlog-feature-improvement.md's "second, compounding root cause" note for PR #157.

No-op if the listener is disabled, the caller has no PR info, the session isn't backlog-linked, or the item isn't currently "review" (avoids clobbering any other in-flight transition). That guard narrows, but does not eliminate, a race with a concurrent pushAndCreatePR call on the same item: TransitionBacklogItemStatus's precondition check is a read-then-write (Get, check in memory, then Save) rather than a true atomic compare-and- swap, so both calls can observe "review" and both succeed. That's harmless here — both write the same target status and equivalent PR fields — but it means two BacklogStatusEvent audit rows can be written instead of one, not that exactly one call is guaranteed to win.

Known limitation (not fixed here — see PR description): unlike pushAndCreatePR, this does not attempt EnablePRAutoMerge, since it has no worktree/git handle to call it with; a PR created via this path currently requires a manual merge. extractPRURL's freeform-text parsing also means prURL/prNumber are not independently verified against GitHub before being persisted — acceptable for this single-operator tool's threat model, but worth knowing if RunOneShot's trust boundary ever changes.

func (*BacklogLifecycleListener) SetAutoReopener added in v1.37.0

func (l *BacklogLifecycleListener) SetAutoReopener(r AutoReopenSpawner)

SetAutoReopener wires in the spawner used to automatically reopen items for rework when a review verdict is FAIL or PARTIAL.

func (*BacklogLifecycleListener) SetBranchReconciler added in v1.39.0

func (l *BacklogLifecycleListener) SetBranchReconciler(f func(worktreePath, branchName string) (*git.MergeMainResult, error))

SetBranchReconciler overrides the function used to fetch+merge a branch's remote ref into its worktree for push_failed remediation (attemptPushRemediation). Overridable in tests to avoid needing a real git repo on disk; production code never needs to call this, since newListenerBase installs git.MergeMainIntoWorktree.

func (*BacklogLifecycleListener) SetChainReconciler added in v1.43.0

func (l *BacklogLifecycleListener) SetChainReconciler(r *TriggerChainReconciler)

SetChainReconciler wires in the pipeline-chain restart-recovery reconciler (webhook-triggers Phase 6). Called via server/dependencies.go once a ChainFirer has been constructed (see Storage.WireChainFirer).

func (*BacklogLifecycleListener) SetDequeuer added in v1.41.0

func (l *BacklogLifecycleListener) SetDequeuer(d QueueDequeuer)

SetDequeuer wires in the spawner used to dequeue queued backlog items once a WIP slot frees up.

func (*BacklogLifecycleListener) SetEnabled added in v1.35.0

func (l *BacklogLifecycleListener) SetEnabled(v bool)

SetEnabled toggles whether this listener processes lifecycle events. Safe to call concurrently.

func (*BacklogLifecycleListener) SetHeadlessPool added in v1.35.0

func (l *BacklogLifecycleListener) SetHeadlessPool(p *headless.Pool)

SetHeadlessPool wires in the headless LLM pool after construction. Calling this enables the headless review gate path even when the listener was created via NewBacklogLifecycleListenerWithSpawner.

func (*BacklogLifecycleListener) SetNotifier added in v1.37.0

func (l *BacklogLifecycleListener) SetNotifier(n Notifier)

SetNotifier wires in the notifier used to publish operator-facing notifications (PR creation failures, security blocks, stale work sessions, rework-cap hits). Optional — nil means notifications are disabled.

func (*BacklogLifecycleListener) SetOneShotShipRunner added in v1.39.0

func (l *BacklogLifecycleListener) SetOneShotShipRunner(r OneShotShipRunner)

SetOneShotShipRunner wires in the runner used by shipViaAgentOrFallback to attempt an agent-driven PR ship (see agentShipPrompt) before falling back to the mechanical pushAndCreatePR path. Optional — nil means every PASS verdict with an ended work session goes straight to the mechanical path, matching this fix's pre-existing behavior.

func (*BacklogLifecycleListener) SetOrphanedPRFinder added in v1.41.0

func (l *BacklogLifecycleListener) SetOrphanedPRFinder(f func(ctx context.Context, repoPath, branch string) (*github.PRInfo, error))

SetOrphanedPRFinder overrides the function used to look up an existing PR for a repo path's branch, used by reconcileOrphanedAgentPRs (Epic 3.2). Overridable in tests to avoid real GitHub API calls or needing a real git remote on disk; production code never needs to call this, since newListenerBase installs defaultOrphanedPRFinder.

func (*BacklogLifecycleListener) SetPRByNumberFinder added in v1.41.0

func (l *BacklogLifecycleListener) SetPRByNumberFinder(f func(ctx context.Context, repoPath string, prNumber int) (*github.PRInfo, error))

SetPRByNumberFinder overrides the function used to look up a PR by its immutable number, used by verifyPRHeadBranchMatchesTracked (Story 6). Overridable in tests to avoid real GitHub API calls or needing a real git remote on disk; production code never needs to call this, since newListenerBase installs defaultPRByNumberFinder.

func (*BacklogLifecycleListener) SetPRCreatorFactory added in v1.37.0

func (l *BacklogLifecycleListener) SetPRCreatorFactory(f func(repoPath, worktreePath, sessionName, branchName, baseCommitSHA string) prCreator)

SetPRCreatorFactory overrides the factory used to construct the push/PR-creation client for pushAndCreatePR. Overridable in tests; production code never needs to call this, since newListenerBase installs defaultPRCreatorFactory.

func (*BacklogLifecycleListener) SetPRFixSpawner added in v1.37.0

func (l *BacklogLifecycleListener) SetPRFixSpawner(s PRFixSpawner)

SetPRFixSpawner wires in the spawner used to automatically reopen pr_pending items for rework when CI checks fail or reviewers request changes.

func (*BacklogLifecycleListener) SetPRPendingCheckerFactory added in v1.37.0

func (l *BacklogLifecycleListener) SetPRPendingCheckerFactory(f func(repoPath string) prPendingChecker)

SetPRPendingCheckerFactory overrides the factory used to construct the PR-status checker for ReconcilePRPending. Overridable in tests (mirrors the timeNow seam in instance_workspace.go:581); production code never needs to call this, since newListenerBase installs defaultPRPendingCheckerFactory.

func (*BacklogLifecycleListener) SetReviewRespawner added in v1.39.0

func (l *BacklogLifecycleListener) SetReviewRespawner(r ReviewRespawner)

SetReviewRespawner wires in the spawner used to automatically re-trigger the review gate for items abandoned in review with no active session.

func (*BacklogLifecycleListener) SetReworkBlockStaleResolver added in v1.41.0

func (l *BacklogLifecycleListener) SetReworkBlockStaleResolver(r ReworkBlockStaleResolver)

SetReworkBlockStaleResolver wires in the resolver used to re-check and clear an open StuckReasonReworkBlockedStale row once its blocking work session recovers, ends, or the item leaves review — same pattern as AutoReopenSpawner/PRFixSpawner/SetStaleWorkRemediator.

func (*BacklogLifecycleListener) SetSessionArchiver added in v1.39.0

func (l *BacklogLifecycleListener) SetSessionArchiver(a SessionArchiver)

SetSessionArchiver wires in the archiver used to soft-archive backlog work sessions belonging to done/archived items that the transition hook missed (see the archive_terminal_sessions detector in ReconcileStuck). Optional — nil means the detector no-ops.

func (*BacklogLifecycleListener) SetSessionCreator added in v1.38.0

func (l *BacklogLifecycleListener) SetSessionCreator(s ReviewGateSpawner)

SetSessionCreator wires in the spawner used to create review-gate sessions after construction. Needed because production wiring (server/dependencies.go) constructs this listener before SessionService exists.

func (*BacklogLifecycleListener) SetSessionLivenessChecker added in v1.38.0

func (l *BacklogLifecycleListener) SetSessionLivenessChecker(f func(sessionUUID string) bool)

SetSessionLivenessChecker wires the function used by the zombie-session review detector (pre-mortem F3) to confirm whether a session's underlying tmux/CLI process is actually still alive, rather than trusting the DB's EndedAt IS NULL row alone. Optional — nil means the zombie detector never flags (conservative: unknown liveness is treated as "assume alive").

func (*BacklogLifecycleListener) SetStaleWorkRemediator added in v1.39.0

func (l *BacklogLifecycleListener) SetStaleWorkRemediator(r StaleWorkRemediator)

SetStaleWorkRemediator wires in the remediator used to clean up and respawn stale (but not zombie) work sessions — the "stale_work" reason's automated remediation action.

func (*BacklogLifecycleListener) SetTriageRespawner added in v1.41.0

func (l *BacklogLifecycleListener) SetTriageRespawner(r TriageRespawner)

SetTriageRespawner wires in the spawner used to automatically re-trigger triage for idea-status items whose triage session orphaned.

func (*BacklogLifecycleListener) Shutdown added in v1.35.0

func (l *BacklogLifecycleListener) Shutdown()

Shutdown cancels in-flight review gate calls. Safe to call concurrently.

func (*BacklogLifecycleListener) TriggerReviewForSession added in v1.37.0

func (l *BacklogLifecycleListener) TriggerReviewForSession(workSessionUUID string)

TriggerReviewForSession immediately spawns a review gate for the work session identified by workSessionUUID. Used by the autonomous driver to trigger review as soon as the driver signals DONE, rather than waiting for ReconcileStuck. No-op if the listener is disabled or no review mechanism is configured.

func (*BacklogLifecycleListener) WireToInstance added in v1.35.0

func (l *BacklogLifecycleListener) WireToInstance(inst *Instance)

WireToInstance creates a per-instance listener shim and registers it on inst. Call this for every Instance that should participate in backlog lifecycle tracking.

type BacklogStatus added in v1.35.0

type BacklogStatus = domain.BacklogStatus

BacklogStatus represents the lifecycle state of a backlog item. Type alias — session.BacklogStatus and domain.BacklogStatus are identical types; all existing callers continue to work without any import changes.

type BacklogStatusEventData added in v1.37.0

type BacklogStatusEventData struct {
	ID          string
	FromStatus  string
	ToStatus    string
	TriggeredBy string
	Note        *string
	CreatedAt   time.Time
}

BacklogStatusEventData is the domain DTO replacing *ent.BacklogStatusEvent in Storage returns.

type BookmarkTarget

type BookmarkTarget struct {
	Name       string
	RevisionID string
	IsRemote   bool
}

BookmarkTarget represents a bookmark/branch as a switch target

type CDPStreamManager added in v1.35.0

type CDPStreamManager = cdp.CDPStreamManager

CDPStreamManager is a local alias for the cdp package interface so that files within the session package can reference it without importing cdp directly.

type CachingPipelineEngine added in v1.38.0

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

CachingPipelineEngine is the single concrete implementation of PipelineEngine. PipelineModeDefault resolves for free (no cache/DB touch); any other slug resolves via pipelineModeCache; any unresolvable/malformed slug falls back to the default behavior and emits exactly one PipelineEngine-prefixed Warn log line naming the item and the unresolved slug (never a silent no-op, never a panic) — see Story 1.3.3's fail-closed acceptance criteria.

func NewPipelineEngine added in v1.38.0

func NewPipelineEngine(repo PipelineModeRepository) (*CachingPipelineEngine, error)

NewPipelineEngine constructs a CachingPipelineEngine backed by repo, doing one synchronous cache.Load at construction time.

Unlike NewDefaultWorkflowEngine's zero-arg, infallible, pure in-memory construction, this constructor performs a real DB call and can fail (DB unavailable, migration race, transient connection error). Per plan.md's Risk Control section ("NewPipelineEngine startup-failure behavior"), a cache.Load failure here NEVER aborts construction: it is logged at Warn and NewPipelineEngine returns a valid, usable engine backed by an empty cache. The signature still returns an error for future-proofing (e.g. a future validation error genuinely worth failing construction on), but this Phase 1 implementation never returns a non-nil error for a cache.Load failure specifically — PipelineEngine is purely additive/opt-in, so a transient DB hiccup at boot must never crash the whole server for a feature most items don't use yet.

func (*CachingPipelineEngine) ContentHashFor added in v1.38.0

func (e *CachingPipelineEngine) ContentHashFor(mode PipelineMode) (string, bool)

ContentHashFor implements PipelineEngine.

No Warn log is emitted for an unresolved slug here — documented exemption: this method is only ever called from the Epic 1.6 snapshot-write path immediately after a successful resolution already logged its own outcome via one of the 4 methods above; a caller invoking ContentHashFor for an already-unresolved slug independently of that flow would be a pre-existing bug elsewhere, not a new failure mode worth logging again here.

func (*CachingPipelineEngine) InitialPromptFor added in v1.38.0

func (e *CachingPipelineEngine) InitialPromptFor(item *BacklogItemData, priorSessions []ItemSessionSummary) string

InitialPromptFor implements PipelineEngine.

func (*CachingPipelineEngine) InteractiveReviewPromptFor added in v1.39.0

func (e *CachingPipelineEngine) InteractiveReviewPromptFor(item *BacklogItemData, acSnapshot []AcCriterion, diff string, diffTruncated bool, itemSessionID string, verificationNotes string) string

InteractiveReviewPromptFor implements PipelineEngine.

Shares ReviewPromptTemplate with ReviewPromptFor rather than introducing a second template field: the PipelineMode schema's review_prompt_template comment ("Prompt template used for review under this pipeline mode") is already style-agnostic, and splitting headless-JSON vs. tool-call variants into two DB fields/UI inputs for one logical "review prompt" concept would be speculative until a real mode author needs genuinely different content for the two paths. Custom-mode authors who want their template to drive the real review gate (this method) must include submit_review_verdict call instructions themselves — see the interface doc comment.

func (*CachingPipelineEngine) InvalidateCache added in v1.38.0

func (e *CachingPipelineEngine) InvalidateCache(ctx context.Context) error

InvalidateCache re-fetches enabled pipeline modes from the repository and swaps the cache wholesale. Exported for the RPC write handlers (Epic 2.2) that must invalidate the cache after every Create/Update/Delete/Enable/ Disable of a PipelineMode. Not yet called by any production code path in this epic — added now because it is cheap and Epic 2.2 needs it.

func (*CachingPipelineEngine) ReviewPromptFor added in v1.38.0

func (e *CachingPipelineEngine) ReviewPromptFor(item *BacklogItemData, acSnapshot []AcCriterion, diff string, diffTruncated bool, verificationNotes string, extras ReviewContextExtras) string

ReviewPromptFor implements PipelineEngine.

Deviation from plan.md's Story 1.3.1 interface text: an extras ReviewContextExtras parameter was added. BuildHeadlessReviewPrompt requires it (PriorSessions/ProgressNotes/ItemDescription), and the real call sites this engine replaces (session/review_gate.go, backlog_service_triage.go's TriggerReReview) always populate it — dropping it silently would have been a real behavior regression on the default path, not just an interface nicety. Epic 1.5's call sites should pass their real extras value here.

func (*CachingPipelineEngine) SlashCommandSet added in v1.38.0

func (e *CachingPipelineEngine) SlashCommandSet(item *BacklogItemData) (map[string]string, error)

SlashCommandSet implements PipelineEngine.

func (*CachingPipelineEngine) TriagePromptFor added in v1.38.0

func (e *CachingPipelineEngine) TriagePromptFor(item *BacklogItemData, artifactAbsPath string) string

TriagePromptFor implements PipelineEngine.

type CallbackDispatcher added in v1.43.0

type CallbackDispatcher interface {
	Dispatch(eventType string, payload any)
}

CallbackDispatcher fires an outbound HTTP callback for a named lifecycle event (e.g. "session_complete", "session_stale", "queue_item_created" — see server/services.CallbackDispatcher's Dispatch doc comment for the full event-type list and payload shapes). Implemented outside this package (server/services.CallbackDispatcher) since this package cannot import server/services — server/services imports session, so the reverse import would be a cycle. Mirrors the ItemChangePublisher cross-package adapter pattern (see ItemChangePublisher, backlog_item_change.go).

Dispatch must never block the caller (FR8) and must never panic into the caller — implementations bound concurrent in-flight dispatch goroutines and drop+log beyond that cap (AC10) rather than queuing unboundedly.

type CancelPendingKillParams added in v1.42.0

type CancelPendingKillParams struct {
	Storage   InstanceStore
	Suspended *SuspendedProcessStore

	InstanceID  string
	OriginalPID int32
}

CancelPendingKillParams holds everything needed to abandon an in-progress import: delete the committed Instance, then resume the original process.

type CanonicalBlock added in v1.35.0

type CanonicalBlock struct {
	Kind              CanonicalBlockKind `json:"kind"`
	Text              string             `json:"text,omitempty"`
	ToolID            string             `json:"tool_id,omitempty"`
	ToolName          string             `json:"tool_name,omitempty"`
	ToolArgs          json.RawMessage    `json:"tool_args,omitempty"`
	ToolResultID      string             `json:"tool_result_id,omitempty"`
	ToolResultContent string             `json:"tool_result_content,omitempty"`
	ToolResultIsError bool               `json:"tool_result_is_error,omitempty"`
}

func NewTextBlock added in v1.35.0

func NewTextBlock(text string) CanonicalBlock

NewTextBlock constructs a valid CanonicalBlock of text kind.

func NewThinkingBlock added in v1.35.0

func NewThinkingBlock(text string) CanonicalBlock

NewThinkingBlock constructs a valid CanonicalBlock of thinking kind.

func NewToolResultBlock added in v1.35.0

func NewToolResultBlock(id, name, content string, isError bool) CanonicalBlock

NewToolResultBlock constructs a valid CanonicalBlock of tool_result kind.

func NewToolUseBlock added in v1.35.0

func NewToolUseBlock(id, name string, args json.RawMessage) CanonicalBlock

NewToolUseBlock constructs a valid CanonicalBlock of tool_use kind.

func (CanonicalBlock) Validate added in v1.35.0

func (b CanonicalBlock) Validate() error

Validate checks if the block is in a valid state.

type CanonicalBlockKind added in v1.35.0

type CanonicalBlockKind string
const (
	BlockKindText       CanonicalBlockKind = "text"
	BlockKindThinking   CanonicalBlockKind = "thinking"
	BlockKindToolUse    CanonicalBlockKind = "tool_use"
	BlockKindToolResult CanonicalBlockKind = "tool_result"
	BlockKindImage      CanonicalBlockKind = "image"
)

type CanonicalRole added in v1.35.0

type CanonicalRole string
const (
	RoleUser      CanonicalRole = "user"
	RoleAssistant CanonicalRole = "assistant"
)

type CanonicalTurn added in v1.35.0

type CanonicalTurn struct {
	Role      CanonicalRole    `json:"role"`
	Blocks    []CanonicalBlock `json:"blocks"`
	Timestamp time.Time        `json:"timestamp"`
	TurnIndex int              `json:"turn_index"`
	Model     string           `json:"model,omitempty"`
}

func ReadCanonicalTurnsFromFile added in v1.42.0

func ReadCanonicalTurnsFromFile(path string) ([]CanonicalTurn, error)

ReadCanonicalTurnsFromFile parses a Claude JSONL transcript at the given path into CanonicalTurns, independent of any live Instance. It is the shared parsing core used both by ClaudeAdapter.Import (which resolves a path from an Instance's working directory + conversation UUID first) and by the import-external-session preview path (Story 1.1.4), which reads a resolved history file's turns without ever constructing a half-built Instance just to read history.

Trailing partial lines (e.g. a JSONL file caught mid-write by a live writer) are tolerated: a line that fails to unmarshal as a well-formed turn is simply skipped rather than treated as an error, per the pitfalls research's "tolerate trailing partial lines" requirement.

func (CanonicalTurn) Validate added in v1.35.0

func (t CanonicalTurn) Validate() error

Validate checks if the turn and all of its blocks are in a valid state.

type ChainFirer added in v1.43.0

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

ChainFirer fires the next workflow in a pipeline chain once a BacklogItem reaches BacklogStatusDone with NextWorkflowID set (webhook-triggers FR10/ AC5). Both the happy-path async dispatch (EntRepository.dispatchChainFire, called immediately after TransitionBacklogItemStatus's own DB write returns — AC9) and TriggerChainReconciler's restart-recovery sweep call the same Fire method, so "fire exactly once per item" logic lives in exactly one place.

Concrete type, not an interface — repo/workflows/fireEvents are this package's own types (EntRepository, WorkflowRepository, TriggerFireEventRepository), referenced directly per .claude/rules/interface-pollution-checklist.md; only firer crosses a real package boundary and needs one (TriggerFirer, above).

func NewChainFirer added in v1.43.0

func NewChainFirer(repo *EntRepository, workflows WorkflowRepository, fireEvents TriggerFireEventRepository, firer TriggerFirer, cfg *config.Config) *ChainFirer

NewChainFirer constructs a ChainFirer. repo is the exact *EntRepository instance the rest of the backlog write path uses (so ChainFirer's own UpdateBacklogItem calls share the same callbackDispatcher/ itemChangePublisher wiring as every other backlog mutation) — callers should obtain it via Storage.WireChainFirer rather than constructing a second EntRepository around the same ent.Client.

func (*ChainFirer) Dispatch added in v1.43.0

func (c *ChainFirer) Dispatch(item *BacklogItemData)

Dispatch reserves a semaphore slot and fires item's chain in a new goroutine — same non-blocking shape as server/services/callback_dispatcher.go's CallbackDispatcher.Dispatch (Task 5.2.1a): a non-blocking select on a semaphore-sized channel either reserves a slot immediately or drops the dispatch and logs a warning. A dropped dispatch loses no work — ChainFired stays false, so TriggerChainReconciler retries the item on its next 60s tick. Never blocks the caller. No-op when the webhook_triggers feature flag is off (Task 8.2.1b — defense in depth beyond route-registration gating).

Takes no ctx parameter — matching CallbackDispatcher.Dispatch's identical signature choice — because the spawned goroutine always derives its own context.WithTimeout(context.Background(), chainFireTimeout) below rather than propagating a caller's ctx: by the time that goroutine's FireTriggerChained (CreateSession) call actually runs, a caller-supplied ctx (e.g. an RPC handler's request-scoped context, or dispatchChainFire's deliberately-Background() context) may already be cancelled or long gone — see dispatchChainFire's doc comment (session/ent_repository_backlog.go) for the same rationale applied one layer up.

func (*ChainFirer) Fire added in v1.43.0

func (c *ChainFirer) Fire(ctx context.Context, item *BacklogItemData) (fired bool, err error)

Fire attempts the chain-fire for item, which must already have NextWorkflowID set (callers filter on ChainFired == false before calling — Fire itself re-checks both as a defense-in-depth no-op guard). Returns fired=true only when a new session was actually created.

Double-fire race (webhook-triggers Task 6.2.1d): EntRepository's happy-path dispatch (right after TransitionBacklogItemStatus's done write) and TriggerChainReconciler's periodic sweep can both observe the same item with ChainFired==false and call Fire concurrently — a genuine risk once a chain sits pending for even one 60s tick. Fire closes this the same way this package already closed an identically-shaped TOCTOU race in TransitionBacklogItemStatus itself (see that function's doc comment, BUG-026): claimIfUnfired below performs a genuine SQL-level compare-and- swap (ChainFired=true, WHERE updated_at = <the value item was read at>) *before* calling TriggerFirer.FireTriggerChained, not after. Exactly one concurrent caller's claim UPDATE affects a row; every other caller's claim affects zero rows (ErrPreconditionFailed) and backs off without ever reaching FireTriggerChained — so at most one goroutine can ever call CreateSession for a given item's chain.

type Checkpoint

type Checkpoint struct {
	ID             string `json:"id"`
	SessionID      string `json:"session_id"`
	ParentID       string `json:"parent_id,omitempty"`
	Label          string `json:"label"`
	ScrollbackSeq  uint64 `json:"scrollback_seq"`
	ScrollbackPath string `json:"scrollback_path,omitempty"`
	ClaudeConvUUID string `json:"claude_conv_uuid,omitempty"`
	// ConvLineCount is the number of JSONL lines in the Claude conversation file at
	// checkpoint time. Used by ForkClaudeConversation to truncate the fork correctly.
	ConvLineCount uint64    `json:"conv_line_count,omitempty"`
	GitCommitSHA  string    `json:"git_commit_sha,omitempty"`
	Timestamp     time.Time `json:"timestamp"`

	// New: CLI-agnostic checkpoint details.
	CanonicalTurnIndex int    `json:"canonical_turn_index,omitempty"`
	CanonicalPath      string `json:"canonical_path,omitempty"`
}

Checkpoint represents a named bookmark of a session's state at a point in time. It captures the scrollback position, git SHA, and conversation UUID so that the session can later be forked or restored from this exact state.

type CheckpointList

type CheckpointList []Checkpoint

CheckpointList is a slice of Checkpoints with helper methods.

func (CheckpointList) FindByID

func (cl CheckpointList) FindByID(id string) *Checkpoint

FindByID returns the Checkpoint with the given ID, or nil if not found.

func (CheckpointList) FindByLabel

func (cl CheckpointList) FindByLabel(label string) *Checkpoint

FindByLabel returns the first Checkpoint with the given label, or nil if not found.

func (CheckpointList) Latest

func (cl CheckpointList) Latest() *Checkpoint

Latest returns the Checkpoint with the most recent Timestamp, or nil if empty.

type CircularBuffer

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

CircularBuffer is a thread-safe circular buffer with automatic disk fallback when the in-memory buffer fills up. This prevents memory overflow while maintaining a history of PTY output for status detection and debugging.

func NewCircularBuffer

func NewCircularBuffer(size int) *CircularBuffer

NewCircularBuffer creates a new circular buffer with the specified size in bytes. When the buffer fills up, old data is automatically overwritten (circular behavior).

func (*CircularBuffer) Cap

func (cb *CircularBuffer) Cap() int

Cap returns the total capacity of the buffer.

func (*CircularBuffer) Clear

func (cb *CircularBuffer) Clear()

Clear resets the buffer to empty state.

func (*CircularBuffer) Close

func (cb *CircularBuffer) Close() error

Close releases resources used by the circular buffer. If disk fallback is enabled, it removes the disk file.

func (*CircularBuffer) DisableDiskFallback

func (cb *CircularBuffer) DisableDiskFallback() error

DisableDiskFallback disables disk fallback and removes the disk file.

func (*CircularBuffer) EnableDiskFallback

func (cb *CircularBuffer) EnableDiskFallback(diskPath string) error

EnableDiskFallback enables automatic disk fallback when buffer is full. The diskPath parameter specifies where to store overflow data. This feature is currently a placeholder for future implementation.

func (*CircularBuffer) GetAll

func (cb *CircularBuffer) GetAll() []byte

GetAll returns all data currently in the buffer. Returns a copy to prevent concurrent modification issues.

func (*CircularBuffer) GetRecent

func (cb *CircularBuffer) GetRecent(n int) []byte

GetRecent returns the last n bytes from the buffer. If n is larger than the buffer size or the available data, returns all available data.

func (*CircularBuffer) GetRecentHash added in v1.37.0

func (cb *CircularBuffer) GetRecentHash(n int) (uint64, bool)

GetRecentHash returns the murmur3-64 hash of the last n bytes without allocating a copy. Returns (0, false) when the buffer has no data. In the common case (contiguous tail segment), this is allocation-free. Only the rare wrapped case allocates via murmur3.New64().

func (*CircularBuffer) GetRecentInto added in v1.37.0

func (cb *CircularBuffer) GetRecentInto(dst []byte, n int) int

GetRecentInto copies the last n bytes into dst and returns the number of bytes written. dst must have length >= n. Returns 0 when the buffer is empty. Prefer over GetRecent when the caller can provide a pooled buffer.

func (*CircularBuffer) Len

func (cb *CircularBuffer) Len() int

Len returns the number of bytes currently in the buffer.

func (*CircularBuffer) TotalBytesWritten added in v1.35.0

func (cb *CircularBuffer) TotalBytesWritten() int64

TotalBytesWritten returns the total bytes ever written to this buffer (monotonically increasing).

func (*CircularBuffer) Write

func (cb *CircularBuffer) Write(data []byte) (int, error)

Write appends data to the circular buffer. If the buffer is full, the oldest data is overwritten. This is an O(1) operation.

func (*CircularBuffer) WriteTo

func (cb *CircularBuffer) WriteTo(w io.Writer) (int64, error)

WriteTo implements io.WriterTo interface for efficient streaming.

type ClaudeAdapter added in v1.35.0

type ClaudeAdapter struct{}

func NewClaudeAdapter added in v1.35.0

func NewClaudeAdapter() *ClaudeAdapter

func (*ClaudeAdapter) CanHandle added in v1.35.0

func (a *ClaudeAdapter) CanHandle(program string) bool

func (*ClaudeAdapter) Export added in v1.35.0

func (a *ClaudeAdapter) Export(ctx context.Context, turns []CanonicalTurn, inst *Instance) error

func (*ClaudeAdapter) Import added in v1.35.0

func (a *ClaudeAdapter) Import(ctx context.Context, inst *Instance) ([]CanonicalTurn, error)

func (*ClaudeAdapter) Name added in v1.35.0

func (a *ClaudeAdapter) Name() string

type ClaudeCommandBuilder

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

ClaudeCommandBuilder constructs Claude CLI commands with session resumption support. This builder intelligently adds the --resume flag when appropriate to maintain conversation continuity across session restarts.

func NewClaudeCommandBuilder

func NewClaudeCommandBuilder(baseProgram string, claudeSession *ClaudeSessionData) *ClaudeCommandBuilder

NewClaudeCommandBuilder creates a new command builder for constructing Claude CLI commands. Parameters:

  • baseProgram: The base command string (e.g., "claude", "claude --model sonnet", "aider")
  • claudeSession: Optional session data for resumption support (can be nil)

func (*ClaudeCommandBuilder) Build

func (b *ClaudeCommandBuilder) Build() string

Build constructs the final command string with session resumption if applicable. The method follows these rules:

  1. If not a Claude command, returns baseProgram unchanged
  2. If no session data exists, returns baseProgram unchanged
  3. If session ID is invalid UUID, returns baseProgram unchanged with warning
  4. If all conditions met, returns "baseProgram --resume <sessionId>"

type ClaudeController

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

ClaudeController provides a high-level API for controlling Claude instances. It orchestrates all the underlying components (queue, executor, history, streams).

Locking discipline:

  • lifecycle (Locked[controllerLifecycle]): write-locked briefly at the boundary of Start/Stop transitions. Slow cleanup in Stop() runs OUTSIDE this lock so that status reads are never blocked by goroutine joins or disk I/O.
  • Sub-components (atomic.Pointer[T]): set once in Start(), cleared in Stop(). Readers call .Load() — a nil result means not yet initialized. Atomic access means GetCurrentStatus, GetRecentOutput, Subscribe, etc. never contend with Stop().
  • listeners (Locked[[]StatusChangeListener]): fan-out callbacks.
  • cache (Locked[cacheState]): tail-hash result cache for status/idle detection.

Cache-line layout: lifecycle.mu (a sync.RWMutex) and the atomic.Pointer fields are separated by [64]byte padding so that write operations on the mutex do not invalidate the cache line read by atomic.Load() calls (Go issue #67764).

func NewClaudeController

func NewClaudeController(instance InstanceContext) (*ClaudeController, error)

NewClaudeController creates a new controller for the given instance.

func (*ClaudeController) AddStatusChangeListener added in v1.35.0

func (cc *ClaudeController) AddStatusChangeListener(fn StatusChangeListener)

AddStatusChangeListener appends fn to the fan-out set of status-change listeners. All registered listeners fire on every status transition. Safe to call before or after Start().

func (*ClaudeController) CancelCommand

func (cc *ClaudeController) CancelCommand(commandID string) error

CancelCommand cancels a pending command in the queue.

func (*ClaudeController) ClearHistory

func (cc *ClaudeController) ClearHistory() error

ClearHistory removes all command history entries.

func (*ClaudeController) ClearQueue

func (cc *ClaudeController) ClearQueue() error

ClearQueue removes all pending commands from the queue.

func (*ClaudeController) GetCommandHistory

func (cc *ClaudeController) GetCommandHistory(limit int) []*HistoryEntry

GetCommandHistory returns recent command history.

func (*ClaudeController) GetCommandStatus

func (cc *ClaudeController) GetCommandStatus(commandID string) (*Command, error)

GetCommandStatus retrieves the current status of a command.

func (*ClaudeController) GetCurrentCommand

func (cc *ClaudeController) GetCurrentCommand() *Command

GetCurrentCommand returns the currently executing command, if any.

func (*ClaudeController) GetCurrentStatus

func (cc *ClaudeController) GetCurrentStatus() (detection.DetectedStatus, string)

GetCurrentStatus detects the current status of the Claude instance.

Two optimisations are applied on every call:

  1. Tail slicing — only the last statusDetectionTailBytes bytes of the terminal content are examined. Status indicators (◇ Ready, Thinking…, esc to interrupt) always appear near the current cursor position, so scanning the full scrollback is unnecessary.
  2. Content hash cache — a FNV-64a hash of the tail is compared against the previous call. If the tail is unchanged the cached result is returned immediately with zero allocations.

This function holds no lifecycle lock — it reads ptyAccess and statusDetector via atomic.Pointer, and the status/idle caches via atomic.Pointer. It therefore never blocks when Stop() is running its slow cleanup.

func (*ClaudeController) GetEscapeParser added in v1.35.0

func (cc *ClaudeController) GetEscapeParser() *analytics.EscapeCodeParser

GetEscapeParser returns the escape code parser from the response stream. Returns nil if the controller is not started or has no response stream.

func (*ClaudeController) GetExecutionOptions

func (cc *ClaudeController) GetExecutionOptions() ExecutionOptions

GetExecutionOptions returns current execution options.

func (*ClaudeController) GetExitContent added in v1.15.0

func (cc *ClaudeController) GetExitContent() []byte

GetExitContent returns the last bytes captured before the PTY exited. Returns nil if the controller has no response stream or no exit content was recorded.

func (*ClaudeController) GetHistoryStatistics

func (cc *ClaudeController) GetHistoryStatistics() HistoryStatistics

GetHistoryStatistics returns statistics about command execution.

func (*ClaudeController) GetIdleDuration

func (cc *ClaudeController) GetIdleDuration() time.Duration

GetIdleDuration returns how long the session has been idle.

func (*ClaudeController) GetIdleState

func (cc *ClaudeController) GetIdleState() (detection.IdleState, time.Time)

GetIdleState returns the current idle state with timing information. Returns the state and the timestamp of last activity.

Applies the same tail-slice + hash-cache optimisations as GetCurrentStatus so that polling the idle state on an unchanged terminal is essentially free.

Holds no lifecycle lock — reads ptyAccess and idleDetector via atomic.Pointer. This also fixes the re-entrant RWMutex bug that existed when calling cc.instance.Preview() → GetRecentOutput() → cc.mu.RLock() while already holding cc.mu.RLock(); with atomic pointers there is no lock to re-enter.

func (*ClaudeController) GetIdleStateInfo

func (cc *ClaudeController) GetIdleStateInfo() detection.IdleStateInfo

GetIdleStateInfo returns comprehensive idle state information.

func (*ClaudeController) GetInstance

func (cc *ClaudeController) GetInstance() InstanceContext

GetInstance returns the InstanceContext backing this controller.

func (*ClaudeController) GetQueuedCommands

func (cc *ClaudeController) GetQueuedCommands() []*Command

GetQueuedCommands returns all commands currently in the queue.

func (*ClaudeController) GetQueuedCommandsCount added in v1.37.0

func (cc *ClaudeController) GetQueuedCommandsCount() int

GetQueuedCommandsCount returns the number of commands in the queue without allocating a slice. Use this instead of len(GetQueuedCommands()) on hot paths.

func (*ClaudeController) GetRateLimitHandler added in v1.35.0

func (cc *ClaudeController) GetRateLimitHandler() *ratelimit.PTYConsumer

GetRateLimitHandler returns the rate limit PTY consumer (for callback wiring). Returns nil if the controller has not been started yet.

func (*ClaudeController) GetRateLimitResetTime added in v1.35.0

func (cc *ClaudeController) GetRateLimitResetTime() time.Time

GetRateLimitResetTime returns the reset time from the rate limit handler. Returns zero time if no handler is active or no reset time is known.

func (*ClaudeController) GetRateLimitState added in v1.12.0

func (cc *ClaudeController) GetRateLimitState() ratelimit.RateLimitState

GetRateLimitState returns the current rate limit detection state.

func (*ClaudeController) GetRecentOutput

func (cc *ClaudeController) GetRecentOutput(bytes int) []byte

GetRecentOutput returns recent output from the PTY buffer. Holds no lifecycle lock; returns nil if the controller is not started.

func (*ClaudeController) GetSessionName

func (cc *ClaudeController) GetSessionName() string

GetSessionName returns the session name for this controller.

func (*ClaudeController) GetStatusAndIdleInfo added in v1.37.0

func (cc *ClaudeController) GetStatusAndIdleInfo() (detection.DetectedStatus, string, detection.IdleStateInfo, int)

GetStatusAndIdleInfo returns both the detected status and idle state info in one call. Saves one GetRecentHash (murmur3 over 4KB) and one cache.Read on every poll tick compared to calling GetCurrentStatus + GetIdleStateInfo separately.

func (*ClaudeController) GetStatusDetector added in v1.35.0

func (cc *ClaudeController) GetStatusDetector() detection.TerminalDetector

GetStatusDetector returns the status detector used by this controller. Used by GetDetectionEvents RPC to retrieve recent detection events for debugging.

func (*ClaudeController) GetTotalBytesWritten added in v1.35.0

func (cc *ClaudeController) GetTotalBytesWritten() int64

GetTotalBytesWritten returns the monotonic PTY byte offset from the response stream's circular buffer. Returns 0 if the controller is not started or has no response stream.

func (*ClaudeController) IsActive

func (cc *ClaudeController) IsActive() bool

IsActive returns whether the Claude instance is actively processing commands.

func (*ClaudeController) IsIdle

func (cc *ClaudeController) IsIdle() bool

IsIdle returns whether the Claude instance is currently idle (waiting for input). This uses pattern-based detection on terminal content.

func (*ClaudeController) IsRateLimitEnabled added in v1.12.0

func (cc *ClaudeController) IsRateLimitEnabled() bool

IsRateLimitEnabled returns whether rate limit detection is enabled.

func (*ClaudeController) IsStarted

func (cc *ClaudeController) IsStarted() bool

IsStarted returns whether the controller is currently started.

func (*ClaudeController) SearchHistory

func (cc *ClaudeController) SearchHistory(query string) []*HistoryEntry

SearchHistory searches command history by text.

func (*ClaudeController) SendCommand

func (cc *ClaudeController) SendCommand(text string, priority int) (string, error)

SendCommand sends a command to the Claude instance (queued execution).

func (*ClaudeController) SendCommandImmediate

func (cc *ClaudeController) SendCommandImmediate(text string) (*ExecutionResult, error)

SendCommandImmediate sends a command for immediate execution (bypasses queue).

func (*ClaudeController) SetExecutionOptions

func (cc *ClaudeController) SetExecutionOptions(options ExecutionOptions)

SetExecutionOptions updates command execution options.

func (*ClaudeController) SetOnEOFCallback added in v1.15.0

func (cc *ClaudeController) SetOnEOFCallback(fn func())

SetOnEOFCallback registers a function called when the PTY backing this controller exits unexpectedly (program exit, not an explicit Stop() call). Must be called before Start().

func (*ClaudeController) SetRateLimitEnabled added in v1.12.0

func (cc *ClaudeController) SetRateLimitEnabled(enabled bool)

SetRateLimitEnabled enables or disables rate limit detection.

func (*ClaudeController) SetStatusChangeListener added in v1.35.0

func (cc *ClaudeController) SetStatusChangeListener(fn StatusChangeListener)

SetStatusChangeListener registers fn as the sole status-change listener, replacing any previously registered listeners. Kept for backward compatibility; prefer AddStatusChangeListener.

func (*ClaudeController) Start

func (cc *ClaudeController) Start(ctx context.Context) error

Start initializes all components and begins background operations (streaming, command execution). This is the single entry point for starting the controller — no separate Initialize() call needed.

The lifecycle write lock is held for the duration of initialization to prevent concurrent Start() calls. Read-only operations (GetCurrentStatus, etc.) do not use this lock and are therefore unblocked — they simply see nil atomic pointers until initialization completes.

func (*ClaudeController) Stop

func (cc *ClaudeController) Stop() error

Stop stops all background operations and cleans up resources.

The lifecycle write lock is held only to cancel the context and clear the lifecycle fields. All slow cleanup (goroutine joins via executor.Stop/responseStream.Stop, disk I/O via queue.Save/history.Save) runs OUTSIDE the lock, so concurrent callers of GetCurrentStatus, GetRecentOutput, Subscribe, etc. are never blocked.

func (*ClaudeController) Subscribe

func (cc *ClaudeController) Subscribe(subscriberID string) (<-chan ResponseChunk, error)

Subscribe creates a new subscription to the response stream.

func (*ClaudeController) Unsubscribe

func (cc *ClaudeController) Unsubscribe(subscriberID string) error

Unsubscribe removes a subscription from the response stream.

type ClaudeConversationMessage

type ClaudeConversationMessage struct {
	Role      string
	Content   string
	Timestamp time.Time
	Model     string
}

ClaudeConversationMessage represents a message in a conversation

type ClaudeHistoryEntry

type ClaudeHistoryEntry struct {
	// ID is the unique identifier for this conversation
	ID string `json:"id"`
	// Name is the conversation title
	Name string `json:"name"`
	// Project is the project/directory path
	Project string `json:"project"`
	// CreatedAt is when the conversation started
	CreatedAt time.Time `json:"created_at"`
	// UpdatedAt is when the conversation was last updated
	UpdatedAt time.Time `json:"updated_at"`
	// Model is the Claude model used (e.g., "claude-sonnet-4")
	Model string `json:"model"`
	// MessageCount is the number of messages in the conversation
	MessageCount int `json:"message_count"`
}

ClaudeHistoryEntry represents a single entry from Claude's history.jsonl file

type ClaudeSession

type ClaudeSession struct {
	ID             string    `json:"id"`
	ConversationID string    `json:"conversation_id"`
	ProjectName    string    `json:"project_name"`
	LastActive     time.Time `json:"last_active"`
	WorkingDir     string    `json:"working_dir"`
	IsActive       bool      `json:"is_active"`
}

ClaudeSession represents a Claude Code session

type ClaudeSessionData

type ClaudeSessionData struct {
	ConversationUUID string            `json:"session_id,omitempty"`       // Claude Code conversation UUID (used for --resume)
	SquadSessionID   string            `json:"squad_session_id,omitempty"` // claude-squad's own session identifier (= Instance.UUID)
	ProjectName      string            `json:"project_name,omitempty"`     // Project name in Claude Code
	LastAttached     time.Time         `json:"last_attached,omitempty"`    // When this session was last used
	Settings         ClaudeSettings    `json:"settings,omitempty"`         // User preferences for Claude Code
	Metadata         map[string]string `json:"metadata,omitempty"`         // Additional session metadata
}

ClaudeSessionData represents Claude Code session information

func (*ClaudeSessionData) UnmarshalJSON added in v1.35.0

func (c *ClaudeSessionData) UnmarshalJSON(data []byte) error

UnmarshalJSON keeps backward compatibility with persisted state written before SquadSessionID was renamed from ConversationID. The legacy "conversation_id" key is read as a fallback when "squad_session_id" is absent, so existing JSON state files continue to hydrate the field on load.

type ClaudeSessionHistory

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

ClaudeSessionHistory manages access to Claude session history

func NewClaudeSessionHistory

func NewClaudeSessionHistory(historyPath string) (*ClaudeSessionHistory, error)

NewClaudeSessionHistory creates a new ClaudeSessionHistory instance

func NewClaudeSessionHistoryFromClaudeDir

func NewClaudeSessionHistoryFromClaudeDir() (*ClaudeSessionHistory, error)

NewClaudeSessionHistoryFromClaudeDir creates a ClaudeSessionHistory from ~/.claude directory

func (*ClaudeSessionHistory) Count

func (sh *ClaudeSessionHistory) Count() int

Count returns the total number of history entries

func (*ClaudeSessionHistory) GetAll

GetAll returns all history entries, sorted by UpdatedAt descending

func (*ClaudeSessionHistory) GetByID

GetByID returns a specific history entry by ID

func (*ClaudeSessionHistory) GetByProject

func (sh *ClaudeSessionHistory) GetByProject(projectPath string) []ClaudeHistoryEntry

GetByProject returns all history entries for a specific project path

func (*ClaudeSessionHistory) GetMessagesFromConversationFile

func (sh *ClaudeSessionHistory) GetMessagesFromConversationFile(sessionID string, limit int) ([]ClaudeConversationMessage, error)

GetMessagesFromConversationFile reads messages from the conversation file for the given sessionID. When limit > 0 only the last limit messages are returned (using an efficient reverse-read that avoids loading the full file). When limit == 0 all messages are returned.

Results are always in chronological order (oldest first).

func (*ClaudeSessionHistory) GetProjects

func (sh *ClaudeSessionHistory) GetProjects() []string

GetProjects returns a list of unique project paths from history

func (*ClaudeSessionHistory) LastLoadTime

func (sh *ClaudeSessionHistory) LastLoadTime() time.Time

LastLoadTime returns when the history was last loaded from disk

func (*ClaudeSessionHistory) Reload

func (sh *ClaudeSessionHistory) Reload() error

Reload loads history from ~/.claude/history.jsonl, which Claude maintains as a compact index of all conversations. Each line is one user message; we aggregate by sessionId to reconstruct per-session metadata (name, timestamps, message count).

func (*ClaudeSessionHistory) Search

func (sh *ClaudeSessionHistory) Search(query string) []ClaudeHistoryEntry

Search searches history entries by name or project path

type ClaudeSessionManager

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

ClaudeSessionManager handles Claude Code session detection and management

func NewClaudeSessionManager

func NewClaudeSessionManager() *ClaudeSessionManager

NewClaudeSessionManager creates a new Claude session manager

func (*ClaudeSessionManager) AttachToSession

func (csm *ClaudeSessionManager) AttachToSession(sessionID string) error

AttachToSession attempts to attach to a Claude Code session

func (*ClaudeSessionManager) CreateSessionData

func (csm *ClaudeSessionManager) CreateSessionData(session ClaudeSession, settings ClaudeSettings) ClaudeSessionData

CreateSessionData creates ClaudeSessionData from a detected session

func (*ClaudeSessionManager) DetectAvailableSessions

func (csm *ClaudeSessionManager) DetectAvailableSessions() ([]ClaudeSession, error)

DetectAvailableSessions scans for available Claude Code sessions

func (*ClaudeSessionManager) FindSessionByProject

func (csm *ClaudeSessionManager) FindSessionByProject(projectPath string) ([]ClaudeSession, error)

FindSessionByProject finds Claude sessions that match a given project/working directory

func (*ClaudeSessionManager) GetSessionByID

func (csm *ClaudeSessionManager) GetSessionByID(sessionID string) (*ClaudeSession, error)

GetSessionByID retrieves a specific Claude session by ID

type ClaudeSettings

type ClaudeSettings struct {
	AutoReattach          bool   `json:"auto_reattach"`           // Automatically reattach to last session on resume
	PreferredSessionName  string `json:"preferred_session_name"`  // Preferred session naming pattern
	CreateNewOnMissing    bool   `json:"create_new_on_missing"`   // Create new session if previous one is missing
	ShowSessionSelector   bool   `json:"show_session_selector"`   // Show session selection menu on resume
	SessionTimeoutMinutes int    `json:"session_timeout_minutes"` // Consider sessions stale after this time
}

ClaudeSettings contains user preferences for Claude Code integration

type CloudContext

type CloudContext struct {
	// Provider is the cloud provider name (aws/gcp/azure/custom)
	Provider string `json:"provider,omitempty"`

	// Region is the cloud region/zone
	Region string `json:"region,omitempty"`

	// InstanceID is the cloud instance identifier
	InstanceID string `json:"instance_id,omitempty"`

	// APIEndpoint is the API endpoint URL for the cloud service
	APIEndpoint string `json:"api_endpoint,omitempty"`

	// APIKeyRef is a reference to secure key storage (not the actual key)
	APIKeyRef string `json:"api_key_ref,omitempty"`

	// CloudSessionID is the cloud provider's session identifier
	CloudSessionID string `json:"cloud_session_id,omitempty"`

	// ConversationID is the conversation/thread identifier for AI services
	ConversationID string `json:"conversation_id,omitempty"`
}

CloudContext represents the cloud-related context for a session. This includes cloud provider details, region, and API configuration.

func (*CloudContext) IsConfigured

func (c *CloudContext) IsConfigured() bool

IsConfigured returns true if the CloudContext has minimum required configuration

func (*CloudContext) IsEmpty

func (c *CloudContext) IsEmpty() bool

IsEmpty returns true if the CloudContext has no meaningful data

type Command

type Command struct {
	ID        string        `json:"id"`
	Text      string        `json:"text"`
	Priority  int           `json:"priority"`  // Higher priority = executed first
	Timestamp time.Time     `json:"timestamp"` // When the command was queued
	Status    CommandStatus `json:"status"`
	Result    string        `json:"result,omitempty"`     // Command result/output
	Error     string        `json:"error,omitempty"`      // Error message if failed
	StartTime time.Time     `json:"start_time,omitempty"` // When execution started
	EndTime   time.Time     `json:"end_time,omitempty"`   // When execution finished
}

Command represents a command to be executed in a Claude instance.

type CommandExecutor

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

CommandExecutor executes commands by writing to PTY and monitoring responses.

func NewCommandExecutor

func NewCommandExecutor(
	sessionName string,
	ptyAccess *PTYAccess,
	responseStream *ResponseStream,
	statusDetector detection.TerminalDetector,
	queue *CommandQueue,
) *CommandExecutor

NewCommandExecutor creates a new command executor for the given session.

func NewCommandExecutorWithOptions

func NewCommandExecutorWithOptions(
	sessionName string,
	ptyAccess *PTYAccess,
	responseStream *ResponseStream,
	statusDetector detection.TerminalDetector,
	queue *CommandQueue,
	options ExecutionOptions,
) *CommandExecutor

NewCommandExecutorWithOptions creates a command executor with custom options.

func (*CommandExecutor) ExecuteImmediate

func (ce *CommandExecutor) ExecuteImmediate(cmd *Command) (*ExecutionResult, error)

ExecuteImmediate executes a command immediately without using the queue. This is useful for interactive commands that need immediate execution.

func (*CommandExecutor) GetCurrentCommand

func (ce *CommandExecutor) GetCurrentCommand() *Command

GetCurrentCommand returns the currently executing command, or nil if none.

func (*CommandExecutor) GetOptions

func (ce *CommandExecutor) GetOptions() ExecutionOptions

GetOptions returns the current execution options.

func (*CommandExecutor) GetSessionName

func (ce *CommandExecutor) GetSessionName() string

GetSessionName returns the session name for this executor.

func (*CommandExecutor) IsExecuting

func (ce *CommandExecutor) IsExecuting() bool

IsExecuting returns whether the executor is currently running.

func (*CommandExecutor) SetOptions

func (ce *CommandExecutor) SetOptions(options ExecutionOptions)

SetOptions updates execution options (only applies to future commands).

func (*CommandExecutor) SetResultCallback

func (ce *CommandExecutor) SetResultCallback(callback func(*ExecutionResult))

SetResultCallback sets a callback function to be invoked after each command execution.

func (*CommandExecutor) Start

func (ce *CommandExecutor) Start(ctx context.Context) error

Start begins processing commands from the queue.

func (*CommandExecutor) Stop

func (ce *CommandExecutor) Stop() error

Stop stops the command executor and waits for completion.

type CommandHistory

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

CommandHistory tracks all executed commands with persistence.

func NewCommandHistory

func NewCommandHistory(sessionName string) *CommandHistory

NewCommandHistory creates a new command history tracker.

func NewCommandHistoryWithPersistence

func NewCommandHistoryWithPersistence(sessionName string, persistDir string) (*CommandHistory, error)

NewCommandHistoryWithPersistence creates a command history with persistence enabled.

func (*CommandHistory) Add

func (ch *CommandHistory) Add(entry *HistoryEntry) error

Add adds a command execution to the history.

func (*CommandHistory) AddFromResult

func (ch *CommandHistory) AddFromResult(result *ExecutionResult) error

AddFromResult creates and adds a history entry from an execution result.

func (*CommandHistory) Clear

func (ch *CommandHistory) Clear() error

Clear removes all history entries.

func (*CommandHistory) Count

func (ch *CommandHistory) Count() int

Count returns the total number of entries in history.

func (*CommandHistory) GetAll

func (ch *CommandHistory) GetAll() []*HistoryEntry

GetAll returns all history entries (most recent first).

func (*CommandHistory) GetByCommandID

func (ch *CommandHistory) GetByCommandID(commandID string) []*HistoryEntry

GetByCommandID returns all history entries for a specific command ID.

func (*CommandHistory) GetByStatus

func (ch *CommandHistory) GetByStatus(status CommandStatus) []*HistoryEntry

GetByStatus returns entries with a specific command status.

func (*CommandHistory) GetByTimeRange

func (ch *CommandHistory) GetByTimeRange(start, end time.Time) []*HistoryEntry

GetByTimeRange returns entries within the specified time range.

func (*CommandHistory) GetFailed

func (ch *CommandHistory) GetFailed() []*HistoryEntry

GetFailed returns all failed command executions.

func (*CommandHistory) GetMaxEntries

func (ch *CommandHistory) GetMaxEntries() int

GetMaxEntries returns the current maximum entries limit.

func (*CommandHistory) GetPersistPath

func (ch *CommandHistory) GetPersistPath() string

GetPersistPath returns the path where history is persisted.

func (*CommandHistory) GetRecent

func (ch *CommandHistory) GetRecent(n int) []*HistoryEntry

GetRecent returns the N most recent history entries.

func (*CommandHistory) GetSessionName

func (ch *CommandHistory) GetSessionName() string

GetSessionName returns the session name for this history.

func (*CommandHistory) GetStatistics

func (ch *CommandHistory) GetStatistics() HistoryStatistics

GetStatistics returns statistics about command execution history.

func (*CommandHistory) GetSuccessful

func (ch *CommandHistory) GetSuccessful() []*HistoryEntry

GetSuccessful returns all successful command executions.

func (*CommandHistory) Load

func (ch *CommandHistory) Load() error

Load restores the history from disk.

func (*CommandHistory) Save

func (ch *CommandHistory) Save() error

Save persists the history to disk.

func (*CommandHistory) Search

func (ch *CommandHistory) Search(query string) []*HistoryEntry

Search searches history entries by command text (case-insensitive substring match).

func (*CommandHistory) SetMaxEntries

func (ch *CommandHistory) SetMaxEntries(max int)

SetMaxEntries sets the maximum number of entries to keep in history. Setting to 0 means unlimited. If current entries exceed the new limit, oldest entries are removed.

func (*CommandHistory) SetPersistPath

func (ch *CommandHistory) SetPersistPath(path string)

SetPersistPath sets the path for history persistence.

type CommandQueue

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

CommandQueue manages a priority queue of commands with persistence.

func NewCommandQueue

func NewCommandQueue(sessionName string) *CommandQueue

NewCommandQueue creates a new command queue for the given session.

func NewCommandQueueWithPersistence

func NewCommandQueueWithPersistence(sessionName string, persistDir string) (*CommandQueue, error)

NewCommandQueueWithPersistence creates a command queue with persistence enabled. The queue state will be saved to the specified directory.

func (*CommandQueue) Cancel

func (cq *CommandQueue) Cancel(id string) error

Cancel marks a command as cancelled and removes it from the queue. Returns an error if the command is not found or is already executing.

func (*CommandQueue) Clear

func (cq *CommandQueue) Clear() error

Clear removes all commands from the queue.

func (*CommandQueue) Dequeue

func (cq *CommandQueue) Dequeue() *Command

Dequeue removes and returns the highest priority command from the queue. Returns nil if the queue is empty.

func (*CommandQueue) Enqueue

func (cq *CommandQueue) Enqueue(cmd *Command) error

Enqueue adds a command to the queue with the specified priority. Higher priority commands are executed first.

func (*CommandQueue) Get

func (cq *CommandQueue) Get(id string) (*Command, error)

Get retrieves a command by ID without removing it from the queue.

func (*CommandQueue) GetPersistPath

func (cq *CommandQueue) GetPersistPath() string

GetPersistPath returns the path where the queue state is persisted.

func (*CommandQueue) IsEmpty

func (cq *CommandQueue) IsEmpty() bool

IsEmpty returns true if the queue is empty.

func (*CommandQueue) Len

func (cq *CommandQueue) Len() int

Len returns the number of commands in the queue.

func (*CommandQueue) List

func (cq *CommandQueue) List() []*Command

List returns all commands currently in the queue. The returned slice is a copy to prevent external modification.

func (*CommandQueue) ListByStatus

func (cq *CommandQueue) ListByStatus(status CommandStatus) []*Command

ListByStatus returns all commands with the specified status.

func (*CommandQueue) Load

func (cq *CommandQueue) Load() error

Load restores the queue state from disk.

func (*CommandQueue) NotifyChannel

func (cq *CommandQueue) NotifyChannel() <-chan struct{}

NotifyChannel returns a channel that receives a notification when commands are added. This can be used to wait for new commands without polling.

func (*CommandQueue) Peek

func (cq *CommandQueue) Peek() *Command

Peek returns the highest priority command without removing it. Returns nil if the queue is empty.

func (*CommandQueue) Save

func (cq *CommandQueue) Save() error

Save persists the queue state to disk.

func (*CommandQueue) SetPersistPath

func (cq *CommandQueue) SetPersistPath(path string)

SetPersistPath sets the path for queue persistence.

func (*CommandQueue) Update

func (cq *CommandQueue) Update(cmd *Command) error

Update updates the status and metadata of a command.

type CommandStatus

type CommandStatus int

CommandStatus represents the current status of a command in the queue.

const (
	CommandPending CommandStatus = iota
	CommandExecuting
	CommandCompleted
	CommandFailed
	CommandCancelled
)

func (CommandStatus) String

func (cs CommandStatus) String() string

String returns a human-readable string for the command status.

type CommitImportParams added in v1.42.0

type CommitImportParams struct {
	Detector     *HistoryFileDetector
	Storage      InstanceStore
	Registry     *Registry
	Linker       *HistoryLinker
	Suspended    *SuspendedProcessStore
	AliveChecker AliveChecker
	TmuxQuerier  TmuxSocketQuerier

	Candidate            ExternalSessionCandidate
	ExpectedCorrelation  CorrelationResult
	DisambiguationChoice string
	OriginalPID          int32
	OriginalCreateTimeMs int64

	// TmuxServerSocket, when non-empty, isolates the committed instance's
	// tmux server via the -L flag (see InstanceOptions.TmuxServerSocket).
	// Empty (the production default) uses the shared default tmux server.
	// Tests that exercise the real Start() path should set this to a unique
	// per-test socket (matching instance_cold_restore_test.go's
	// coldRestoreSocket pattern) to avoid contending with every other test's
	// tmux operations on the single shared server under parallel CI load.
	TmuxServerSocket string
}

CommitImportParams holds everything CommitImportExternalSession needs to persist a managed Instance for an external candidate, start it resumed, and suspend the original process.

type CommitImportResult added in v1.42.0

type CommitImportResult struct {
	Instance *Instance
	// FreshCreateTimeMs is the caller-supplied OriginalCreateTimeMs, echoed
	// back for the caller to mint a fresh PIDIdentity in the RPC response
	// (per import.proto's doc comment on
	// CommitImportExternalSessionResponse.pid_identity). Trustworthy because
	// startAndSuspend re-verifies OriginalPID/OriginalCreateTimeMs against
	// the live process (via AliveChecker) immediately before suspending it
	// -- it is not a fresh OS re-read taken at this point.
	FreshCreateTimeMs int64
}

CommitImportResult is the domain-level outcome of a successful commit.

func CommitImportExternalSession added in v1.42.0

func CommitImportExternalSession(ctx context.Context, params CommitImportParams) (CommitImportResult, error)

CommitImportExternalSession re-runs correlation fresh (never trusting the caller-supplied ExpectedCorrelation), resolves the conversation UUID to resume, persists a managed Instance, registers it with HistoryLinker, starts it resumed, and finally suspends the original process -- in that order, so the original process only stops writing once the resumed session is confirmed up and running.

On any failure after the Instance has been persisted, the Instance is compensating-deleted before returning (Story 1.2.3) so no partial/orphan row is left behind. On any failure before suspension, the original process is never touched.

type CompletionCallback added in v1.35.0

type CompletionCallback func(instanceName string, outcome AutonomousDriverOutcome)

CompletionCallback is called when the driver exits with a final outcome.

type ContentProvider added in v1.35.0

type ContentProvider interface {
	GetContent(inst *Instance, statusInfo InstanceStatusInfo, paneActivity map[string]time.Time) string
	EvictInstance(title string)
}

ContentProvider fetches terminal content for a session. Defined at the consumption point so tests can inject fakes without tmux.

func NewPollerContentProvider added in v1.35.0

func NewPollerContentProvider() ContentProvider

NewPollerContentProvider creates a new pollerContentProvider. It is exported so server/dependencies.go can pass it to NewStartupScanner.

type ContextOptions

type ContextOptions struct {
	// Context loading flags
	LoadGit        bool // Git repository context (branch, commit, remotes)
	LoadFilesystem bool // Filesystem context (directory state, file counts)
	LoadTerminal   bool // Terminal context (output, command history)
	LoadUI         bool // UI context (position, focus state, expanded/collapsed)
	LoadActivity   bool // Activity context (last active, duration, events)
	LoadCloud      bool // Cloud context (API sessions, remote state)

	// Child data loading flags (from existing LoadOptions)
	LoadWorktree      bool // Git worktree data
	LoadDiffStats     bool // Diff statistics (added/removed counts)
	LoadDiffContent   bool // Full diff content (heavy - only load when needed)
	LoadTags          bool // Session tags
	LoadClaudeSession bool // Claude Code session data
}

ContextOptions specifies which optional contexts to load when querying sessions. This enables optimized queries that only load the data needed for each use case.

func FromLoadOptions

func FromLoadOptions(lo LoadOptions) ContextOptions

FromLoadOptions creates ContextOptions from the legacy LoadOptions type. This provides backward compatibility when migrating existing code.

func (ContextOptions) AnyChildDataLoaded

func (o ContextOptions) AnyChildDataLoaded() bool

AnyChildDataLoaded returns true if any child data is configured to load.

func (ContextOptions) AnyContextLoaded

func (o ContextOptions) AnyContextLoaded() bool

AnyContextLoaded returns true if any context is configured to load.

func (ContextOptions) Merge

Merge combines two ContextOptions, returning options that load the union of both. This is useful for combining requirements from multiple components.

func (ContextOptions) String

func (o ContextOptions) String() string

String returns a human-readable description of what will be loaded.

func (ContextOptions) ToLoadOptions

func (o ContextOptions) ToLoadOptions() LoadOptions

ToLoadOptions converts ContextOptions to the legacy LoadOptions type. This provides backward compatibility with existing code.

func (ContextOptions) WithActivity

func (o ContextOptions) WithActivity() ContextOptions

WithActivity returns a copy of options with activity context loading enabled.

func (ContextOptions) WithCloud

func (o ContextOptions) WithCloud() ContextOptions

WithCloud returns a copy of options with cloud context loading enabled.

func (ContextOptions) WithDiffContent

func (o ContextOptions) WithDiffContent() ContextOptions

WithDiffContent returns a copy of options with diff content loading enabled.

func (ContextOptions) WithFilesystem

func (o ContextOptions) WithFilesystem() ContextOptions

WithFilesystem returns a copy of options with filesystem context loading enabled.

func (ContextOptions) WithGit

func (o ContextOptions) WithGit() ContextOptions

WithGit returns a copy of options with git context loading enabled.

func (ContextOptions) WithTags

func (o ContextOptions) WithTags() ContextOptions

WithTags returns a copy of options with tag loading enabled.

func (ContextOptions) WithTerminal

func (o ContextOptions) WithTerminal() ContextOptions

WithTerminal returns a copy of options with terminal context loading enabled.

func (ContextOptions) WithUI

func (o ContextOptions) WithUI() ContextOptions

WithUI returns a copy of options with UI context loading enabled.

func (ContextOptions) WithoutDiffContent

func (o ContextOptions) WithoutDiffContent() ContextOptions

WithoutDiffContent returns a copy of options with diff content loading disabled.

func (ContextOptions) WithoutTags

func (o ContextOptions) WithoutTags() ContextOptions

WithoutTags returns a copy of options with tag loading disabled.

type ControllerManager

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

ControllerManager owns the ClaudeController and InstanceStatusManager references that were previously bare fields on Instance.

Instance keeps thin wrapper methods (with lifecycle guards) that delegate here. ControllerManager itself has no knowledge of Instance lifecycle; it only manages the controller and status-manager references.

Note: claudeSession is intentionally NOT included here because it is a rich data object with complex lifecycle management (persistence, re-attachment, session selection) that is tightly coupled to Instance business logic. It remains a direct field on Instance for now.

Both controller and statusManager use atomic.Pointer for lock-free concurrent access. Write operations (Register/Unregister/Set) are expected to be called sequentially from the Instance lifecycle path and are not themselves concurrency-safe against each other. ControllerManager must not be copied after first use (enforced by noCopy).

func (*ControllerManager) GetController

func (cm *ControllerManager) GetController() *ClaudeController

GetController returns the current ClaudeController (may be nil).

func (*ControllerManager) GetStatusManager

func (cm *ControllerManager) GetStatusManager() *InstanceStatusManager

GetStatusManager returns the current InstanceStatusManager (may be nil).

func (*ControllerManager) HasController

func (cm *ControllerManager) HasController() bool

HasController reports whether a ClaudeController has been registered.

func (*ControllerManager) RegisterController

func (cm *ControllerManager) RegisterController(title string, controller *ClaudeController)

RegisterController wires a new controller into the status manager and stores it. Any existing controller is stopped first.

func (*ControllerManager) SetController

func (cm *ControllerManager) SetController(c *ClaudeController)

SetController replaces the controller. Callers are responsible for stopping the old controller before calling this.

func (*ControllerManager) SetStatusManager

func (cm *ControllerManager) SetStatusManager(m *InstanceStatusManager)

SetStatusManager replaces the status manager.

func (*ControllerManager) StopAndClearController

func (cm *ControllerManager) StopAndClearController()

StopAndClearController stops the controller (if running) and clears the reference.

func (*ControllerManager) UnregisterController

func (cm *ControllerManager) UnregisterController(title string)

UnregisterController stops and clears the controller, and removes it from the status manager.

type ConversationID added in v1.35.0

type ConversationID string

ConversationID represents a validated Claude/Antigravity conversation UUID.

func ParseConversationID added in v1.35.0

func ParseConversationID(s string) (ConversationID, error)

ParseConversationID parses and validates a raw string as a ConversationID.

type CorrelationConfidence added in v1.42.0

type CorrelationConfidence int

CorrelationConfidence records *why* a Resolved match was made, surfaced to the UI so the user can see the basis for the match.

const (
	// ConfidenceNone applies when Kind is not Resolved.
	ConfidenceNone CorrelationConfidence = iota
	// ConfidencePIDExact means HistoryFileDetector.Detect found the file via
	// the candidate's live open file descriptors.
	ConfidencePIDExact
	// ConfidencePathHeuristic means DetectAllByPath found exactly one
	// candidate for the project path (no live PID match was available).
	ConfidencePathHeuristic
)

type CorrelationKind added in v1.42.0

type CorrelationKind int

CorrelationKind is the discriminant of a CorrelationResult. Callers must exhaustively switch on it — Ambiguous and NotFound are valid, non-error outcomes and must never be silently collapsed to a single guess (see pitfalls research must-not-happen #5).

const (
	// CorrelationNotFound means neither PID nor path correlation found a
	// history file. This is a valid state — the JSONL may not exist yet.
	CorrelationNotFound CorrelationKind = iota
	// CorrelationResolved means exactly one history file was identified.
	CorrelationResolved
	// CorrelationAmbiguous means more than one history file could plausibly
	// belong to this candidate; the caller must require a DisambiguationChoice.
	CorrelationAmbiguous
)

func (CorrelationKind) String added in v1.42.0

func (k CorrelationKind) String() string

String returns a human-readable name for logging.

type CorrelationResult added in v1.42.0

type CorrelationResult struct {
	Kind       CorrelationKind
	UUID       string
	Confidence CorrelationConfidence
	// Candidates is populated only when Kind == CorrelationAmbiguous.
	Candidates []HistoryFileInfo
}

CorrelationResult is the exhaustive, non-silent outcome of running correlation against an ExternalSessionCandidate.

func CorrelateCandidate added in v1.42.0

func CorrelateCandidate(detector *HistoryFileDetector, candidate ExternalSessionCandidate) (CorrelationResult, error)

CorrelateCandidate runs HistoryFileDetector against a candidate and returns an exhaustive Resolved/Ambiguous/NotFound result. It is a one-shot call: it does not register the candidate with HistoryLinker and does not start any polling/backoff (that only happens after commit).

PID-based detection is tried first; if it finds nothing (dead process, no open Claude file, or PlainTmux candidates that have no PID-openable file), it falls back to path-based detection using DetectAllByPath so that ambiguity is never silently collapsed to "most recent".

type CostSnapshot added in v1.41.0

type CostSnapshot struct {
	TotalTokens      int64
	EstimatedCostUSD float64
	// DataUnavailable distinguishes "genuinely zero tokens" from "cost data could
	// not be read" (e.g. no transcript found for this session).
	DataUnavailable bool
}

CostSnapshot captures token usage/cost data for a session.

func BuildCostSnapshot added in v1.41.0

func BuildCostSnapshot(sessionUUID string, tokenStore tokens.TokenStoreReader) CostSnapshot

BuildCostSnapshot builds a CostSnapshot from tokenStore.GetByUUID(sessionUUID). tokenStore is accepted as the narrow tokens.TokenStoreReader interface (already used by InsightsService) rather than the concrete *tokens.TokenStore, so tests can inject a fake without constructing a real store. Nil-safe: a nil tokenStore or a nil ParseResult (no transcript found) returns CostSnapshot{DataUnavailable: true} — distinct from "zero tokens were genuinely used" (research/ux.md §4).

type CreateInstanceOptions added in v1.42.0

type CreateInstanceOptions = InstanceOptions

CreateInstanceOptions is a type alias for session.InstanceOptions, kept as a distinct name in this file so CreateManagedInstanceParams reads clearly at call sites (options for the instance to create) without forcing every caller to spell out the full InstanceOptions literal inline.

type CreateManagedInstanceParams added in v1.42.0

type CreateManagedInstanceParams struct {
	Options CreateInstanceOptions

	// Storage persists the constructed Instance (mirrors
	// SessionService.storage; typically a *session.Storage backed by
	// EntRepository). Required.
	Storage InstanceStore

	// Registry, if non-nil, registers the instance's live handle before
	// Storage.AddInstance runs, matching CreateSession's existing
	// register-before-persist ordering so there is never a window where the
	// session is findable via storage but has no live actor. Optional --
	// callers that don't use the live-handle registry (there are none today,
	// but the field is a plain pointer, not an interface, so this is not
	// speculative) may leave it nil.
	Registry *Registry

	// CreateIfMissing controls whether a Directory-mode session may be
	// created even though its target path does not yet exist on disk. This
	// mirrors CreateSessionRequest.CreateIfMissing but is surfaced separately
	// from CreateInstanceOptions since it drives a pre-flight check rather
	// than an InstanceOptions field.
	CreateIfMissing bool

	// ResumeID, when non-empty, is the conversation UUID this instance
	// should resume via `--resume`. Named ResumeID (not embedded in
	// InstanceOptions.ResumeId) to match the plan's
	// CreateManagedInstanceParams{SessionType, Path, ResumeID} example
	// verbatim; it is copied onto InstanceOptions.ResumeId internally.
	ResumeID string
}

CreateManagedInstanceParams holds everything needed to construct and persist a managed Instance, independent of how the caller arrived at these values (an RPC request, an import commit, etc). Deliberately a plain struct with no connect.Request/connect.Response types in sight -- both SessionService.CreateSession's connect handler and CommitImportExternalSession (Story 1.2.1) build one of these and call CreateManagedInstance directly, so the dependency always points RPC layer -> domain function, never handler -> handler (see project_plans/import-external-session/implementation/plan.md Story 1.2.0a).

type CriterionVerdict added in v1.35.0

type CriterionVerdict = domain.CriterionVerdict

CriterionVerdict holds the review outcome for a single acceptance criterion. Type alias — session.CriterionVerdict and domain.CriterionVerdict are identical types.

type DecisionRecord added in v1.41.0

type DecisionRecord struct {
	// NotificationType is the record's NotificationType field — compare against
	// sessionv1.NotificationType_NOTIFICATION_TYPE_APPROVAL_NEEDED /
	// _AUTO_APPROVED.
	NotificationType int32
	// ApprovalDecision is the record's Metadata["approval_decision"] value
	// ("allow"/"deny"/"" for unresolved/other outcomes) — the same key
	// server/services/approval_handler.go and
	// server/notifications.AppendAutoApproved stamp.
	ApprovalDecision string
}

DecisionRecord is the minimal per-notification-record data BuildDecisionsSnapshot needs to classify an approval decision. Deliberately decoupled from server/notifications.NotificationRecord: session cannot import server/notifications directly (server/notifications -> server/events -> pkg/events -> session is a real import cycle, confirmed via `go list -deps`), so a NotificationDecisionLister implementation (wired in Phase 2, server/services) maps *notifications.NotificationHistoryStore.List results onto this shape.

type DecisionsSnapshot added in v1.41.0

type DecisionsSnapshot struct {
	AutoApproved        int
	ManuallyApproved    int
	Denied              int
	ReviewQueueResolved int
	StillOpen           int
}

DecisionsSnapshot is a deterministic count of approval decisions made during a session.

func BuildDecisionsSnapshot added in v1.41.0

func BuildDecisionsSnapshot(ctx context.Context, sessionID string, notifLister NotificationDecisionLister, reviewLookup ReviewQueueLookup) (DecisionsSnapshot, error)

BuildDecisionsSnapshot queries a NotificationDecisionLister for approval-decision records and ReviewQueueLookup for backlog review-queue resolution counts to build a DecisionsSnapshot. The real NotificationHistoryStore-backed lister never returns a non-nil error today (file corruption is swallowed silently at load time — server/notifications/store.go) — reviewLookup.ReviewQueueResolvedCount's DB-backed call is this function's only realistic error source.

func (DecisionsSnapshot) Percent added in v1.41.0

func (d DecisionsSnapshot) Percent(n int) float64

Percent returns what percentage n represents of the snapshot's Total, or 0 if Total is 0.

func (DecisionsSnapshot) Total added in v1.41.0

func (d DecisionsSnapshot) Total() int

Total returns the sum of all decision counts.

type DefaultStatusDeterminer added in v1.35.0

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

DefaultStatusDeterminer implements StatusDeterminer with the standard detection logic.

func NewDefaultStatusDeterminer added in v1.35.0

func NewDefaultStatusDeterminer(config ReviewQueuePollerConfig) *DefaultStatusDeterminer

NewDefaultStatusDeterminer creates a DefaultStatusDeterminer with the given config.

func (*DefaultStatusDeterminer) Determine added in v1.35.0

func (d *DefaultStatusDeterminer) Determine(
	inst *Instance,
	content string,
	statusInfo InstanceStatusInfo,
	detector detection.TerminalDetector,
) DetectionResult

Determine evaluates a session's state and returns a DetectionResult. It is pure: no queue mutations, no storage calls, no side effects.

type DefaultWorkflowEngine added in v1.35.0

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

DefaultWorkflowEngine implements WorkflowEngine using the hardcoded validTransitions map and TransitionGuard function from backlog.go.

func NewDefaultWorkflowEngine added in v1.35.0

func NewDefaultWorkflowEngine() *DefaultWorkflowEngine

NewDefaultWorkflowEngine constructs an engine backed by the static validTransitions map. The map is deep-copied to avoid shared mutable state.

func (*DefaultWorkflowEngine) AllowedTransitions added in v1.35.0

func (e *DefaultWorkflowEngine) AllowedTransitions(from BacklogStatus) []BacklogStatus

AllowedTransitions implements WorkflowEngine.

func (*DefaultWorkflowEngine) CanTransition added in v1.35.0

func (e *DefaultWorkflowEngine) CanTransition(from, to BacklogStatus) bool

CanTransition implements WorkflowEngine.

func (*DefaultWorkflowEngine) ValidateGates added in v1.35.0

ValidateGates implements WorkflowEngine by delegating to TransitionGuard.

type DetectionAction added in v1.35.0

type DetectionAction int

DetectionAction represents what the poller should do after status determination.

const (
	DetectionActionSkip   DetectionAction = iota // No change to queue
	DetectionActionAdd                           // Add/update item in queue
	DetectionActionRemove                        // Remove item from queue
)

type DetectionResult added in v1.35.0

type DetectionResult struct {
	Action       DetectionAction
	Reason       AttentionReason
	Priority     Priority
	Context      string
	ClaudeStatus detection.DetectedStatus
	// CleanWorktree is true when the worktree was inspected and found clean.
	// checkSession uses this to remove a queued UncommittedChanges entry immediately.
	CleanWorktree bool
}

DetectionResult is the output of status determination — pure data, no side effects.

func (DetectionResult) IsHighPriority added in v1.35.0

func (r DetectionResult) IsHighPriority() bool

IsHighPriority returns true when the result warrants bypassing grace-period suppression.

type DiffSnapshot added in v1.41.0

type DiffSnapshot struct {
	FilesChanged int
	Added        int
	Removed      int
}

DiffSnapshot is a deterministic, point-in-time summary of a session's git diff.

func BuildDiffSnapshot added in v1.41.0

func BuildDiffSnapshot(stats *git.DiffStats) DiffSnapshot

BuildDiffSnapshot converts a captured *git.DiffStats into a DiffSnapshot. Nil-safe: a nil stats (no worktree / directory session with no changes) returns an empty, non-error DiffSnapshot.

func (DiffSnapshot) IsEmpty added in v1.41.0

func (d DiffSnapshot) IsEmpty() bool

IsEmpty reports whether the snapshot represents no changes at all.

type DiffStatsData

type DiffStatsData struct {
	Added   int    `json:"added"`
	Removed int    `json:"removed"`
	Content string `json:"-"` // Excluded from serialization - generated on-demand
}

DiffStatsData represents the serializable data of a DiffStats Note: Content is excluded from JSON serialization to reduce state file size. Diffs are generated on-demand via GetSessionDiff RPC when needed.

type DiscoveryMode

type DiscoveryMode int

DiscoveryMode controls what instances are discovered and how they can be interacted with

const (
	// DiscoveryModeManaged discovers only squad-managed sessions (default, safest)
	DiscoveryModeManaged DiscoveryMode = iota

	// DiscoveryModeExtended discovers managed + external instances in read-only mode
	DiscoveryModeExtended

	// DiscoveryModeFull discovers all instances with attach capability (power user mode)
	DiscoveryModeFull
)

func ParseDiscoveryMode

func ParseDiscoveryMode(s string) DiscoveryMode

ParseDiscoveryMode parses a string into a DiscoveryMode

func (DiscoveryMode) String

func (dm DiscoveryMode) String() string

type DriverOption added in v1.35.0

type DriverOption func(*AutonomousDriver)

DriverOption is a functional option for configuring an AutonomousDriver.

func WithStartupTimeout added in v1.35.0

func WithStartupTimeout(d time.Duration) DriverOption

WithStartupTimeout overrides the default 60s startup idle-wait timeout. Use a longer timeout for sessions that spawn parallel subagents (e.g. triage).

type EntPipelineModeRepository added in v1.38.0

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

EntPipelineModeRepository implements PipelineModeRepository using the ent ORM.

func NewEntPipelineModeRepository added in v1.38.0

func NewEntPipelineModeRepository(client *ent.Client) *EntPipelineModeRepository

NewEntPipelineModeRepository creates a new ent-backed pipeline mode repository.

func (*EntPipelineModeRepository) Create added in v1.38.0

Create inserts a new pipeline mode definition. Returns ent.ConstraintError when a duplicate slug exists.

func (*EntPipelineModeRepository) Delete added in v1.38.0

Delete removes a pipeline mode by UUID.

func (*EntPipelineModeRepository) GetByID added in v1.38.0

GetByID retrieves a pipeline mode by UUID.

func (*EntPipelineModeRepository) GetBySlug added in v1.38.0

GetBySlug retrieves a pipeline mode by slug.

func (*EntPipelineModeRepository) ListAll added in v1.38.0

ListAll returns all pipeline modes sorted ascending by created_at. A safety cap of 1000 is applied to prevent runaway queries.

func (*EntPipelineModeRepository) ListEnabled added in v1.38.0

func (r *EntPipelineModeRepository) ListEnabled(ctx context.Context) ([]*ent.PipelineMode, error)

ListEnabled returns only pipeline modes where enabled is true.

func (*EntPipelineModeRepository) Update added in v1.38.0

Update applies a partial update to an existing pipeline mode by UUID.

type EntRepository

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

EntRepository implements the Repository interface using Ent ORM as the storage backend. It provides type-safe database operations with automatic schema migrations.

func NewEntRepository

func NewEntRepository(opts ...RepositoryOption) (*EntRepository, error)

NewEntRepository creates a new Ent repository with the given options. The database will be initialized with the schema if it doesn't exist.

func NewEntRepositoryFromClient added in v1.35.0

func NewEntRepositoryFromClient(client *ent.Client) *EntRepository

NewEntRepositoryFromClient wraps a pre-existing *ent.Client in an EntRepository. The caller is responsible for running schema migration on the client beforehand. Use this when you need to share an already-opened client across subsystems (e.g. injecting a test client or reusing an existing connection).

func (*EntRepository) AddBacklogItemDependency added in v1.43.0

func (r *EntRepository) AddBacklogItemDependency(ctx context.Context, edge BacklogItemDependencyEdge) error

AddBacklogItemDependency records that edge.BlockedID may not be dequeued/started until edge.BlockerID reaches a resolved status — see UnresolvedBlockerItemIDs for what counts as resolved (done or archived). Adding an already-existing pair is a no-op (upsert against the unique (blocker_id, blocked_id) index). Returns ErrDependencyCycle if the new edge would create a cycle, including the degenerate self-dependency case (BlockerID == BlockedID).

func (*EntRepository) AllRules added in v1.12.0

func (r *EntRepository) AllRules(ctx context.Context) ([]ApprovalRuleData, error)

func (*EntRepository) AppendProgressNote added in v1.38.0

func (r *EntRepository) AppendProgressNote(ctx context.Context, itemID string, criterionIndex int, note, status string) error

AppendProgressNote records a single report_progress call as an immutable history entry, in addition to (not instead of) the current-note-per-criterion stored on BacklogItem.AcceptanceCriteria. Callers should treat failures here as best-effort: the history is an enrichment for reviewers, not part of report_progress's primary contract of updating the criterion's current status/note.

func (*EntRepository) ArchiveBacklogItem added in v1.35.0

func (r *EntRepository) ArchiveBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)

ArchiveBacklogItem sets the archived_at timestamp on a backlog item.

func (*EntRepository) AssignSessionsToProject added in v1.23.0

func (r *EntRepository) AssignSessionsToProject(ctx context.Context, projectName string, sessionTitles []string) error

AssignSessionsToProject links sessions (by title) to a project (by name).

func (*EntRepository) BackfillMissingPRNumbers added in v1.37.0

func (r *EntRepository) BackfillMissingPRNumbers(ctx context.Context) (int, error)

BackfillMissingPRNumbers finds pr_pending items with a pr_url but no pr_number (pr_number == 0) and parses the number out of the URL. Such items are otherwise permanently invisible to FindPRPendingItems' PrNumberGT(0) filter, so ReconcilePRPending never polls them — a real stuck-forever case found via manual QA against live data, not something the loop itself would ever have surfaced. Best-effort: a URL that doesn't match is left as-is and logged, not treated as fatal.

func (*EntRepository) BulkResetStuckRemediation added in v1.39.0

func (r *EntRepository) BulkResetStuckRemediation(ctx context.Context, reason *domain.StuckReason, onlyParked bool) (int, error)

BulkResetStuckRemediation applies ResetStuckRemediation's reset (attempts -> 0, next_remediation_at/notified_at cleared) to every open BacklogStuckState row, optionally filtered to a single reason (reason == nil means "every reason") and, when onlyParked is true (the default from the RPC layer), restricted to rows that actually hit the attempt cap (remediation_attempts >= MaxRemediationAttempts) — the "something upstream broke a batch of these, give them all a fresh shot" admin action. Returns the number of rows reset.

func (*EntRepository) ClaimChainFire added in v1.43.0

func (r *EntRepository) ClaimChainFire(ctx context.Context, id string, expectedUpdatedAt time.Time) (claimed bool, err error)

ClaimChainFire atomically claims item id's pipeline chain-fire attempt by flipping chain_fired from false to true, conditioned on both chain_fired still being false and updated_at still matching expectedUpdatedAt — a genuine SQL-level compare-and-swap folded into the UPDATE's own WHERE clause, the same pattern (and for the identical reason) as TransitionBacklogItemStatus's precondition handling above (see that method's doc comment, BUG-026): a Get-then-check-in-Go-then-write sequence (which UpdateBacklogItem's precondition parameter still is, deliberately not reused here) leaves a race window wide enough for two concurrent callers to both pass the check and both issue their own write. Used by ChainFirer.Fire to guarantee at most one goroutine ever reaches TriggerFirer.FireTriggerChained for a given item (webhook-triggers Task 6.2.1d — the happy-path async dispatch and TriggerChainReconciler's periodic sweep can otherwise race on the same item).

Returns claimed=false (not an error) when another caller already won the claim or the row's updated_at moved for any other reason — callers must treat that as "someone else is handling this," not a failure.

func (*EntRepository) Close

func (r *EntRepository) Close() error

Close performs cleanup and releases resources

func (*EntRepository) CountReviewCyclesSince added in v1.38.0

func (r *EntRepository) CountReviewCyclesSince(ctx context.Context, itemID string, since time.Time) (int, error)

CountReviewCyclesSince counts in_progress->review BacklogStatusEvent transitions for itemID created at or after since — the "round trip" signal the bouncing detector (isBouncing) keys off.

func (*EntRepository) Create

func (r *EntRepository) Create(ctx context.Context, data InstanceData) error

Create inserts a new session into the database

func (*EntRepository) CreateBacklogItem added in v1.35.0

func (r *EntRepository) CreateBacklogItem(ctx context.Context, data BacklogItemData) (*BacklogItemData, error)

CreateBacklogItem inserts a new backlog item.

func (*EntRepository) CreateItemSession added in v1.35.0

func (r *EntRepository) CreateItemSession(ctx context.Context, data ItemSessionData) (ItemSessionSummary, error)

CreateItemSession creates a new ItemSession linked to a BacklogItem.

func (*EntRepository) CreateItemSessionWithVerdict added in v1.35.0

func (r *EntRepository) CreateItemSessionWithVerdict(ctx context.Context, isData ItemSessionData, verdict ReviewVerdictData) (ItemSessionSummary, error)

CreateItemSessionWithVerdict atomically creates an ItemSession and its initial ReviewVerdict in a single transaction. If the verdict write fails the ItemSession is rolled back, preventing dangling sessions with no verdict.

func (*EntRepository) CreateItemSource added in v1.35.0

func (r *EntRepository) CreateItemSource(ctx context.Context, data ItemSourceData) (*ItemSourceData, error)

CreateItemSource registers a new external item source.

func (*EntRepository) CreateProject added in v1.23.0

func (r *EntRepository) CreateProject(ctx context.Context, data ProjectData) (*ProjectData, error)

CreateProject inserts a new project.

func (*EntRepository) CreateSession

func (r *EntRepository) CreateSession(ctx context.Context, session *Session) error

CreateSession creates a new session from the Session domain model. Stub: not yet implemented; use InstanceData-based methods instead.

func (*EntRepository) CreateShell added in v1.35.0

func (r *EntRepository) CreateShell(ctx context.Context, sessionTitle string, data ShellData) (*ent.Shell, error)

CreateShell persists a new Shell entity for the given session title.

func (*EntRepository) CreateSourceSyncEvent added in v1.35.0

func (r *EntRepository) CreateSourceSyncEvent(ctx context.Context, sourceID string, cursorAfter string, created, updated, skipped, errored int, errMsg string, startedAt, finishedAt time.Time) error

CreateSourceSyncEvent records a completed (or failed) sync run for an ItemSource. errMsg should be non-empty only when the sync run failed outright (e.g. the plugin's Fetch call errored); errored counts per-item failures within an otherwise-successful fetch.

func (*EntRepository) Delete

func (r *EntRepository) Delete(ctx context.Context, title string) error

Delete removes a session from the database by title

func (*EntRepository) DeleteBacklogItem added in v1.35.0

func (r *EntRepository) DeleteBacklogItem(ctx context.Context, id string) error

DeleteBacklogItem permanently removes an item and all its child records.

func (*EntRepository) DeleteItemSource added in v1.35.0

func (r *EntRepository) DeleteItemSource(ctx context.Context, id string) error

DeleteItemSource removes an item source by UUID string.

func (*EntRepository) DeleteProject added in v1.23.0

func (r *EntRepository) DeleteProject(ctx context.Context, name string) error

DeleteProject removes a project; sessions are unassigned (FK cleared) atomically.

func (*EntRepository) DeleteRule added in v1.12.0

func (r *EntRepository) DeleteRule(ctx context.Context, id string) error

func (*EntRepository) DeleteShell added in v1.35.0

func (r *EntRepository) DeleteShell(ctx context.Context, shellID string) error

DeleteShell removes a Shell entity by ID.

func (*EntRepository) FindDoneItemsOlderThan added in v1.39.0

func (r *EntRepository) FindDoneItemsOlderThan(ctx context.Context, cutoff time.Time) ([]BacklogItemData, error)

FindDoneItemsOlderThan returns backlog items currently in "done" status whose most recent transition INTO "done" happened at or before cutoff. Used by the auto-archive sweep (archiveStaleDoneItems in backlog_lifecycle.go) to find items eligible for automatic archival.

Deliberately keys off the status-event history rather than UpdatedAt: UpdatedAt changes on any field edit (progress notes, notification flags, etc.), which would reset — and so make unreliable — a clock meant to measure "how long has this been done". TransitionBacklogItemStatus always appends an audit BacklogStatusEvent row on every transition, so this reuses existing infrastructure instead of adding a dedicated done_at column.

An item whose status-event history has no toStatus=="done" record is skipped (never considered eligible) rather than defaulting to "always eligible" — this should not happen in practice, since every transition through TransitionBacklogItemStatus writes one, but guards a partially- migrated or directly-seeded row against aging out on an unrelated basis.

func (*EntRepository) FindDriftedPRItems added in v1.39.0

func (r *EntRepository) FindDriftedPRItems(ctx context.Context) ([]*ent.BacklogItem, error)

FindDriftedPRItems returns backlog items with a live PR reference (a non-zero pr_number and non-empty pr_url) whose status is neither "pr_pending" nor a terminal state (done/archived) — i.e. items that ReconcilePRPending's FindPRPendingItems can never see because it anchors purely on status=="pr_pending", even though the item demonstrably has a real PR that needs the same merge/CI polling. This happens when pushAndCreatePR/shipViaAgentOrFallback persist prNumber/prUrl (which they do unconditionally, before attempting the status transition) but the follow-up CAS transition to pr_pending then loses a race to some other legitimate concurrent event — e.g. markAbandonedReview's grace period firing and respawning a review pass while an agent-driven ship is still mid-flight, or a rework/bounce cycle that exhausts its cap before ever re-shipping. Confirmed live 2026-07-20 on two items (c2ad7bf3-91bf-4d47- 8654-0f2f20869080, PR #251; 6700a3f2-8c0d-4a98-8bbd-39515d5391b1, PR #172) stuck at status="review" with real, still-open PRs neither ReconcilePRPending nor any other reconciler was polling.

Excludes items with an active (EndedAt still nil) work or review session: recovery must never steal an item out from under a live, still-legitimately -running session — mirrors AutoReopenForPRFix's/AutoRespawnReview's identical hasActiveWorkSession/hasActiveReviewSession guard. An item with a genuinely active session will naturally reappear in this query once that session ends without making further progress.

func (*EntRepository) FindOpenStuckStates added in v1.38.0

func (r *EntRepository) FindOpenStuckStates(ctx context.Context) ([]OpenStuckStateData, error)

FindOpenStuckStates returns every BacklogStuckState row that is currently open (resolved_at IS NULL) and not currently snoozed (snoozed_until IS NULL OR snoozed_until is in the past), eager-loading the parent item so the projection carries title/status/pr_number/pr_url for rendering without a second round trip.

func (*EntRepository) FindPRPendingItems added in v1.37.0

func (r *EntRepository) FindPRPendingItems(ctx context.Context) ([]*ent.BacklogItem, error)

FindPRPendingItems returns backlog items in "pr_pending" status that have a PR number set. Used by ReconcilePRPending to poll for merged PRs.

func (*EntRepository) FindReviewItemsWithUnprocessedVerdict added in v1.39.0

func (r *EntRepository) FindReviewItemsWithUnprocessedVerdict(ctx context.Context) ([]*ent.BacklogItem, error)

FindReviewItemsWithUnprocessedVerdict returns backlog items in "review" status whose most recent review-role ItemSession already has a terminal ReviewVerdict recorded. Distinct from FindZombieReviewItems: that detector requires EVERY open review-or-work session on the item to be confirmed dead before acting, but AutoReopenAfterFailedReview's live-session-reuse (a work session intentionally stays open polling for the verdict once the item is back in "review" — see docs/tasks/backlog-feature-improvement.md's "WIP limit now undercounts live sessions" finding) means the item never looks like a full zombie even when the review session itself died with its verdict never actioned (handleReviewSessionExited never fired — a server restart or crash mid-exit, the same class of gap as the crash-resilience fixes elsewhere in this package). Each returned item eager-loads its review-role sessions (most recent first) with their ReviewVerdict, so the caller can act on the newest one without a second round-trip.

func (*EntRepository) FindReviewItemsWithoutGate added in v1.37.0

func (r *EntRepository) FindReviewItemsWithoutGate(ctx context.Context) ([]*ent.BacklogItem, error)

FindReviewItemsWithoutGate returns backlog items in "review" status that have no review ItemSession. These are items where the review gate was never spawned (e.g. the headless pool was unavailable at the time of the work session exit). Each returned item has its ItemSessions edge loaded (work sessions only).

func (*EntRepository) FindStuckReviewItems added in v1.38.0

func (r *EntRepository) FindStuckReviewItems(ctx context.Context) ([]*ent.BacklogItem, error)

FindStuckReviewItems returns backlog items in "review" status that already have at least one review ItemSession (so FindReviewItemsWithoutGate's "no gate at all" filter won't catch them) but currently have no active (EndedAt still nil) review or work session — i.e. nothing is in flight for the item, yet it never resolved to done/in_progress/pr_pending.

This is the class of item left behind when AutoReopenAfterFailedReview's spawn attempt failed and rolled the status back to "review" (e.g. blocked by hasActiveWorkSession because the prior work session's underlying tmux/CLI session never got marked ended), or when a legacy/interactive review session exited without ever calling submit_review_verdict. Both cases leave the item permanently invisible to every other reconciler: FindReviewItemsWithoutGate excludes it (a review session does exist), and reconcileStaleWorkSessions only scans "in_progress" items. Found via manual QA against a live-data item stuck in review for 24+ hours with three UNVERIFIABLE re-review verdicts and only ever one work session on record.

func (*EntRepository) FindZombieReviewItems added in v1.38.0

func (r *EntRepository) FindZombieReviewItems(ctx context.Context) ([]*ent.BacklogItem, error)

FindZombieReviewItems returns backlog items in "review" status that have an active (EndedAt IS NULL) review-or-work ItemSession recorded in the DB — exactly the class FindStuckReviewItems excludes (its "nothing in flight" filter requires no un-ended session at all). Each returned item eager-loads only that active session so the caller can verify, via an injected liveness checker, whether the underlying tmux/CLI process the row claims is active has actually gone away (a zombie: the DB row looks live, the process is gone). Not every returned item is a zombie — the caller must still check liveness per active session.

func (*EntRepository) FinishSourceSync added in v1.35.0

func (r *EntRepository) FinishSourceSync(ctx context.Context, sourceID string, cursorAfter string, created, updated, skipped, errored int, startedAt, finishedAt time.Time) error

FinishSourceSync atomically advances an ItemSource's sync cursor/last_synced_at and records the SourceSyncEvent for a successful sync run. Wrapping both writes in one transaction prevents a crash between them from leaving the cursor advanced with no corresponding history row — which would silently hide the fact that a batch of items was processed (or dropped) in that run.

func (*EntRepository) Get

func (r *EntRepository) Get(ctx context.Context, title string) (*InstanceData, error)

Get retrieves a single session by title

func (*EntRepository) GetAllItemSessionsWithBacklogInfo added in v1.37.0

func (r *EntRepository) GetAllItemSessionsWithBacklogInfo(ctx context.Context) ([]ItemSessionBacklogEntry, error)

GetAllItemSessionsWithBacklogInfo returns all item sessions joined with their parent backlog item's ID, title, and status. Used by the Insights dashboard index.

func (*EntRepository) GetAllSessionArtifacts added in v1.35.0

func (r *EntRepository) GetAllSessionArtifacts(ctx context.Context) (map[string]string, error)

GetAllSessionArtifacts returns a map of title → raw artifacts JSON for all sessions that have a non-empty session_artifacts column. Single query replaces N per-session queries in LoadInstances (M-4 fix).

func (*EntRepository) GetBacklogItem added in v1.35.0

func (r *EntRepository) GetBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)

GetBacklogItem retrieves a backlog item by UUID string.

func (*EntRepository) GetBacklogItemByExternalID added in v1.35.0

func (r *EntRepository) GetBacklogItemByExternalID(ctx context.Context, sourceID, externalID string) (*ent.BacklogItem, error)

GetBacklogItemByExternalID retrieves a BacklogItem by its external_id, scoped to sourceID. External IDs (e.g. GitHub issue/PR numbers) are only unique within their source, not globally — two different repos can both have an issue #1, so this must never match across sources.

func (*EntRepository) GetBacklogItemsByExternalIDs added in v1.41.0

func (r *EntRepository) GetBacklogItemsByExternalIDs(ctx context.Context, sourceID string, externalIDs []string) (map[string]*ent.BacklogItem, error)

GetBacklogItemsByExternalIDs batches GetBacklogItemByExternalID's lookup across many external IDs at once (a single IN query rather than one query per ID), scoped to sourceID the same way. Used by SyncLoop.PreviewBackwardSyncImpact, which previously issued one GetBacklogItemByExternalID call per closed issue in a loop — an N+1 query pattern against an index that isn't composite with the source FK. Returns a map keyed by external_id; IDs with no matching local item are simply absent from the map (not an error), matching the "not locally-imported, exclude it" semantics the per-item lookup had.

func (*EntRepository) GetBaseCommitSHAsForSessions added in v1.37.0

func (r *EntRepository) GetBaseCommitSHAsForSessions(ctx context.Context, sessionUUIDs []string) (map[string]string, error)

GetBaseCommitSHAsForSessions returns a map of sessionUUID → base_commit_sha for the given session UUIDs, including only rows with a non-empty value. Used at startup to restore dirBaseSHA for directory-mode backlog sessions — the counterpart to the SetDirBaseSHA call at spawn.

Reads base_commit_sha, falling back to last_commit_sha only for rows written before the two were split. That fallback is safe precisely because the bug being fixed meant the two fields held the same value on every legacy row; it must NOT be extended to rows that have a base_commit_sha, since last_commit_sha is now live-refreshed to the session's tip and would give a moving diff base.

func (*EntRepository) GetClaudeConversationUUIDBySessionUUID added in v1.37.0

func (r *EntRepository) GetClaudeConversationUUIDBySessionUUID(ctx context.Context, sessionUUID string) (string, error)

GetClaudeConversationUUIDBySessionUUID returns the Claude conversation UUID for the session whose title (tmux session name) matches sessionUUID. Returns "" if the session has no associated ClaudeSession.

func (*EntRepository) GetEntClient added in v1.35.0

func (r *EntRepository) GetEntClient() *ent.Client

GetEntClient returns the underlying *ent.Client so callers (e.g. ErrorRegistry) can operate on entities not managed by the Repository interface.

func (*EntRepository) GetItemSession added in v1.35.0

func (r *EntRepository) GetItemSession(ctx context.Context, id string) (ItemSessionSummary, error)

GetItemSession retrieves an ItemSession by entity UUID string. Loads the BacklogItem edge.

func (*EntRepository) GetItemSessionBySessionAndItem added in v1.35.0

func (r *EntRepository) GetItemSessionBySessionAndItem(ctx context.Context, sessionUUID string, itemID string) (ItemSessionSummary, error)

GetItemSessionBySessionAndItem looks up an ItemSession by both sessionUUID and backlog item ID.

func (*EntRepository) GetItemSessionBySessionUUID added in v1.35.0

func (r *EntRepository) GetItemSessionBySessionUUID(ctx context.Context, sessionUUID string) (ItemSessionSummary, error)

GetItemSessionBySessionUUID looks up the most recent active ItemSession by session UUID alone. session_uuid is not unique across records (a session may be reused), so we order by created_at descending and take the first match. Returns ErrNotFound if no record exists. Loads the BacklogItem edge so BacklogItemID is populated in the returned summary.

func (*EntRepository) GetItemSourceByID added in v1.35.0

func (r *EntRepository) GetItemSourceByID(ctx context.Context, id string) (*ent.ItemSource, error)

GetItemSourceByID retrieves a raw *ent.ItemSource by UUID string.

func (*EntRepository) GetMostRecentReviewVerdictForItem added in v1.35.0

func (r *EntRepository) GetMostRecentReviewVerdictForItem(ctx context.Context, itemID string) (ReviewOutcome, error)

GetMostRecentReviewVerdictForItem returns the OverallOutcome from the most recently created ReviewVerdict associated with any ItemSession for the given BacklogItem UUID. Returns "" (not an error) when no verdict exists yet.

func (*EntRepository) GetMostRecentStatusEventAt added in v1.38.0

func (r *EntRepository) GetMostRecentStatusEventAt(ctx context.Context, itemID string, toStatus BacklogStatus) (time.Time, bool, error)

GetMostRecentStatusEventAt returns the created_at timestamp of the most recent BacklogStatusEvent for itemID whose to_status equals toStatus. Returns (zero time, false, nil) when no such event exists (e.g. an item seeded directly into a status without a recorded transition). Used by the abandoned_review 15-minute grace check (abandonedReview pure fn) so a item that JUST entered review isn't flagged before the reconciler has had a chance to re-spawn a review gate.

func (*EntRepository) GetRecentReviewVerdictSummaries added in v1.39.0

func (r *EntRepository) GetRecentReviewVerdictSummaries(ctx context.Context, itemID string, limit int) ([]ReviewVerdictSummary, error)

GetRecentReviewVerdictSummaries returns up to limit ReviewVerdicts for the given BacklogItem UUID, most recent first. Reuses the existing ReviewVerdictSummary DTO (see repository.go) — only OverallOutcome, Summary, and DiffHash are populated, since that's all callers (IsRepeatedFailure and IsFlakyVerdictFlipFlop in stuck_decisions.go) need.

func (*EntRepository) GetRepoPathAndLatestCompletedWorkSessionCommits added in v1.42.0

func (r *EntRepository) GetRepoPathAndLatestCompletedWorkSessionCommits(ctx context.Context, itemID string) (repoPath, baseSHA, headSHA string, err error)

GetRepoPathAndLatestCompletedWorkSessionCommits returns itemID's RepoPath plus the Base/LastCommitSha of its most recent completed (session_role == work, ended_at set) ItemSession — the minimal data Storage.ComputeCurrentDiffHash needs. Unlike GetBacklogItem (which also eager-loads StatusEvents/ProgressNotes) and ListItemSessions (unbounded, eager-loads ReviewVerdict for every session), this pushes the "most recent completed work session" filter/order/limit into SQL — two bounded, no-edge queries regardless of how many sessions/events/notes the item has accumulated, rather than O(sessions_for_item) work on every review-verdict save. baseSHA/headSHA are both "" (no error) when the item has no completed work session yet.

func (*EntRepository) GetSession

func (r *EntRepository) GetSession(ctx context.Context, title string, opts ContextOptions) (*Session, error)

GetSession retrieves a session using the new Session domain model. Stub: not yet implemented; use InstanceData-based methods instead.

func (*EntRepository) GetSessionArtifacts added in v1.35.0

func (r *EntRepository) GetSessionArtifacts(ctx context.Context, title string) (string, error)

GetSessionArtifacts loads the raw JSON artifact blob for a session. Returns ("", nil) if the session exists but has no artifacts stored yet.

func (*EntRepository) GetSubcommandBreakdown added in v1.35.0

func (r *EntRepository) GetSubcommandBreakdown(ctx context.Context, program string, since time.Time) ([]SubcommandDecisionCount, error)

func (*EntRepository) GetSubcommandTrend added in v1.35.0

func (r *EntRepository) GetSubcommandTrend(ctx context.Context, program, subcommand string, since time.Time) ([]AnalyticsData, error)

func (*EntRepository) GetWithOptions

func (r *EntRepository) GetWithOptions(ctx context.Context, title string, options LoadOptions) (*InstanceData, error)

GetWithOptions retrieves a single session with selective child data loading.

func (*EntRepository) GetWorktreeDataBySessionUUID added in v1.37.0

func (r *EntRepository) GetWorktreeDataBySessionUUID(ctx context.Context, sessionUUID string) (GitWorktreeData, error)

GetWorktreeDataBySessionUUID returns the git worktree data for the Session with the given UUID. Returns an empty GitWorktreeData (no error) if the session does not exist or is a directory-mode session without a dedicated worktree.

func (*EntRepository) List

func (r *EntRepository) List(ctx context.Context) ([]InstanceData, error)

List retrieves all sessions from the database

func (*EntRepository) ListAnalytics added in v1.12.0

func (r *EntRepository) ListAnalytics(ctx context.Context, limit int) ([]AnalyticsData, error)

func (*EntRepository) ListAnalyticsByProgramSince added in v1.35.0

func (r *EntRepository) ListAnalyticsByProgramSince(ctx context.Context, program string, since time.Time, limit int) ([]AnalyticsData, error)

func (*EntRepository) ListAnalyticsSince added in v1.35.0

func (r *EntRepository) ListAnalyticsSince(ctx context.Context, since time.Time, limit int) ([]AnalyticsData, error)

func (*EntRepository) ListBacklogItemSummaries added in v1.37.0

func (r *EntRepository) ListBacklogItemSummaries(ctx context.Context, filter BacklogItemFilter) ([]BacklogItemSummary, error)

ListBacklogItemSummaries returns lightweight BacklogItemSummary values for list views. Three-phase: (1) scalar fields via .All(), (2) item sessions + review verdicts via edge loading.

func (*EntRepository) ListBacklogItems added in v1.35.0

func (r *EntRepository) ListBacklogItems(ctx context.Context, filter BacklogItemFilter) ([]BacklogItemData, error)

ListBacklogItems returns backlog items with optional filtering.

func (*EntRepository) ListByStatus

func (r *EntRepository) ListByStatus(ctx context.Context, status Status) ([]InstanceData, error)

ListByStatus retrieves sessions filtered by status

func (*EntRepository) ListByStatusWithOptions

func (r *EntRepository) ListByStatusWithOptions(ctx context.Context, status Status, options LoadOptions) ([]InstanceData, error)

ListByStatusWithOptions retrieves sessions filtered by status with selective loading.

func (*EntRepository) ListByTag

func (r *EntRepository) ListByTag(ctx context.Context, tagName string) ([]InstanceData, error)

ListByTag retrieves sessions that have a specific tag

func (*EntRepository) ListByTagWithOptions

func (r *EntRepository) ListByTagWithOptions(ctx context.Context, tagName string, options LoadOptions) ([]InstanceData, error)

ListByTagWithOptions retrieves sessions with a specific tag with selective loading.

func (*EntRepository) ListItemSessions added in v1.35.0

func (r *EntRepository) ListItemSessions(ctx context.Context, itemID string) ([]ItemSessionSummary, error)

ListItemSessions returns all ItemSessions for a given BacklogItem UUID string.

func (*EntRepository) ListItemSources added in v1.35.0

func (r *EntRepository) ListItemSources(ctx context.Context) ([]ItemSourceData, error)

ListItemSources returns all registered item sources.

func (*EntRepository) ListProgressNotesForItem added in v1.38.0

func (r *EntRepository) ListProgressNotesForItem(ctx context.Context, itemID string) ([]ProgressNoteData, error)

ListProgressNotesForItem returns the full append-only history of report_progress calls for a backlog item, ordered by created_at ascending (oldest first).

func (*EntRepository) ListProjects added in v1.23.0

func (r *EntRepository) ListProjects(ctx context.Context) ([]ProjectData, error)

ListProjects returns all projects.

func (*EntRepository) ListRecentCommandsByProgram added in v1.35.0

func (r *EntRepository) ListRecentCommandsByProgram(ctx context.Context, program, subcommand string, since time.Time, n int) ([]string, error)

func (*EntRepository) ListSessions

func (r *EntRepository) ListSessions(ctx context.Context, opts ContextOptions) ([]*Session, error)

ListSessions retrieves all sessions using the new Session domain model. Stub: not yet implemented; use InstanceData-based methods instead.

func (*EntRepository) ListShells added in v1.35.0

func (r *EntRepository) ListShells(ctx context.Context, sessionTitle string) ([]*ent.Shell, error)

ListShells returns all shells for the given session title, ordered by order_index.

func (*EntRepository) ListSourceSyncEvents added in v1.35.0

func (r *EntRepository) ListSourceSyncEvents(ctx context.Context, sourceID string) ([]SourceSyncEventData, bool, error)

ListSourceSyncEvents returns sync history events for an item source, most recent first, capped at maxSourceSyncEventsHistory rows. truncated is true when older events exist beyond the cap — callers should surface this to avoid silently hiding history for sources with long or frequent sync runs.

func (*EntRepository) ListWithOptions

func (r *EntRepository) ListWithOptions(ctx context.Context, options LoadOptions) ([]InstanceData, error)

ListWithOptions retrieves all sessions with selective child data loading.

func (*EntRepository) MarkStuck added in v1.38.0

func (r *EntRepository) MarkStuck(ctx context.Context, itemID string, reason domain.StuckReason, expectedStatus BacklogStatus, stuckContext string) (applied bool, err error)

MarkStuck opens, refreshes, or reopens a durable BacklogStuckState row for the given item + reason via a resolve-in-place upsert on the (item_id, reason) unique index — there is exactly one row per pair at all times.

A best-effort item-status precondition is applied before writing: if the item's current status does not equal expectedStatus, MarkStuck returns (false, nil) without writing. This precondition is NOT atomic with the write itself (a concurrent transition can still race in between); the self-heal sweep (reconcile pipeline, Phase 2) is the correctness backstop for any stale write that still lands.

Row semantics on conflict with an existing (item_id, reason) row:

  • OPEN row (resolved_at IS NULL): only last_checked_at and context are refreshed. first_detected_at and notified_at are left untouched, so notify-once dedup and the "stuck for N" duration both survive repeated ticks.
  • RESOLVED row (resolved_at IS NOT NULL): the SAME row is reopened in place — resolved_at and notified_at are cleared and first_detected_at is reset to now — never a second row for the same pair.

Implementation note: this is two atomic statements (an INSERT ... ON CONFLICT upsert, then a conditional UPDATE ... WHERE resolved_at IS NOT NULL) inside one DB transaction, rather than a single raw SQL statement. Ent's generated upsert Update() callback has no portable way to express a per-row CASE WHEN keyed off the pre-existing resolved_at value without hand-written dialect-specific SQL, so the reopen adjustment is split into its own atomic, idempotent conditional UPDATE. Row-dedup itself — the concurrency-sensitive part — is still guaranteed by the single upsert statement; there is no read-then-write for detecting whether the row exists.

func (*EntRepository) MarkStuckNotified added in v1.38.0

func (r *EntRepository) MarkStuckNotified(ctx context.Context, itemID string, reason domain.StuckReason) (bool, error)

MarkStuckNotified sets notified_at=now on an open, not-yet-notified stuck row — the durable notify-once dedup write, called once after a stuck notification has actually been sent. A no-op (not an error) if the row is already notified or doesn't exist.

func (*EntRepository) ReconcileStuckItems added in v1.35.0

func (r *EntRepository) ReconcileStuckItems(ctx context.Context) (int, error)

ReconcileStuckItems finds in_progress items whose all linked ItemSessions have ended, and transitions them to review status. Returns the count of transitioned items. All updates are wrapped in a single transaction so they succeed or fail atomically.

func (*EntRepository) RecordAnalytics added in v1.12.0

func (r *EntRepository) RecordAnalytics(ctx context.Context, data AnalyticsData) error

func (*EntRepository) RecordRemediationAttempt added in v1.39.0

func (r *EntRepository) RecordRemediationAttempt(ctx context.Context, itemID string, reason domain.StuckReason, attempts int32, nextAt *time.Time) (bool, error)

RecordRemediationAttempt records that an automated (or operator-triggered, see TriggerRemediationNow) remediation attempt was just made for an open (item_id, reason) row: sets remediation_attempts to attempts and next_remediation_at to nextAt (nil once attempts has hit the cap — see nextRemediationAt in backlog_remediation.go). Callers compute attempts/nextAt themselves (via the shared backoff gate) rather than this method incrementing in place, so a single code path (evaluateRemediation) owns the backoff-schedule arithmetic. Scoped to WHERE resolved_at IS NULL, matching every other stuck-state write in this file — a row that resolved between the gate's read and this write is left alone rather than resurrected.

func (*EntRepository) RecordRemediationRestartGrace added in v1.39.0

func (r *EntRepository) RecordRemediationRestartGrace(ctx context.Context, itemID string, reason domain.StuckReason, bootTime time.Time) (bool, error)

RecordRemediationRestartGrace records that itemID/reason's open row just consumed its one-per-boot restart-grace pass (see evaluateRemediation): sets grace_boot_time to bootTime WITHOUT touching remediation_attempts or next_remediation_at — a grace pass lets the wrapped remediation action run without spending any of the row's 5-attempt budget.

func (*EntRepository) RecordSourceSyncFailure added in v1.41.0

func (r *EntRepository) RecordSourceSyncFailure(ctx context.Context, sourceID string, message string) error

RecordSourceSyncFailure persists a zero-item sync-history row recording a forward-sync failure (e.g. the forward-sync EventBus subscriber's CloseIssue call erroring — see server/services/backlog_github_forward_sync.go), so the failure is queryable via ListSourceSyncEvents / the Settings UI's row-level warning (Story 4.3.2) instead of only appearing in server logs. Mirrors CreateSourceSyncEvent's error-message convention used by SyncOne's own fetch-failure path, but for the write direction (forward sync) rather than the read direction (Fetch).

func (*EntRepository) ResetStuckRemediation added in v1.39.0

func (r *EntRepository) ResetStuckRemediation(ctx context.Context, itemID string, reason domain.StuckReason) (bool, error)

ResetStuckRemediation clears the automated-remediation counters on a single open (item_id, reason) row: remediation_attempts back to 0, next_remediation_at and notified_at cleared. Clearing notified_at (in addition to the remediation counters) lets a fresh notify+respawn cycle fire on the very next detector tick instead of waiting on stale dedup state — the same reasoning as MarkStuck's reopen-in-place path. A no-op (false, nil), not an error, when no open row matches (item_id, reason).

func (*EntRepository) ResolveStuck added in v1.38.0

func (r *EntRepository) ResolveStuck(ctx context.Context, itemID string, reason domain.StuckReason) (bool, error)

ResolveStuck atomically, idempotently closes an open BacklogStuckState row via a single conditional UPDATE ... WHERE resolved_at IS NULL. Returns whether a row was actually resolved by this call; resolving an already-resolved or nonexistent (item_id, reason) row is a no-op, not an error, and never overwrites an existing resolved_at.

func (*EntRepository) RevertChainFireClaim added in v1.43.0

func (r *EntRepository) RevertChainFireClaim(ctx context.Context, id string) error

RevertChainFireClaim unconditionally resets chain_fired back to false — used by ChainFirer.Fire to release its claim after a subsequent FireTriggerChained attempt fails, so TriggerChainReconciler retries on its next tick. Safe to call unconditionally (no precondition): only the goroutine that just won ClaimChainFire's claim for this item can ever reach this call, so nothing else can be racing this specific revert.

func (*EntRepository) SaveReviewVerdict added in v1.35.0

func (r *EntRepository) SaveReviewVerdict(ctx context.Context, itemSessionID string, verdict ReviewVerdictData) error

SaveReviewVerdict upserts a ReviewVerdict for a given ItemSession. The query-then-create/update is wrapped in a transaction to prevent a check-then-act race condition when concurrent callers save verdicts for the same item session.

func (*EntRepository) SetCallbackDispatcher added in v1.43.0

func (r *EntRepository) SetCallbackDispatcher(d CallbackDispatcher)

SetCallbackDispatcher wires a CallbackDispatcher into this repository so TransitionBacklogItemStatus (on_session_complete) and BacklogLifecycleListener.reconcileStaleWorkSessions (on_session_stale, which is handed this *EntRepository directly) can fire outbound callbacks (webhook-triggers Phase 5). Called via Storage.SetCallbackDispatcher's forwarding method, the same pattern SetItemChangePublisher uses.

func (*EntRepository) SetChainFirer added in v1.43.0

func (r *EntRepository) SetChainFirer(f *ChainFirer)

SetChainFirer wires a ChainFirer into this repository so TransitionBacklogItemStatus can dispatch the pipeline-chain fire immediately after a "done" transition commits (webhook-triggers Phase 6, AC5/AC9). Called via Storage.WireChainFirer's forwarding call in server/dependencies.go, the same pattern SetCallbackDispatcher uses.

func (*EntRepository) SetItemChangePublisher added in v1.41.0

func (r *EntRepository) SetItemChangePublisher(p ItemChangePublisher)

SetItemChangePublisher wires an ItemChangePublisher into this repository so its backlog mutation methods (TransitionBacklogItemStatus, UpdateBacklogItem, ArchiveBacklogItem, DeleteBacklogItem, SaveReviewVerdict, CreateItemSessionWithVerdict, CreateItemSession, UpdateItemSessionSessionUUID, UpdateItemSessionTriageResult) can publish a best-effort change notification after each successful mutation. Called via Storage.SetItemChangePublisher's forwarding method (session/storage.go), which is the only entry point server/dependencies.go has since it holds a *Storage, not a concrete *EntRepository.

func (*EntRepository) SetItemSessionBaseCommit added in v1.41.0

func (r *EntRepository) SetItemSessionBaseCommit(ctx context.Context, id, sha string) error

SetItemSessionBaseCommit records the worktree's pre-work HEAD SHA for the item session, so the review gate can diff base..HEAD across every commit the agent makes rather than just HEAD~1..HEAD.

This is deliberately NOT UpdateItemSessionGitActivity: that function's fields mean "the session's latest commit", and seeding them with the spawn-time base SHA is what let closeIfSupersededByMain close real, unmerged PRs as "superseded" — a branch's own base commit is by construction already an ancestor of main, so IsCommitOnMain on it is always true (BUG-047).

func (*EntRepository) SnoozeStuckState added in v1.38.0

func (r *EntRepository) SnoozeStuckState(ctx context.Context, itemID string, reason domain.StuckReason, until time.Time) (bool, error)

SnoozeStuckState sets snoozed_until on an open BacklogStuckState row via a single atomic conditional UPDATE ... WHERE resolved_at IS NULL, matching the ResolveStuck pattern. Returns whether a row was actually updated by this call; snoozing a nonexistent or already-resolved (item_id, reason) row is a no-op, not an error. A snoozed-until-past-now value simply un-snoozes the row on the next FindOpenStuckStates read (its predicate is snoozed_until IS NULL OR snoozed_until < now).

func (*EntRepository) TransitionBacklogItemStatus added in v1.35.0

func (r *EntRepository) TransitionBacklogItemStatus(ctx context.Context, id string, toStatus BacklogStatus, precondition *BacklogItemPrecondition, triggeredBy string) (*BacklogItemData, error)

TransitionBacklogItemStatus changes the status of a backlog item with optional precondition.

The precondition is enforced as a genuine SQL-level compare-and-swap: it is folded into the UPDATE statement's own WHERE clause (status = ? AND updated_at = ?, via the bulk Update().Where(...) builder — not UpdateOneID, which cannot scope beyond id) rather than checked against a separately-fetched row beforehand. Save() on a bulk update returns the affected row count instead of the updated entity, so the row is re-fetched after a successful write.

The previous implementation did Get() → check-in-Go → UpdateOneID().Save(), a read-then-write race: nothing stopped a second, concurrent caller's write from landing in the gap between this call's read and its write, so a precondition that was true at read time could be false (and silently ignored) by write time. Two concrete incidents motivated closing this: a stale AutoReopenAfterFailedReview call reopened an item that had, in the meantime, already legitimately shipped to "done" (see docs/bugs/fixed/BUG-026-backlog-transition-status-toctou-reopen.md, live 2026-07-20 repro, item 0fd4a940, PR #176), and the backlog work-item queue feature's concurrent-dequeue-claim test found the same race could double-claim a single queued item between two dequeue sweeps (PR #199).

func (*EntRepository) TransitionBacklogItemStatusWithPRFields added in v1.41.0

func (r *EntRepository) TransitionBacklogItemStatusWithPRFields(ctx context.Context, id string, toStatus BacklogStatus, prURL string, prNumber int, precondition *BacklogItemPrecondition, triggeredBy string) (*BacklogItemData, error)

TransitionBacklogItemStatusWithPRFields atomically transitions a backlog item's status while also persisting its PrURL/PrNumber, as a single UPDATE ... WHERE statement — the same CAS precondition guards both writes together, so a reader can never observe the status having changed without the PR fields already being set, or vice versa.

This exists to close a narrower race than the one Storage.SetBacklogItemPRAndTransition originally had: an earlier fix reordered that function to run the status transition (review -> pr_pending) before a separate PrURL/PrNumber field write, which fixed the lost-update bug (two racing callers clobbering each other's PR number) but left a smaller gap — between the transition committing and the field write committing, a concurrent reader could observe status=pr_pending with PrNumber==0. That exact shape is what the pr_pending_no_pr / BUG-040 stuck detector (reconcilePRPendingWithoutPRItems, session/backlog_lifecycle.go) exists to flag as a HIGH-priority, non-auto-recoverable alert, and its resolution condition (selfHealStuck) is anchored on the item leaving pr_pending entirely — so a reconcile tick landing in that multi-millisecond window could raise a spurious stuck alert that stays open for days on a perfectly healthy item. Folding both writes into one atomic UPDATE removes the window rather than narrowing it.

Not part of the Repository interface — Storage type-asserts s.repo to *EntRepository to call this, the same pattern already used for ListItemSessions (see server/mcp/tools_backlog.go's listItemSessionsFn doc comment): EntRepository has no second real implementation to abstract this over, so adding it to the interface would be pure speculation (see .claude/rules/interface-pollution-checklist.md).

func (*EntRepository) UnarchiveBacklogItem added in v1.44.0

func (r *EntRepository) UnarchiveBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)

UnarchiveBacklogItem clears archived_at and restores the item to the "idea" status — the sole valid archived-> reopen transition. It does not attempt to restore whatever status the item held before archiving (no history-based restoration): the item simply re-enters the idea column needing a fresh session, matching UnarchiveSession's identical unconditional-flip precedent (server/services/session_service.go) rather than erroring or no-op'ing on an already-non-archived item.

func (*EntRepository) UnresolvedBlockerIDs added in v1.43.0

func (r *EntRepository) UnresolvedBlockerIDs(ctx context.Context, itemID string) ([]string, error)

UnresolvedBlockerIDs returns the blocker item IDs still unresolved for a single blocked item, for building a human-readable stuck-reason message. Unlike UnresolvedBlockerItemIDs (batched presence check across many candidates), this returns which specific items are doing the blocking.

func (*EntRepository) UnresolvedBlockerItemIDs added in v1.43.0

func (r *EntRepository) UnresolvedBlockerItemIDs(ctx context.Context, itemIDs []string) (map[string]bool, error)

UnresolvedBlockerItemIDs returns, for the given candidate itemIDs, the subset that have at least one dependency edge whose blocker has not yet reached a resolved status. A blocker counts as resolved once it reaches BacklogStatusDone (shipped) or BacklogStatusArchived (won't ship) — an archived blocker is never coming back to "done", so treating it as still blocking would permanently strand its dependent. Batches the check into a single query (rather than one per candidate) to avoid N+1 queries in dequeue paths.

func (*EntRepository) Update

func (r *EntRepository) Update(ctx context.Context, data InstanceData) error

Update modifies an existing session in the database

func (*EntRepository) UpdateAcCriterionStatus added in v1.35.0

func (r *EntRepository) UpdateAcCriterionStatus(ctx context.Context, itemID string, criterionIndex int, status string, note string) error

UpdateAcCriterionStatus updates a single acceptance criterion's status by index.

func (*EntRepository) UpdateBacklogItem added in v1.35.0

func (r *EntRepository) UpdateBacklogItem(ctx context.Context, id string, update BacklogItemUpdate, precondition *BacklogItemPrecondition) (*BacklogItemData, error)

UpdateBacklogItem modifies an existing backlog item with optional precondition check.

func (*EntRepository) UpdateGitHubPRNumber added in v1.35.0

func (r *EntRepository) UpdateGitHubPRNumber(ctx context.Context, title string, prNumber int) error

UpdateGitHubPRNumber persists a discovered PR number for a session. Called by PRStatusPoller when it auto-discovers a PR for a branch-based session.

func (*EntRepository) UpdateItemSessionEnded added in v1.35.0

func (r *EntRepository) UpdateItemSessionEnded(ctx context.Context, id string, endedAt time.Time) error

UpdateItemSessionEnded records the end time for an ItemSession.

func (*EntRepository) UpdateItemSessionEndedWithReason added in v1.41.0

func (r *EntRepository) UpdateItemSessionEndedWithReason(ctx context.Context, id string, endedAt time.Time, reason string) error

UpdateItemSessionEndedWithReason records the end time for an ItemSession alongside classifyHeadlessCallError's bucket (or "" for a successful end) — see the end_reason schema comment for why this exists: it lets orphan-recovery sweeps tell a call killed by our own graceful shutdown apart from one that failed on its own merits.

func (*EntRepository) UpdateItemSessionFailureCapture added in v1.42.0

func (r *EntRepository) UpdateItemSessionFailureCapture(ctx context.Context, id string, path string) error

UpdateItemSessionFailureCapture records the absolute path to a durable raw-output capture file (session.WriteHeadlessFailureCapture) for a headless triage/review call that errored or produced unparseable output — see the failure_capture_path schema comment. Set independently of UpdateItemSessionEndedWithReason (a separate, orthogonal column) so a caller that already wrote the capture file to disk can record its path without re-specifying ended_at/end_reason.

func (*EntRepository) UpdateItemSessionFileTouch added in v1.35.0

func (r *EntRepository) UpdateItemSessionFileTouch(ctx context.Context, id string, touchAt time.Time) error

UpdateItemSessionFileTouch updates the last file touch timestamp on an ItemSession.

func (*EntRepository) UpdateItemSessionGitActivity added in v1.35.0

func (r *EntRepository) UpdateItemSessionGitActivity(ctx context.Context, id string, sha, msg string, commitAt time.Time, commitCount int) error

UpdateItemSessionGitActivity records the item session's *current* tip commit and the count of commits it has authored since its base. Called repeatedly by refreshWorkSessionGitActivity (session/backlog_lifecycle.go) while the session is active, so downstream "has this session's work landed on main?" checks read live data. To record the spawn-time baseline instead, use SetItemSessionBaseCommit.

func (*EntRepository) UpdateItemSessionSessionUUID added in v1.35.0

func (r *EntRepository) UpdateItemSessionSessionUUID(ctx context.Context, id string, sessionUUID string) error

UpdateItemSessionSessionUUID updates the session_uuid field on an existing ItemSession.

func (*EntRepository) UpdateItemSessionStarted added in v1.35.0

func (r *EntRepository) UpdateItemSessionStarted(ctx context.Context, id string, startedAt time.Time) error

UpdateItemSessionStarted records the start time for an ItemSession.

func (*EntRepository) UpdateItemSessionTriageResult added in v1.35.0

func (r *EntRepository) UpdateItemSessionTriageResult(ctx context.Context, id string, triageResult string) error

UpdateItemSessionTriageResult stores the triage result JSON payload on an ItemSession.

func (*EntRepository) UpdateItemSessionVerificationNotes added in v1.37.0

func (r *EntRepository) UpdateItemSessionVerificationNotes(ctx context.Context, id string, verificationNotes string) error

UpdateItemSessionVerificationNotes stores the verification evidence reported via request_review (commands run, manual checks performed) on an ItemSession.

func (*EntRepository) UpdateItemSource added in v1.35.0

func (r *EntRepository) UpdateItemSource(ctx context.Context, id string, update ItemSourceUpdate) (*ItemSourceData, error)

UpdateItemSource modifies an existing item source.

func (*EntRepository) UpdateLastAcknowledged added in v1.35.0

func (r *EntRepository) UpdateLastAcknowledged(ctx context.Context, title string, t time.Time) error

UpdateLastAcknowledged sets only the last_acknowledged field for a session, issuing a single UPDATE WHERE title=? without a prior SELECT.

func (*EntRepository) UpdateLastAddedToQueue added in v1.35.0

func (r *EntRepository) UpdateLastAddedToQueue(ctx context.Context, title string, t time.Time) error

UpdateLastAddedToQueue sets only the last_added_to_queue field for a session, issuing a single UPDATE WHERE title=? without a prior SELECT.

func (*EntRepository) UpdateLastViewed added in v1.35.0

func (r *EntRepository) UpdateLastViewed(ctx context.Context, title string, t time.Time) error

UpdateLastViewed sets only the last_viewed field for a session, issuing a single UPDATE WHERE title=? without a prior SELECT.

func (*EntRepository) UpdateProject added in v1.23.0

func (r *EntRepository) UpdateProject(ctx context.Context, data ProjectData) (*ProjectData, error)

UpdateProject modifies an existing project.

func (*EntRepository) UpdateReviewQueueState added in v1.35.0

func (r *EntRepository) UpdateReviewQueueState(ctx context.Context, title string, lastUserResponse, processingGraceUntil, lastPromptDetected time.Time, lastPromptSignature string) error

UpdateReviewQueueState efficiently updates only the review-queue interaction fields for a session, avoiding the full read-modify-write cycle of updateFieldInRepo.

func (*EntRepository) UpdateSession

func (r *EntRepository) UpdateSession(ctx context.Context, session *Session) error

UpdateSession updates an existing session using the Session domain model. Stub: not yet implemented; use InstanceData-based methods instead.

func (*EntRepository) UpdateSessionArtifacts added in v1.35.0

func (r *EntRepository) UpdateSessionArtifacts(ctx context.Context, title string, blob string) error

UpdateSessionArtifacts persists the JSON-encoded artifact blob for a session. Wrapped in a transaction for correctness under concurrent writes (M-6 fix). The per-title mutex in ArtifactExtractor (C-1) serializes calls at the application layer; the transaction is belt-and-suspenders for correctness.

func (*EntRepository) UpdateSessionMetadata added in v1.42.0

func (r *EntRepository) UpdateSessionMetadata(ctx context.Context, currentTitle string, newTitle, category, note, workingDir *string) error

UpdateSessionMetadata efficiently updates only title/category/note/working_dir fields for a session, issuing a single UPDATE WHERE title=? without a prior SELECT and without the worktree/diffstats/tags/claude_session writes the full Update method performs — mirrors UpdateLastViewed's shape. currentTitle must be the row's title from BEFORE any rename already applied to the caller's in-memory Instance in this same request: Update looks the row up by data.Title (the post-rename value), which misses the still-old-titled DB row and falls into Update's Create fallback, orphaning it under the new title. Using currentTitle as the WHERE key avoids that. Category/WorkingDir are only set when non-nil AND non-empty, matching Update's existing guarded (`data.Category != ""`) semantics; Note is set whenever non-nil (including ""), since an empty note is a meaningful cleared state, not "unset" — same asymmetry as Update's unconditional SetNote(data.Note).

func (*EntRepository) UpdateShellStatus added in v1.35.0

func (r *EntRepository) UpdateShellStatus(ctx context.Context, shellID, status string, exitCode *int) error

UpdateShellStatus updates the status (and optionally exit code + stopped_at) for a shell.

func (*EntRepository) UpdateTimestamps

func (r *EntRepository) UpdateTimestamps(ctx context.Context, title string, lastTerminalUpdate, lastMeaningfulOutput time.Time, lastOutputSignature string) error

UpdateTimestamps efficiently updates only timestamp fields for a session

func (*EntRepository) UpsertRule added in v1.12.0

func (r *EntRepository) UpsertRule(ctx context.Context, data ApprovalRuleData) error

type EntTriggerFireEventRepository added in v1.43.0

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

EntTriggerFireEventRepository implements TriggerFireEventRepository using the ent ORM.

func NewEntTriggerFireEventRepository added in v1.43.0

func NewEntTriggerFireEventRepository(client *ent.Client) *EntTriggerFireEventRepository

NewEntTriggerFireEventRepository creates a new ent-backed TriggerFireEvent repository.

func (*EntTriggerFireEventRepository) Create added in v1.43.0

Create inserts a new TriggerFireEvent row.

func (*EntTriggerFireEventRepository) ListByWorkflow added in v1.43.0

func (r *EntTriggerFireEventRepository) ListByWorkflow(ctx context.Context, workflowID uuid.UUID, limit int) ([]*ent.TriggerFireEvent, error)

ListByWorkflow returns the most recent TriggerFireEvent rows for workflowID, newest first, capped at limit (a value <= 0 defaults to 100).

func (*EntTriggerFireEventRepository) UpdateOutcome added in v1.43.0

func (r *EntTriggerFireEventRepository) UpdateOutcome(ctx context.Context, workflowID uuid.UUID, deliveryID, outcome, sessionID, errMsg string) error

UpdateOutcome transitions the TriggerFireEvent row matching (workflowID, deliveryID) to outcome, optionally setting sessionID/errMsg.

type EntWorkflowRepository added in v1.35.0

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

EntWorkflowRepository implements WorkflowRepository using the ent ORM.

func NewEntWorkflowRepository added in v1.35.0

func NewEntWorkflowRepository(client *ent.Client) *EntWorkflowRepository

NewEntWorkflowRepository creates a new ent-backed workflow repository.

func (*EntWorkflowRepository) Create added in v1.35.0

Create inserts a new workflow definition. Returns ent.ConstraintError when a duplicate slug exists.

func (*EntWorkflowRepository) Delete added in v1.35.0

func (r *EntWorkflowRepository) Delete(ctx context.Context, id uuid.UUID) error

Delete removes a workflow by UUID.

func (*EntWorkflowRepository) GetByID added in v1.35.0

func (r *EntWorkflowRepository) GetByID(ctx context.Context, id uuid.UUID) (*ent.Workflow, error)

GetByID retrieves a workflow by UUID.

func (*EntWorkflowRepository) GetBySlug added in v1.35.0

func (r *EntWorkflowRepository) GetBySlug(ctx context.Context, slug string) (*ent.Workflow, error)

GetBySlug retrieves a workflow by slug.

func (*EntWorkflowRepository) GetByWebhookSlug added in v1.43.0

func (r *EntWorkflowRepository) GetByWebhookSlug(ctx context.Context, slug string) (*ent.Workflow, error)

GetByWebhookSlug retrieves a workflow by its webhook_slug.

func (*EntWorkflowRepository) ListAll added in v1.35.0

func (r *EntWorkflowRepository) ListAll(ctx context.Context) ([]*ent.Workflow, error)

ListAll returns all workflows sorted ascending by created_at. A safety cap of 1000 is applied to prevent runaway queries.

func (*EntWorkflowRepository) ListByTriggerType added in v1.43.0

func (r *EntWorkflowRepository) ListByTriggerType(ctx context.Context, triggerType string) ([]*ent.Workflow, error)

ListByTriggerType returns all workflows with the given trigger_type, regardless of cron_enabled (see interface doc comment for why enabled/repo/branch filtering is left to the caller).

func (*EntWorkflowRepository) ListEnabled added in v1.35.0

func (r *EntWorkflowRepository) ListEnabled(ctx context.Context) ([]*ent.Workflow, error)

ListEnabled returns only workflows where cron_enabled is true.

func (*EntWorkflowRepository) Update added in v1.35.0

Update applies a partial update to an existing workflow by UUID, unconditionally. Thin wrapper over UpdateConditional with the zero time.Time, which applies no updated_at precondition — kept as a separate method so existing single-writer callers don't need to thread an expectedUpdatedAt they don't have.

func (*EntWorkflowRepository) UpdateConditional added in v1.44.0

func (r *EntWorkflowRepository) UpdateConditional(ctx context.Context, id uuid.UUID, w WorkflowUpdateInput, expectedUpdatedAt time.Time) (*ent.Workflow, error)

UpdateConditional applies a partial update to an existing workflow by UUID, only if the row's current updated_at exactly matches expectedUpdatedAt — an optimistic- concurrency CAS. A zero expectedUpdatedAt applies no precondition (always writes), matching Update's unconditional behavior.

Built on WorkflowUpdateOne (UpdateOneID), not the bulk Update().Where() builder that an earlier version of this method used: UpdateOneID's Save() returns the mutated entity directly, computed inside the same UPDATE statement/transaction ent's generated sqlgraph.UpdateNode issues — one atomic round trip, matching this method's pre-CAS performance and read-your-own-write consistency exactly when expectedUpdatedAt is zero (the common case: every existing single-writer caller, plus the hot per-fire LastFiredAt bump in Scheduler). The bulk builder's Update().Where() only returns an affected-row count, forcing a second, separate, unguarded Get to reload the entity — which both doubles the round trips on every call AND lets a concurrent writer's update land in the gap between the CAS write and that reload, so the caller could receive someone else's state as if it were their own write's result. UpdateOneID.Where() supports the same predicate this needs (workflow.UpdatedAtEQ), so there's no capability lost by using it instead.

type ErrDuplicateTag

type ErrDuplicateTag struct {
	Tag string
}

ErrDuplicateTag is returned when adding a tag that already exists.

func (ErrDuplicateTag) Error

func (e ErrDuplicateTag) Error() string

type ErrInvalidTransition

type ErrInvalidTransition struct {
	From Status
	To   Status
}

ErrInvalidTransition is returned when a status transition is not allowed by the state machine defined in state_machine.go.

func (ErrInvalidTransition) Error

func (e ErrInvalidTransition) Error() string

type ErrTagTooLong

type ErrTagTooLong struct {
	Tag    string
	MaxLen int
}

ErrTagTooLong is returned when a tag exceeds the maximum length.

func (ErrTagTooLong) Error

func (e ErrTagTooLong) Error() string

type ErrTooManyTags added in v1.9.0

type ErrTooManyTags struct {
	Count    int
	MaxCount int
}

ErrTooManyTags is returned when setting more tags than MaxTagCount allows.

func (ErrTooManyTags) Error added in v1.9.0

func (e ErrTooManyTags) Error() string

type ExecutionOptions

type ExecutionOptions struct {
	// Timeout for command execution (0 = no timeout)
	Timeout time.Duration
	// MaxOutputSize limits captured output (0 = unlimited)
	MaxOutputSize int
	// StatusCheckInterval for polling status detector
	StatusCheckInterval time.Duration
	// TerminalStatuses are statuses that indicate command completion
	TerminalStatuses []detection.DetectedStatus
}

ExecutionOptions configures command execution behavior.

func DefaultExecutionOptions

func DefaultExecutionOptions() ExecutionOptions

DefaultExecutionOptions returns sensible defaults for command execution.

type ExecutionResult

type ExecutionResult struct {
	Command       *Command
	Success       bool
	Output        string
	Error         error
	StartTime     time.Time
	EndTime       time.Time
	FinalStatus   detection.DetectedStatus
	StatusChanges []StatusChange
}

ExecutionResult represents the result of a command execution.

type ExternalApprovalCallback

type ExternalApprovalCallback func(*ExternalApprovalEvent)

ExternalApprovalCallback is called when an approval is detected.

type ExternalApprovalEvent

type ExternalApprovalEvent struct {
	Request      *detection.ApprovalRequest
	SessionID    string // Socket path or unique identifier
	SessionTitle string
	Source       ExternalApprovalSource
	Cwd          string
	Command      string
}

ExternalApprovalEvent represents an approval detected in an external session.

type ExternalApprovalMonitor

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

ExternalApprovalMonitor monitors external sessions for approval requests.

func NewExternalApprovalMonitor

func NewExternalApprovalMonitor() *ExternalApprovalMonitor

NewExternalApprovalMonitor creates a new external approval monitor.

func (*ExternalApprovalMonitor) GetAllPendingApprovals

func (m *ExternalApprovalMonitor) GetAllPendingApprovals() map[string][]*detection.ApprovalRequest

GetAllPendingApprovals returns pending approvals across all monitored sessions.

func (*ExternalApprovalMonitor) GetDetector

GetDetector returns the underlying approval detector for configuration.

func (*ExternalApprovalMonitor) GetMonitoredSessions

func (m *ExternalApprovalMonitor) GetMonitoredSessions() []string

GetMonitoredSessions returns the socket paths of all monitored sessions.

func (*ExternalApprovalMonitor) GetPendingApprovals

func (m *ExternalApprovalMonitor) GetPendingApprovals(socketPath string) []*detection.ApprovalRequest

GetPendingApprovals returns all pending approval requests for a session.

func (*ExternalApprovalMonitor) IntegrateWithDiscovery

func (m *ExternalApprovalMonitor) IntegrateWithDiscovery(
	discovery *ExternalSessionDiscovery,
	streamerManager *ExternalStreamerManager,
)

IntegrateWithDiscovery connects the approval monitor to external session discovery. This auto-monitors new external sessions as they're discovered.

func (*ExternalApprovalMonitor) IntegrateWithDiscoveryTmux

func (m *ExternalApprovalMonitor) IntegrateWithDiscoveryTmux(
	discovery *ExternalSessionDiscovery,
	tmuxStreamerManager *ExternalTmuxStreamerManager,
)

IntegrateWithDiscoveryTmux connects the approval monitor to external session discovery using tmux-based streaming instead of socket-based streaming.

func (*ExternalApprovalMonitor) MarkApprovalHandled

func (m *ExternalApprovalMonitor) MarkApprovalHandled(socketPath, requestID string, approved bool) error

MarkApprovalHandled marks an approval request as handled.

func (*ExternalApprovalMonitor) MonitorSession

func (m *ExternalApprovalMonitor) MonitorSession(
	streamer *ExternalStreamer,
	title string,
	source ExternalApprovalSource,
) error

MonitorSession starts monitoring an external session for approval requests.

func (*ExternalApprovalMonitor) MonitorSessionTmux

func (m *ExternalApprovalMonitor) MonitorSessionTmux(
	streamer *ExternalTmuxStreamer,
	tmuxSessionName string,
	title string,
	source ExternalApprovalSource,
) error

MonitorSessionTmux starts monitoring an external session using tmux-based streaming.

func (*ExternalApprovalMonitor) OnApproval

func (m *ExternalApprovalMonitor) OnApproval(callback ExternalApprovalCallback)

OnApproval registers a callback for approval events.

func (*ExternalApprovalMonitor) Start

func (m *ExternalApprovalMonitor) Start()

Start begins monitoring for approvals.

func (*ExternalApprovalMonitor) Stop

func (m *ExternalApprovalMonitor) Stop()

Stop stops all monitoring.

func (*ExternalApprovalMonitor) StopMonitoringSession

func (m *ExternalApprovalMonitor) StopMonitoringSession(socketPath string)

StopMonitoringSession stops monitoring a specific session.

type ExternalApprovalSource

type ExternalApprovalSource string

ExternalApprovalSource identifies the source of an external approval.

const (
	SourceIntelliJ ExternalApprovalSource = "IntelliJ"
	SourceTerminal ExternalApprovalSource = "Terminal"
	SourceVSCode   ExternalApprovalSource = "VS Code"
	SourceMux      ExternalApprovalSource = "mux"
	SourceUnknown  ExternalApprovalSource = "Unknown"
)

type ExternalInstanceMetadata

type ExternalInstanceMetadata struct {
	// TmuxSocket is the tmux server socket this instance belongs to
	// Empty string means the default tmux server
	TmuxSocket string

	// TmuxSessionName is the full tmux session name
	TmuxSessionName string

	// DiscoveredAt is when this external instance was first discovered
	DiscoveredAt time.Time

	// LastSeen is when this instance was last seen during discovery
	LastSeen time.Time

	// OriginalPID is the process ID when first discovered
	OriginalPID int

	// MuxSocketPath is the path to an ssq-mux Unix domain socket
	// If set, this instance was discovered via ssq-mux and supports
	// full bidirectional terminal access
	MuxSocketPath string

	// MuxEnabled indicates whether this instance supports mux protocol
	MuxEnabled bool

	// SourceTerminal identifies the source (e.g., "IntelliJ", "Terminal", "tmux")
	SourceTerminal string
}

ExternalInstanceMetadata contains metadata for externally discovered Claude instances

type ExternalItem added in v1.35.0

type ExternalItem struct {
	ExternalID  string
	Title       string
	Description string
	Labels      []string
	Priority    int // 1-5, derived from labels
	URL         string
	// State is the external item's raw state string (e.g. GitHub issue
	// "open"/"closed"). Only populated by plugins that support two-way sync
	// (GitHubIssuesPlugin); left at zero value ("") for plugins like
	// GitHubPRsPlugin where two-way sync is out of scope.
	State string
	// IssueUpdatedAt is the external item's own last-modified timestamp (e.g.
	// GitHub issue updated_at), used as the loop-prevention watermark
	// comparison value. Zero value for plugins that don't populate it.
	IssueUpdatedAt time.Time
}

ExternalItem is a platform-agnostic representation of an external issue/ticket.

type ExternalSessionCandidate added in v1.42.0

type ExternalSessionCandidate struct {
	SourceKind  ImportSourceKind
	Path        string
	Program     string
	PID         int32
	TmuxSession string
	// SocketPath is empty for PlainTmux candidates.
	SocketPath string
}

ExternalSessionCandidate describes an unmanaged, discovered process/pane eligible for import, before any Instance is constructed for it. It is never persisted — it exists only to carry enough information through the preview/commit/kill pipeline.

func NewCandidateFromDiscovered added in v1.42.0

func NewCandidateFromDiscovered(ds *mux.DiscoveredSession) ExternalSessionCandidate

NewCandidateFromDiscovered maps an ssq-mux DiscoveredSession into a source-agnostic ExternalSessionCandidate. Pure mapping, no I/O.

type ExternalSessionDiscovery

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

ExternalSessionDiscovery discovers and manages external Claude sessions from ssq-mux multiplexed terminals.

func NewExternalSessionDiscovery

func NewExternalSessionDiscovery() *ExternalSessionDiscovery

NewExternalSessionDiscovery creates a new external session discovery service.

func (*ExternalSessionDiscovery) GetSession

func (e *ExternalSessionDiscovery) GetSession(socketPath string) *Instance

GetSession returns a specific external session by socket path (deprecated - use GetSessionByTmux).

func (*ExternalSessionDiscovery) GetSessionByTmux

func (e *ExternalSessionDiscovery) GetSessionByTmux(tmuxSessionName string) *Instance

GetSessionByTmux returns a specific external session by tmux session name.

func (*ExternalSessionDiscovery) GetSessions

func (e *ExternalSessionDiscovery) GetSessions() []*Instance

GetSessions returns all currently discovered external sessions.

func (*ExternalSessionDiscovery) OnSessionAdded

func (e *ExternalSessionDiscovery) OnSessionAdded(callback func(*Instance))

OnSessionAdded registers a callback for when a new external session is discovered. Multiple callbacks can be registered and will all be invoked.

func (*ExternalSessionDiscovery) OnSessionRemoved

func (e *ExternalSessionDiscovery) OnSessionRemoved(callback func(*Instance))

OnSessionRemoved registers a callback for when an external session is removed. Multiple callbacks can be registered and will all be invoked.

func (*ExternalSessionDiscovery) Start

func (e *ExternalSessionDiscovery) Start(interval time.Duration)

Start begins periodic discovery of external sessions.

func (*ExternalSessionDiscovery) Stop

func (e *ExternalSessionDiscovery) Stop()

Stop stops the discovery service.

type ExternalStreamer

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

ExternalStreamer connects to a mux socket and streams terminal output. It handles reconnection and broadcasts output to registered consumers.

func NewExternalStreamer

func NewExternalStreamer(socketPath string, bufferSize int) *ExternalStreamer

NewExternalStreamer creates a new streamer for the given mux socket.

func (*ExternalStreamer) AddConsumer

func (s *ExternalStreamer) AddConsumer(consumer OutputConsumer, catchUp bool) string

AddConsumer registers a callback to receive output data. If catchUp is true, the consumer receives buffered recent output first. Returns a token that must be passed to RemoveConsumer to deregister.

func (*ExternalStreamer) ConsumerCount

func (s *ExternalStreamer) ConsumerCount() int

ConsumerCount returns the number of registered consumers.

func (*ExternalStreamer) GetMetadata

func (s *ExternalStreamer) GetMetadata() *mux.SessionMetadata

GetMetadata returns the session metadata from the mux.

func (*ExternalStreamer) GetRecentOutput

func (s *ExternalStreamer) GetRecentOutput() []byte

GetRecentOutput returns the buffered recent output.

func (*ExternalStreamer) GetSnapshot

func (s *ExternalStreamer) GetSnapshot() ([]byte, error)

GetSnapshot requests a clean screen snapshot from the mux session. This uses tmux capture-pane on the server side to get clean terminal content without ANSI escape sequences, suitable for pattern matching and initial state. The snapshot request is coordinated with the readLoop to avoid race conditions.

func (*ExternalStreamer) IsConnected

func (s *ExternalStreamer) IsConnected() bool

IsConnected returns whether the streamer is currently connected.

func (*ExternalStreamer) RemoveConsumer

func (s *ExternalStreamer) RemoveConsumer(key string)

RemoveConsumer deregisters a consumer by the token returned from AddConsumer.

func (*ExternalStreamer) SendInput

func (s *ExternalStreamer) SendInput(data []byte) error

SendInput sends input data to the mux session.

func (*ExternalStreamer) SendResize

func (s *ExternalStreamer) SendResize(cols, rows uint16) error

SendResize sends a terminal resize command to the mux session.

func (*ExternalStreamer) SocketPath

func (s *ExternalStreamer) SocketPath() string

SocketPath returns the path to the mux socket.

func (*ExternalStreamer) Start

func (s *ExternalStreamer) Start() error

Start connects to the mux socket and begins streaming.

func (*ExternalStreamer) Stop

func (s *ExternalStreamer) Stop()

Stop disconnects and stops the streamer.

type ExternalStreamerManager

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

ExternalStreamerManager manages multiple external streamers.

func NewExternalStreamerManager

func NewExternalStreamerManager(bufferSize int) *ExternalStreamerManager

NewExternalStreamerManager creates a new streamer manager.

func (*ExternalStreamerManager) Count

func (m *ExternalStreamerManager) Count() int

Count returns the number of active streamers.

func (*ExternalStreamerManager) Get

func (m *ExternalStreamerManager) Get(socketPath string) *ExternalStreamer

Get returns a streamer if it exists.

func (*ExternalStreamerManager) GetOrCreate

func (m *ExternalStreamerManager) GetOrCreate(socketPath string) (*ExternalStreamer, error)

GetOrCreate returns an existing streamer or creates a new one.

func (*ExternalStreamerManager) Remove

func (m *ExternalStreamerManager) Remove(socketPath string)

Remove stops and removes a streamer.

func (*ExternalStreamerManager) StopAll

func (m *ExternalStreamerManager) StopAll()

StopAll stops all streamers.

type ExternalTmuxStreamer

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

ExternalTmuxStreamer provides terminal content streaming for external sessions.

It uses two strategies in priority order:

  1. Control mode (preferred): Starts "tmux -C attach-session -t <name> -r" which provides real-time %output notifications via the tmux control protocol. When an %output event arrives it signals that the pane content has changed, triggering a single capture-pane call to obtain the full terminal snapshot. This eliminates blind polling while preserving the full-snapshot semantic that consumers expect.

  2. Capture-pane polling (fallback): If control mode fails to start (e.g. older tmux, session not found) the streamer falls back to polling capture-pane every 500ms. This is less responsive but universally compatible.

func NewExternalTmuxStreamer

func NewExternalTmuxStreamer(tmuxSessionName string) *ExternalTmuxStreamer

NewExternalTmuxStreamer creates a new tmux-based streamer for an external session.

func (*ExternalTmuxStreamer) AddConsumer

func (s *ExternalTmuxStreamer) AddConsumer(consumer func(content string)) string

AddConsumer registers a callback to receive content updates. The consumer will be called with the full terminal content whenever it changes. Returns a token that must be passed to RemoveConsumer to deregister.

func (*ExternalTmuxStreamer) ConsumerCount

func (s *ExternalTmuxStreamer) ConsumerCount() int

ConsumerCount returns the number of registered consumers.

func (*ExternalTmuxStreamer) GetContent

func (s *ExternalTmuxStreamer) GetContent() string

GetContent returns the current terminal content.

func (*ExternalTmuxStreamer) IsRunning

func (s *ExternalTmuxStreamer) IsRunning() bool

IsRunning returns whether the streamer is currently running.

func (*ExternalTmuxStreamer) RemoveConsumer

func (s *ExternalTmuxStreamer) RemoveConsumer(key string)

RemoveConsumer deregisters a consumer by the token returned from AddConsumer.

func (*ExternalTmuxStreamer) Start

func (s *ExternalTmuxStreamer) Start() error

Start begins streaming the tmux session for content changes. It first attempts to use tmux control mode for event-driven updates. If control mode is unavailable, it falls back to capture-pane polling.

func (*ExternalTmuxStreamer) Stop

func (s *ExternalTmuxStreamer) Stop()

Stop stops the streamer.

type ExternalTmuxStreamerManager

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

ExternalTmuxStreamerManager manages multiple external tmux streamers.

func NewExternalTmuxStreamerManager

func NewExternalTmuxStreamerManager() *ExternalTmuxStreamerManager

NewExternalTmuxStreamerManager creates a new streamer manager.

func (*ExternalTmuxStreamerManager) Count

func (m *ExternalTmuxStreamerManager) Count() int

Count returns the number of active streamers.

func (*ExternalTmuxStreamerManager) Get

func (m *ExternalTmuxStreamerManager) Get(tmuxSessionName string) *ExternalTmuxStreamer

Get returns a streamer if it exists.

func (*ExternalTmuxStreamerManager) GetOrCreate

func (m *ExternalTmuxStreamerManager) GetOrCreate(tmuxSessionName string) (*ExternalTmuxStreamer, error)

GetOrCreate returns an existing streamer or creates a new one.

func (*ExternalTmuxStreamerManager) Remove

func (m *ExternalTmuxStreamerManager) Remove(tmuxSessionName string)

Remove stops and removes a streamer.

func (*ExternalTmuxStreamerManager) StopAll

func (m *ExternalTmuxStreamerManager) StopAll()

StopAll stops all streamers.

type FilesystemContext

type FilesystemContext struct {
	// ProjectPath is the root project/repository directory
	ProjectPath string `json:"project_path,omitempty"`

	// WorkingDir is the current working directory within the project
	WorkingDir string `json:"working_dir,omitempty"`

	// IsWorktree indicates if this session is using a git worktree
	IsWorktree bool `json:"is_worktree,omitempty"`

	// MainRepoPath is the parent repository path if this is a worktree
	MainRepoPath string `json:"main_repo_path,omitempty"`

	// ClonedRepoPath is the path to the cloned repository for external PRs
	ClonedRepoPath string `json:"cloned_repo_path,omitempty"`

	// ExistingWorktree is the path to an existing worktree being used
	ExistingWorktree string `json:"existing_worktree,omitempty"`

	// SessionType indicates the type of session workflow
	SessionType SessionType `json:"session_type,omitempty"`
}

FilesystemContext represents the filesystem-related context for a session. This includes project paths, working directories, and worktree information.

func (*FilesystemContext) IsEmpty

func (f *FilesystemContext) IsEmpty() bool

IsEmpty returns true if the FilesystemContext has no meaningful data

type ForceReleaseFunc added in v1.35.0

type ForceReleaseFunc func()

ForceReleaseFunc marks an unconditional-teardown closure: evicts regardless of refcount. No wrapper of ForceRelease exists in this plan — ForceRelease is always called directly with a sessionID. This type exists so that if a future caller wraps ForceRelease into a closure, the return type says so explicitly instead of degrading to a bare func().

type GitContext

type GitContext struct {
	// Branch is the current git branch name
	Branch string `json:"branch,omitempty"`

	// BaseCommitSHA is the commit SHA where this branch diverged from main/master
	BaseCommitSHA string `json:"base_commit_sha,omitempty"`

	// WorktreeID is a foreign key to the worktrees table (nil if no worktree)
	WorktreeID *int64 `json:"worktree_id,omitempty"`

	// PRNumber is the pull request number
	PRNumber int `json:"pr_number,omitempty"`

	// PRURL is the full URL to the pull request
	PRURL string `json:"pr_url,omitempty"`

	// Owner is the GitHub repository owner/organization
	Owner string `json:"owner,omitempty"`

	// Repo is the GitHub repository name
	Repo string `json:"repo,omitempty"`

	// SourceRef is the source branch reference for the PR
	SourceRef string `json:"source_ref,omitempty"`
}

GitContext represents the Git-related context for a session. This includes repository information, branch details, and GitHub PR integration.

func (*GitContext) IsEmpty

func (g *GitContext) IsEmpty() bool

IsEmpty returns true if the GitContext has no meaningful data

type GitHubIntegration added in v1.35.0

type GitHubIntegration struct {
	// Repository identity and PR linkage
	GitHubPRNumber  int
	GitHubPRURL     string
	GitHubOwner     string
	GitHubRepo      string
	GitHubSourceRef string
	ClonedRepoPath  string
	MainRepoPath    string
	IsWorktree      bool
	GitHubIsFork    bool

	// PR status fields (populated by PRStatusPoller)
	GitHubPRState          string
	GitHubPRIsDraft        bool
	GitHubPRPriority       string
	GitHubApprovedCount    int
	GitHubChangesReqCount  int
	GitHubCheckConclusion  string
	GitHubPRStatusTerminal bool
	LastPRStatusCheck      time.Time
}

GitHubIntegration groups all GitHub PR / URL integration fields within InstanceSnapshot (CDD Epic 3, Task 3.1a). Access via snap.GitHub.GitHubPRURL etc.

type GitHubIssuesPlugin added in v1.35.0

type GitHubIssuesPlugin struct{}

GitHubIssuesPlugin fetches backlog items from a GitHub repository's issue tracker.

func NewGitHubIssuesPlugin added in v1.35.0

func NewGitHubIssuesPlugin() *GitHubIssuesPlugin

NewGitHubIssuesPlugin returns a new GitHubIssuesPlugin.

func (*GitHubIssuesPlugin) CloseIssue added in v1.41.0

func (g *GitHubIssuesPlugin) CloseIssue(ctx context.Context, config PluginConfig, externalID string, existingLabels []string, closeLabel string) (time.Time, error)

CloseIssue closes a GitHub issue and, if closeLabel is non-empty, merges it into the issue's existing labels (never replacing them — GitHub's labels field on this endpoint fully replaces the array, so existingLabels must be passed in and merged locally; this replace-vs-merge semantic is documented at https://docs.github.com/en/rest/issues/issues#update-an-issue — the `labels` field, when present, sets the issue's full label list). Omitting the `labels` field entirely (rather than sending an empty array) leaves the issue's existing labels untouched, which is why closeLabel == "" skips the field rather than sending `"labels":[]`.

Returns the issue's post-close updated_at from GitHub's own response — the caller uses this (not local wall-clock time) for the ADR-003 loop-prevention watermark, to avoid clock-skew/read-after-write-lag entirely. Returns a zero time (with a nil error) only in the narrow case where the close succeeded (HTTP status < 300) but the response body couldn't be decoded — the close itself must not be treated as failed just because the confirmation body was unparseable.

func (*GitHubIssuesPlugin) Fetch added in v1.35.0

func (g *GitHubIssuesPlugin) Fetch(ctx context.Context, config PluginConfig, cursor string) ([]ExternalItem, string, error)

Fetch retrieves new and updated GitHub issues since the cursor. The cursor is an ISO 8601 timestamp passed as the `since` query parameter. Returns the updated cursor (the most recent updated_at seen) and the fetched items. If the token field is empty, Fetch returns an empty list and the original cursor.

Fetch is single-page (githubIssuesPerPage per call) — correct for the incremental sync path, where the cursor bounds results to items updated since the last tick. Callers that need the full result set regardless of page size (e.g. PreviewBackwardSyncImpact) must use FetchAll instead; a single Fetch call silently misses older items on repos with more than one page of history, since GitHub sorts by `created` descending by default.

func (*GitHubIssuesPlugin) FetchAll added in v1.41.0

func (g *GitHubIssuesPlugin) FetchAll(ctx context.Context, config PluginConfig, cursor string) (items []ExternalItem, newCursor string, possiblyIncomplete bool, err error)

FetchAll retrieves every GitHub issue across up to maxPreviewFetchPages pages, aggregating results the way Fetch's single-page call cannot. Used only by PreviewBackwardSyncImpact, which needs to see the true state of all already-imported items rather than just the newest page.

possiblyIncomplete is true if the page cap was hit while the last page fetched was still full — meaning there may be more issues beyond what was returned, and callers must not treat the result as exhaustive.

func (*GitHubIssuesPlugin) MapToBacklogItem added in v1.35.0

func (g *GitHubIssuesPlugin) MapToBacklogItem(item ExternalItem, sourceID string) BacklogItemData

MapToBacklogItem converts a GitHub ExternalItem to a BacklogItemData.

func (*GitHubIssuesPlugin) PluginID added in v1.35.0

func (g *GitHubIssuesPlugin) PluginID() string

PluginID returns the unique identifier for this plugin.

func (*GitHubIssuesPlugin) PostIssueComment added in v1.41.0

func (g *GitHubIssuesPlugin) PostIssueComment(ctx context.Context, config PluginConfig, externalID string, body string) error

PostIssueComment posts a comment on a GitHub issue — used by the forward-sync subscriber to leave a visible trail explaining an automated close, per the "no silent automated action" convention (pitfalls research §7). Returns an error on any non-2xx response; callers treat a comment failure as best-effort (the close itself already succeeded by the time this is called).

type GitHubMetadataView

type GitHubMetadataView struct {
	PRNumber       int
	PRURL          string
	Owner          string
	Repo           string
	SourceRef      string
	ClonedRepoPath string
}

GitHubMetadataView is a read-only value object for GitHub session metadata. Constructed by Instance.GitHub() from the underlying fields. This is intentionally a value type (not a pointer) for safe concurrent reads.

func (GitHubMetadataView) IsEmpty

func (gh GitHubMetadataView) IsEmpty() bool

IsEmpty returns true if no GitHub metadata is set.

func (GitHubMetadataView) IsGitHubSession

func (gh GitHubMetadataView) IsGitHubSession() bool

IsGitHubSession returns true if owner and repo are both set.

func (GitHubMetadataView) IsPRSession

func (gh GitHubMetadataView) IsPRSession() bool

IsPRSession returns true if this metadata represents a PR-based session.

func (GitHubMetadataView) PRDisplayInfo

func (gh GitHubMetadataView) PRDisplayInfo() string

PRDisplayInfo returns human-readable PR description for UI display. Returns empty string if not a PR session.

func (GitHubMetadataView) RepoFullName

func (gh GitHubMetadataView) RepoFullName() string

RepoFullName returns "owner/repo" format, or empty string if either is missing.

type GitHubPRsPlugin added in v1.35.0

type GitHubPRsPlugin struct{}

GitHubPRsPlugin fetches open pull requests from a GitHub repository.

func NewGitHubPRsPlugin added in v1.35.0

func NewGitHubPRsPlugin() *GitHubPRsPlugin

NewGitHubPRsPlugin returns a new GitHubPRsPlugin.

func (*GitHubPRsPlugin) Fetch added in v1.35.0

func (g *GitHubPRsPlugin) Fetch(ctx context.Context, config PluginConfig, cursor string) ([]ExternalItem, string, error)

Fetch retrieves open pull requests. Cursor is unused (full refresh each time). Returns empty list when token is absent.

func (*GitHubPRsPlugin) MapToBacklogItem added in v1.35.0

func (g *GitHubPRsPlugin) MapToBacklogItem(item ExternalItem, sourceID string) BacklogItemData

MapToBacklogItem converts a GitHub PR ExternalItem to a BacklogItemData.

func (*GitHubPRsPlugin) PluginID added in v1.35.0

func (g *GitHubPRsPlugin) PluginID() string

PluginID returns the unique identifier for this plugin.

type GitHubRef

type GitHubRef struct {
	Host     string // GitHub host, e.g. "github.com" or a GHES hostname; "" means github.com
	Owner    string
	Repo     string
	Branch   string
	PRNumber int
	Type     GitHubRefType
}

GitHubRef represents a parsed GitHub reference.

func ParseGitHubURL

func ParseGitHubURL(input string) (*GitHubRef, error)

ParseGitHubURL parses a github.com URL and returns the components. Supported formats:

func ParseGitHubURLWithHosts added in v1.41.0

func ParseGitHubURLWithHosts(input string, enterpriseHosts []string) (*GitHubRef, error)

ParseGitHubURLWithHosts parses a GitHub URL or shorthand, additionally recognizing URLs against any of the given GitHub Enterprise hostnames (in addition to github.com). Delegates the regex/host matching to the github package's ParseGitHubRefWithHosts, then re-validates Owner/Repo against isTraversalSegment: the github package's own parsing provides no path-traversal protection (its only validation, isValidGitHubName, is used solely for the shorthand path and explicitly permits "." and ".."), so this guard must be re-applied here before Owner/Repo are ever used to build a local filesystem path in GetRepoPath.

func ResolveGitHubInput

func ResolveGitHubInput(input string) (localPath string, ref *GitHubRef, err error)

ResolveGitHubInput is a convenience function using the default manager.

func ResolveGitHubInputCtx added in v1.41.0

func ResolveGitHubInputCtx(ctx context.Context, input string) (localPath string, ref *GitHubRef, err error)

ResolveGitHubInputCtx is a convenience function using the default manager, threading ctx down to the underlying git clone/fetch subprocess.

func ResolveGitHubInputCtxWithHosts added in v1.41.0

func ResolveGitHubInputCtxWithHosts(ctx context.Context, input string, enterpriseHosts []string) (localPath string, ref *GitHubRef, err error)

ResolveGitHubInputCtxWithHosts is a convenience function using the default manager, recognizing URLs against the given GitHub Enterprise hostnames in addition to github.com.

func (*GitHubRef) PRURL added in v1.42.0

func (r *GitHubRef) PRURL() string

PRURL returns the canonical URL of the PR this ref points at, or "" if PRNumber is not set (e.g. a plain repo or branch ref). This is the single source of truth for GitHub PR URL construction — session_service.go's CreateSession handler and the github_pr_url_backfill.go migration both call this instead of formatting the URL themselves, so a host-normalization or URL-shape fix only has to be made once.

type GitHubRefType

type GitHubRefType int

GitHubRefType indicates what kind of GitHub reference this is.

const (
	GitHubRefTypeRepo GitHubRefType = iota
	GitHubRefTypeBranch
	GitHubRefTypePR
)

type GitManager added in v1.15.0

type GitManager interface {
	HasWorktree() bool
	GetWorktree() *git.GitWorktree
	SetWorktree(*git.GitWorktree)
	GetWorktreePath() string
	GetRepoPath() string
	GetRepoName() string
	GetBranchName() string
	GetBaseCommitSHA() string
	Setup() error
	Cleanup() error
	Remove() error
	Prune() error
	IsDirty() (bool, error)
	InvalidateDirtyCache()
	CommitChanges(commitMsg string) error
	PushChanges(commitMsg string, open bool) error
	IsBranchCheckedOut() (bool, error)
	OpenBranchURL() error
	ComputeDiffIfReady() (stats *git.DiffStats, needsPause bool)
	ComputeDiff() *git.DiffStats
	UpdateDiffStats()
	GetDiffStats() *git.DiffStats
	SetDiffStats(*git.DiffStats)
	ClearDiffStats()
	GetCurrentCommitSHA() (string, error)
	PrimeDirtyCacheJitter()
}

GitManager is the interface satisfied by *GitWorktreeManager. It covers all git worktree operations used by Instance and can be implemented by test doubles to avoid requiring a real git repository.

type GitWorktreeData

type GitWorktreeData struct {
	RepoPath      string `json:"repo_path"`
	WorktreePath  string `json:"worktree_path"`
	SessionName   string `json:"session_name"`
	BranchName    string `json:"branch_name"`
	BaseCommitSHA string `json:"base_commit_sha"`
}

GitWorktreeData represents the serializable data of a GitWorktree

type GitWorktreeManager

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

GitWorktreeManager owns the git worktree and diff-stats state that were previously bare fields on Instance.

Instance keeps thin wrapper methods that delegate here. GitWorktreeManager itself has no knowledge of Instance lifecycle; it only manages the worktree and diff operations.

worktree/diffStats are guarded by mu, not by Instance.stateMutex: setup (setupFirstTimeWorktree, called under Instance.startMu) and read-side callers (e.g. GetEffectiveRootDir) don't consistently hold stateMutex, so GitWorktreeManager protects its own fields directly.

func (*GitWorktreeManager) Cleanup

func (gm *GitWorktreeManager) Cleanup() error

Cleanup removes the worktree from the filesystem and git metadata. Returns nil if no worktree is set.

func (*GitWorktreeManager) ClearDiffStats

func (gm *GitWorktreeManager) ClearDiffStats()

ClearDiffStats sets diffStats to nil.

func (*GitWorktreeManager) CommitChanges

func (gm *GitWorktreeManager) CommitChanges(commitMsg string) error

CommitChanges stages all changes and creates a commit.

func (*GitWorktreeManager) ComputeDiff

func (gm *GitWorktreeManager) ComputeDiff() *git.DiffStats

ComputeDiff runs git diff and returns the result without storing it. Returns nil if no worktree is set.

func (*GitWorktreeManager) ComputeDiffIfReady

func (gm *GitWorktreeManager) ComputeDiffIfReady() (stats *git.DiffStats, needsPause bool)

ComputeDiffIfReady checks if the worktree path exists and computes a new diff. Returns (stats, needsPause) where needsPause is true if the worktree directory is missing. This method performs I/O and should be called WITHOUT holding Instance.mu. Returns (nil, false) if no worktree is set.

func (*GitWorktreeManager) GetBaseCommitSHA

func (gm *GitWorktreeManager) GetBaseCommitSHA() string

GetBaseCommitSHA returns the base commit SHA or "" if no worktree.

func (*GitWorktreeManager) GetBranchName

func (gm *GitWorktreeManager) GetBranchName() string

GetBranchName returns the branch name or "" if no worktree.

func (*GitWorktreeManager) GetCurrentCommitSHA

func (gm *GitWorktreeManager) GetCurrentCommitSHA() (string, error)

GetCurrentCommitSHA returns the current HEAD commit SHA for the worktree. Returns an empty string (not an error) if no worktree is set or the repo has no commits yet — this is safe to use in checkpoint creation.

func (*GitWorktreeManager) GetDiffStats

func (gm *GitWorktreeManager) GetDiffStats() *git.DiffStats

GetDiffStats returns the most recently computed diff stats (may be nil).

func (*GitWorktreeManager) GetDirBaseSHA added in v1.37.0

func (gm *GitWorktreeManager) GetDirBaseSHA() string

GetDirBaseSHA returns the base commit SHA for directory-mode diff computation.

func (*GitWorktreeManager) GetRepoName

func (gm *GitWorktreeManager) GetRepoName() string

GetRepoName returns the repository name or "" if no worktree.

func (*GitWorktreeManager) GetRepoPath

func (gm *GitWorktreeManager) GetRepoPath() string

GetRepoPath returns the repo root path or "" if no worktree.

func (*GitWorktreeManager) GetWorktree

func (gm *GitWorktreeManager) GetWorktree() *git.GitWorktree

GetWorktree returns the underlying GitWorktree (may be nil before Setup).

func (*GitWorktreeManager) GetWorktreePath

func (gm *GitWorktreeManager) GetWorktreePath() string

GetWorktreePath returns the worktree path or "" if no worktree.

func (*GitWorktreeManager) HasWorktree

func (gm *GitWorktreeManager) HasWorktree() bool

HasWorktree reports whether a git worktree has been initialized.

func (*GitWorktreeManager) InvalidateDirtyCache added in v1.35.0

func (gm *GitWorktreeManager) InvalidateDirtyCache()

InvalidateDirtyCache clears the IsDirty TTL cache so the next call re-runs git status. Call after transitions that may change worktree dirty state (Resume, Stop). No-op if no worktree is set.

func (*GitWorktreeManager) IsBranchCheckedOut

func (gm *GitWorktreeManager) IsBranchCheckedOut() (bool, error)

IsBranchCheckedOut reports whether the branch is currently checked out.

func (*GitWorktreeManager) IsDirty

func (gm *GitWorktreeManager) IsDirty() (bool, error)

IsDirty reports whether the worktree has uncommitted changes.

func (*GitWorktreeManager) OpenBranchURL

func (gm *GitWorktreeManager) OpenBranchURL() error

OpenBranchURL opens the branch URL in the browser.

func (*GitWorktreeManager) PrimeDirtyCacheJitter added in v1.35.0

func (gm *GitWorktreeManager) PrimeDirtyCacheJitter()

PrimeDirtyCacheJitter staggers the dirty-cache TTL by setting the cache timestamp to a random point in [now-IsDirtyCacheTTL, now). Call this when adding a session to the poller so sessions added in a burst don't all run git-status subprocesses simultaneously when their caches expire.

func (*GitWorktreeManager) Prune

func (gm *GitWorktreeManager) Prune() error

Prune cleans up stale worktree references.

func (*GitWorktreeManager) PushChanges

func (gm *GitWorktreeManager) PushChanges(commitMsg string, open bool) error

PushChanges commits and pushes the worktree branch.

func (*GitWorktreeManager) Remove

func (gm *GitWorktreeManager) Remove() error

Remove removes the worktree from git without pruning.

func (*GitWorktreeManager) SetDiffStats

func (gm *GitWorktreeManager) SetDiffStats(stats *git.DiffStats)

SetDiffStats directly replaces the diff stats (used during deserialization).

func (*GitWorktreeManager) SetDirBaseSHA added in v1.37.0

func (gm *GitWorktreeManager) SetDirBaseSHA(sha string)

SetDirBaseSHA sets the base commit SHA for directory-mode diff computation.

func (*GitWorktreeManager) SetWorktree

func (gm *GitWorktreeManager) SetWorktree(wt *git.GitWorktree)

SetWorktree replaces the underlying GitWorktree. Used during session start and by tests.

func (*GitWorktreeManager) Setup

func (gm *GitWorktreeManager) Setup() error

Setup prepares the worktree (creates directories, checks out branch, etc.).

func (*GitWorktreeManager) UpdateDiffStats

func (gm *GitWorktreeManager) UpdateDiffStats()

UpdateDiffStats computes a new diff and stores it. Returns nil and clears stats if worktree is not ready.

type HeadlessPoolClient added in v1.35.0

type HeadlessPoolClient interface {
	CallBlocking(ctx context.Context, key headless.FeatureKey, systemPrompt string, userPrompt string, opts headless.CallOptions) (string, float64, error)
}

HeadlessPoolClient is the narrow interface AutonomousDriver needs from the headless pool. *headless.Pool satisfies this interface directly.

type HeadlessTriageResult added in v1.35.0

type HeadlessTriageResult struct {
	Title   string `json:"title"`
	Summary string `json:"summary"`
	// Priority is the LLM's assessed urgency/impact (1=P1 critical ... 5=P5
	// trivial), applied to the item once triage completes — see
	// applyTriageResultToUpdate (server/services/backlog_service_triage.go).
	// Zero (omitted by the model) means "no assessment" and leaves the item's
	// existing priority untouched, same convention as AcceptanceCriteria below.
	Priority int `json:"priority,omitempty"`
	// ItemCategory is the LLM's classification of what kind of work this is —
	// one of session.BacklogCategory's values (bugfix/feature/chore/refactor).
	// Named distinctly from TriageTask.Category (engineering area: backend/
	// frontend/test/infra/docs) to avoid the two colliding in the same JSON
	// object the model produces. Empty or invalid leaves the item's existing
	// category untouched.
	ItemCategory       string             `json:"item_category,omitempty"`
	Suggestions        []TriageSuggestion `json:"suggestions"`
	Tasks              []TriageTask       `json:"tasks,omitempty"`
	AcceptanceCriteria []AcCriterion      `json:"acceptance_criteria,omitempty"`
	// Iteration and Feedback are not part of the LLM's JSON output — the caller
	// sets them after parsing, from server-tracked state, before persisting.
	Iteration int    `json:"iteration,omitempty"`
	Feedback  string `json:"feedback,omitempty"`
}

HeadlessTriageResult is the parsed output from a headless triage LLM call.

func ParseHeadlessTriageResult added in v1.35.0

func ParseHeadlessTriageResult(raw string) (HeadlessTriageResult, error)

ParseHeadlessTriageResult unmarshals an LLM JSON response into HeadlessTriageResult. Tolerates preamble text before the JSON block (e.g. "Here is the result:\n\n{...}") and stray unrelated braces earlier in the response (e.g. an illustrative snippet).

The triage prompt instructs the model to emit the JSON object last, so candidates are tried from the end of the response backwards — the first candidate (i.e. the last brace-delimited span in raw) that unmarshals cleanly wins. This correctly skips over any earlier decoy object that happens to also be syntactically valid JSON but isn't the real result.

Caps tasks at maxHeadlessTriageTasks.

type HealthCheckResult

type HealthCheckResult struct {
	InstanceTitle     string
	IsHealthy         bool
	Issues            []string
	Actions           []string
	RecoveryAttempted bool
	RecoverySuccess   bool
}

HealthCheckResult represents the result of a session health check

type HibernationSweeper added in v1.35.0

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

HibernationSweeper periodically checks all sessions and hibernates those that have been idle longer than the configured timeout or that are consuming memory while the system is under pressure.

func NewHibernationSweeper added in v1.35.0

func NewHibernationSweeper(storage *Storage, cfg *appconfig.Config, reader memory.Reader) *HibernationSweeper

NewHibernationSweeper creates a HibernationSweeper using the given storage, config, and memory reader.

func (*HibernationSweeper) GetCachedRSSMB added in v1.35.0

func (s *HibernationSweeper) GetCachedRSSMB(sessionUUID string) int64

GetCachedRSSMB returns the last-measured RSS in MB for the given session UUID. Returns 0 if not yet measured or entry expired. Implements MemoryCacheReader.

func (*HibernationSweeper) SetLiveProvider added in v1.35.0

func (s *HibernationSweeper) SetLiveProvider(p LiveInstancesProvider)

SetLiveProvider wires the fast-path instance source. Call this after constructing the ReviewQueuePoller so that sweep() uses live in-memory instances instead of calling LoadInstances() (which spawns PTY/tmux subprocesses).

func (*HibernationSweeper) Start added in v1.35.0

func (s *HibernationSweeper) Start(ctx context.Context)

Start runs the periodic sweep loop. Blocks until ctx is cancelled.

func (*HibernationSweeper) SystemMemoryPct added in v1.35.0

func (s *HibernationSweeper) SystemMemoryPct() (float64, error)

SystemMemoryPct returns the current system memory usage percentage. The result is cached for sysMemCacheTTL to avoid a syscall on every ListSessions request. The mutex is released before calling the reader to avoid holding the lock during /proc I/O. Implements MemoryCacheReader.

type HistoryAdapter added in v1.35.0

type HistoryAdapter interface {
	Name() string
	CanHandle(program string) bool

	// Import reads this CLI's native format and returns canonical turns.
	Import(ctx context.Context, inst *Instance) ([]CanonicalTurn, error)

	// Export writes canonical turns into this CLI's native format so it can resume.
	Export(ctx context.Context, turns []CanonicalTurn, inst *Instance) error
}

type HistoryEntry

type HistoryEntry struct {
	Command       Command          `json:"command"`
	Result        *ExecutionResult `json:"result,omitempty"`
	Timestamp     time.Time        `json:"timestamp"`
	SessionName   string           `json:"session_name"`
	ExecutionTime time.Duration    `json:"execution_time"`
}

HistoryEntry represents a single command execution in history.

type HistoryFileDetector

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

HistoryFileDetector detects Claude JSONL history files for a given process.

func NewHistoryFileDetector

func NewHistoryFileDetector(inspector ProcessFileInspector) *HistoryFileDetector

NewHistoryFileDetector creates a new HistoryFileDetector.

func NewHistoryFileDetectorWithHomeDir added in v1.12.0

func NewHistoryFileDetectorWithHomeDir(inspector ProcessFileInspector, homeDir string) *HistoryFileDetector

NewHistoryFileDetectorWithHomeDir creates a HistoryFileDetector with a fixed home directory. Use this in tests to avoid writing to the real home dir.

func NewHistoryFileDetectorWithRealInspector

func NewHistoryFileDetectorWithRealInspector() *HistoryFileDetector

NewHistoryFileDetectorWithRealInspector creates a HistoryFileDetector using the real gopsutil-based ProcessInspector on darwin.

func (*HistoryFileDetector) Detect

func (d *HistoryFileDetector) Detect(pid int32) (*HistoryFileInfo, error)

Detect scans the open files of the given PID for Claude JSONL history files. Returns nil, nil if no matching file is found or the process is dead.

func (*HistoryFileDetector) DetectAllByPath added in v1.42.0

func (d *HistoryFileDetector) DetectAllByPath(projectPath string) ([]HistoryFileInfo, error)

DetectAllByPath scans ~/.claude/projects/<encoded-path>/ and returns every valid conversation JSONL file found, sorted most-recently-modified first. Unlike DetectByPath, it does not silently collapse multiple candidates down to "most recent" — callers that need to detect ambiguity (e.g. import correlation) use this variant; DetectByPath's existing single-result contract and callers (HistoryLinker) are unchanged.

Returns nil, nil if the project directory does not exist or contains no valid conversation files.

func (*HistoryFileDetector) DetectByPath added in v1.12.0

func (d *HistoryFileDetector) DetectByPath(projectPath string) (*HistoryFileInfo, error)

DetectByPath scans ~/.claude/projects/<encoded-path>/ for the most recently modified conversation JSONL file. It does NOT require a live process, making it suitable for sessions whose tmux session is dead (e.g. after a reboot).

Returns nil, nil if the project directory does not exist or contains no valid conversation files.

func (*HistoryFileDetector) ResolveFilePath added in v1.42.0

func (d *HistoryFileDetector) ResolveFilePath(projectPath, conversationUUID string) (string, error)

ResolveFilePath reconstructs the on-disk JSONL path for a known (projectPath, conversationUUID) pair, using the same home-dir resolution (including the test override) and path-encoding convention as Detect/DetectByPath/DetectAllByPath. Used by callers that already know the UUID (e.g. a Resolved CorrelationResult from CorrelateCandidate) and need the file path without re-scanning the directory.

type HistoryFileInfo

type HistoryFileInfo struct {
	ConversationUUID string
	HistoryFilePath  string
	ProjectDir       string
	// ModTime is the on-disk mtime of the winning candidate. Populated by
	// DetectByPath; left zero by Detect() since the PID-based fast path is
	// process ground truth and doesn't need mtime gating.
	ModTime time.Time
}

HistoryFileInfo contains information about a detected Claude history file.

type HistoryFileWatcher

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

HistoryFileWatcher watches ~/.claude/projects/ for new JSONL files.

func NewHistoryFileWatcher

func NewHistoryFileWatcher(watchDir string, callback func(filePath string)) *HistoryFileWatcher

NewHistoryFileWatcher creates a watcher for the given directory. If watchDir is empty, defaults to ~/.claude/projects/.

func (*HistoryFileWatcher) Start

func (w *HistoryFileWatcher) Start(ctx context.Context) error

Start begins watching the directory. It returns without error even if the directory does not exist (degraded mode — polling fallback still works).

func (*HistoryFileWatcher) Stop

func (w *HistoryFileWatcher) Stop()

Stop closes the watcher.

func (*HistoryFileWatcher) Stopped added in v1.35.0

func (w *HistoryFileWatcher) Stopped() <-chan struct{}

Stopped returns a channel that is closed when the watcher goroutine has exited.

type HistoryLinker

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

HistoryLinker is a background service that correlates running sessions with their Claude JSONL history files. It populates Instance.claudeSession.ConversationUUID and Instance.HistoryFilePath when a conversation file is detected.

Detection uses two complementary paths:

  • Polling (every 5 s): scans all running sessions via proc_pidinfo open-files
  • fsnotify (fast path): watcher callback fires as soon as a new JSONL is created

Both paths call the same correlateSession helper, which is idempotent. Sessions that repeatedly yield no JSONL file are throttled via exponential backoff to reduce subprocess spawn rate on idle worktrees.

func NewHistoryLinker

func NewHistoryLinker(detector *HistoryFileDetector, watcher *HistoryFileWatcher) *HistoryLinker

NewHistoryLinker creates a HistoryLinker backed by the given detector and watcher. Call SetInstances (or AddInstance) to register sessions before starting.

func NewHistoryLinkerFromRealInspector added in v1.8.0

func NewHistoryLinkerFromRealInspector() *HistoryLinker

NewHistoryLinkerFromRealInspector creates a HistoryLinker backed by the real gopsutil-based process inspector and an fsnotify watcher on ~/.claude/projects/. This is the production constructor; use NewHistoryLinker in tests.

func (*HistoryLinker) AddInstance

func (hl *HistoryLinker) AddInstance(instance *Instance)

AddInstance adds a single instance for monitoring. A no-op if an instance with the same ID is already registered: callers may legitimately invoke this more than once for the same instance (e.g. wireCallbacks runs on every loadInstancesWithWiring call, including the ListSessions fallback), and an unguarded append would leave duplicate entries in hl.instances, causing correlateSession to run twice per poll tick for the same session.

func (*HistoryLinker) Instances added in v1.8.0

func (hl *HistoryLinker) Instances() []*Instance

Instances returns a snapshot of the currently monitored instances. Used by shutdown hooks that need the live set (including externally added sessions).

func (*HistoryLinker) RegisterFileCallback added in v1.35.0

func (hl *HistoryLinker) RegisterFileCallback(cb func(filePath string))

RegisterFileCallback registers a callback that receives the file path whenever a JSONL history file is created or modified. Used to wire the TokenStore into the existing fsnotify infrastructure without creating a second watcher.

func (*HistoryLinker) RemoveInstance

func (hl *HistoryLinker) RemoveInstance(title string)

RemoveInstance stops monitoring the named instance.

func (*HistoryLinker) ScanAll

func (hl *HistoryLinker) ScanAll()

ScanAll triggers an immediate correlation pass over all monitored instances, including those already linked to a UUID. Exported for use by HistoryFileWatcher callbacks and called on startup. Resets backoffs and force-rechecks all sessions so that UUID changes (e.g., /clear creating a new conversation) are detected promptly rather than waiting for the next cold restore.

func (*HistoryLinker) SetInstances

func (hl *HistoryLinker) SetInstances(instances []*Instance)

SetInstances replaces the full instance list.

func (*HistoryLinker) Start

func (hl *HistoryLinker) Start(ctx context.Context)

Start performs an initial synchronous scan and then runs a background poll loop until ctx is cancelled. The fsnotify watcher is also started here so that new JSONL files trigger instant correlation.

type HistoryStatistics

type HistoryStatistics struct {
	TotalCommands        int
	SuccessfulCommands   int
	FailedCommands       int
	CancelledCommands    int
	AverageExecutionTime time.Duration
	FirstCommandTime     time.Time
	LastCommandTime      time.Time
}

HistoryStatistics provides summary statistics about command history.

type ImportSourceKind added in v1.42.0

type ImportSourceKind int

ImportSourceKind identifies which discovery mechanism produced an ExternalSessionCandidate. It determines which correlation inputs are available (socket+PID vs. pane-only) and which kill primitive applies.

PlainTmux is defined now (Phase 1) even though it is not yet produced by any discovery path — that arrives in Phase 2 — so that ExternalSessionCandidate does not need a breaking change when the second source is added.

const (
	// MuxDiscovered indicates the candidate came from an ssq-mux-wrapped
	// session discovered via mux.Discovery.Scan().
	MuxDiscovered ImportSourceKind = iota
	// PlainTmux indicates the candidate came from a plain tmux pane with no
	// ssq-mux wrapper (Phase 2; unused in Phase 1).
	PlainTmux
)

func (ImportSourceKind) String added in v1.42.0

func (k ImportSourceKind) String() string

String returns a human-readable name for logging.

type InhibitionEngine added in v1.42.0

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

InhibitionEngine filters and redacts sensitive data from canonical turns.

func NewInhibitionEngine added in v1.42.0

func NewInhibitionEngine() *InhibitionEngine

NewInhibitionEngine creates a new InhibitionEngine with default security patterns.

func (*InhibitionEngine) SanitizeString added in v1.42.0

func (ie *InhibitionEngine) SanitizeString(s string) string

SanitizeString redacts any sensitive credentials or secrets found in text.

func (*InhibitionEngine) SanitizeTurn added in v1.42.0

func (ie *InhibitionEngine) SanitizeTurn(turn CanonicalTurn) CanonicalTurn

SanitizeTurn applies inhibition rules to all blocks within a CanonicalTurn.

func (*InhibitionEngine) SanitizeTurns added in v1.42.0

func (ie *InhibitionEngine) SanitizeTurns(turns []CanonicalTurn) []CanonicalTurn

SanitizeTurns applies inhibition rules to a slice of CanonicalTurns.

type Instance

type Instance struct {
	// ID is the stable, immutable identifier for this instance.
	// Set once at creation; never changes even if Title is renamed.
	// Falls back to Title when empty for backward compatibility.
	ID string
	// Title is the title of the instance.
	Title string
	// UUID is a stable unique identifier for this instance, generated at creation time.
	// Unlike Title, UUID does not change when the session is renamed.
	UUID string
	// Path is the path to the workspace repository root.
	Path string
	// WorkingDir is the directory within the repository to start in.
	WorkingDir string
	// Branch is the branch of the instance.
	Branch string
	// Status is the status of the instance.
	Status Status
	// Program is the program to run in the instance.
	Program string
	// Height is the height of the instance.
	Height int
	// Width is the width of the instance.
	Width int
	// CreatedAt is the time the instance was created.
	CreatedAt time.Time
	// UpdatedAt is the time the instance was last updated.
	UpdatedAt time.Time
	// AutoYes is true if the instance should automatically press enter when prompted.
	AutoYes bool
	// AutoApprove is true if the launch command should get a per-agent CLI flag
	// that skips permission/approval prompts entirely (e.g.
	// --dangerously-skip-permissions for Claude). Independent of AutoYes (see
	// its doc comment above) -- resolved via yoloFlagFor in instance_tmux.go.
	AutoApprove bool
	// Prompt is passed as a CLI argument to the program at process-spawn time (buildClaudeCommand),
	// so it only takes effect on a truly fresh spawn (claudeSessionID == "", no --resume) or OneShot.
	// Use for content that must exist before the process's first turn, e.g. backlog task context.
	// See InitialPrompt for the tmux-typed alternative — the two are independent and can both be
	// set on the same instance (e.g. Omnibar sends attachments via Prompt, typed text via InitialPrompt).
	Prompt string
	// InitialPrompt, unlike Prompt, is typed into the tmux pane as simulated keystrokes once the
	// session reaches Ready state (session_driver.go) — the only delivery path that works for
	// resuming/attaching to an already-running pane, where a CLI arg can't be injected after the
	// fact. Replaces the static driverInitialPrompt when non-empty.
	InitialPrompt string
	// ExistingWorktree is an optional path to an existing worktree to reuse
	ExistingWorktree string
	// Category is used for organizing sessions into groups
	Category string
	// Note is a user-authored free-form markdown note attached to this session.
	Note string
	// IsExpanded indicates whether this session's category is expanded in the UI
	IsExpanded bool
	// SessionType determines the session workflow (directory, new_worktree, existing_worktree)
	SessionType SessionType
	// CreateIfMissing: when SessionTypeDirectory, create the directory and run git init
	// if the path does not exist. Set from the request's create_if_missing field.
	// Not persisted — only relevant during initial session start.
	CreateIfMissing bool `json:"-"`
	// TmuxPrefix is the prefix to use for tmux session names
	TmuxPrefix string
	// TmuxServerSocket is the server socket name for tmux isolation (used with -L flag)
	// If empty, uses the default tmux server. For complete isolation (e.g., testing),
	// set to a unique value like "test" or "teatest_123" to create separate tmux servers.
	TmuxServerSocket string
	// Tags are multi-valued labels for flexible session organization
	// Sessions can have multiple tags and appear in multiple groups simultaneously
	// Examples: ["frontend", "urgent", "client-work"]
	Tags []string
	// AutonomousMode enables autonomous Earpiece mode (crew autonomy).
	// When true, the Fixer will inject correction prompts without user confirmation.
	// When false (default), the session runs in supervised mode.
	AutonomousMode bool `json:"autonomous_mode,omitempty"`
	// AutonomousTurn is the current turn during an active autonomous run.
	AutonomousTurn int32 `json:"autonomous_turn,omitempty"`
	// AutonomousMaxTurns is the configured max turns for the current run.
	AutonomousMaxTurns int32 `json:"autonomous_max_turns,omitempty"`
	// AutonomousOutcome is the result of the last autonomous run: "", "done", or "stuck".
	AutonomousOutcome string `json:"autonomous_outcome,omitempty"`

	// GitHub integration fields for PR/URL-based session creation
	// GitHubPRNumber is the PR number if this session was created from a PR URL
	GitHubPRNumber int `json:"github_pr_number,omitempty"`
	// GitHubPRURL is the full URL to the PR on GitHub
	GitHubPRURL string `json:"github_pr_url,omitempty"`
	// GitHubOwner is the repository owner (user or organization)
	GitHubOwner string `json:"github_owner,omitempty"`
	// GitHubRepo is the repository name
	GitHubRepo string `json:"github_repo,omitempty"`
	// GitHubSourceRef is the original URL or reference used to create this session
	GitHubSourceRef string `json:"github_source_ref,omitempty"`
	// ClonedRepoPath is the path where we cloned the repo (if cloned)
	ClonedRepoPath string `json:"cloned_repo_path,omitempty"`
	// MainRepoPath is the path to the main repository when Path is a worktree
	// Detected automatically via `git rev-parse --git-common-dir`
	MainRepoPath string `json:"main_repo_path,omitempty"`
	// IsWorktree indicates whether Path is a git worktree (not the main repo)
	IsWorktree bool `json:"is_worktree,omitempty"`
	// GitHubIsFork is true when the remote repo is a fork (PR lookup uses upstream)
	GitHubIsFork bool `json:"github_is_fork,omitempty"`

	// PR status fields — populated by PRStatusPoller; not set on session creation
	// GitHubPRState is the PR lifecycle state: "open", "closed", "merged"
	GitHubPRState string `json:"github_pr_state,omitempty"`
	// GitHubPRIsDraft is true when the PR is in draft mode
	GitHubPRIsDraft bool `json:"github_pr_is_draft,omitempty"`
	// GitHubPRPriority is the derived priority: blocking/ready/pending/draft/complete/no_pr
	GitHubPRPriority string `json:"github_pr_priority,omitempty"`
	// GitHubApprovedCount is the count of current non-dismissed APPROVED reviews
	GitHubApprovedCount int `json:"github_approved_count,omitempty"`
	// GitHubChangesReqCount is the count of current non-dismissed CHANGES_REQUESTED reviews
	GitHubChangesReqCount int `json:"github_changes_req_count,omitempty"`
	// GitHubCheckConclusion is the CI rollup: success/failure/pending/action_required/neutral/""
	GitHubCheckConclusion string `json:"github_check_conclusion,omitempty"`
	// GitHubPRStatusTerminal is true when the PR is merged/closed and polling should stop
	GitHubPRStatusTerminal bool `json:"github_pr_status_terminal,omitempty"`
	// LastPRStatusCheck is when the PR status was last successfully fetched
	LastPRStatusCheck time.Time `json:"last_pr_status_check,omitempty"`

	Checkpoints      CheckpointList
	ActiveCheckpoint string
	ForkedFromID     string

	// OneShot runs claude in -p mode; the session exits after the task completes.
	OneShot bool

	// Hidden excludes this session from the default session list and review queue.
	// Set true for system/background sessions (triage, validation) that should not
	// appear in the user-facing session viewer.
	Hidden bool

	// ProjectID is the optional project this session belongs to.
	ProjectID string

	// HistoryFilePath is the path to the Claude conversation JSONL history file.
	// Set by HistoryLinker when it correlates this session to an open JSONL file.
	HistoryFilePath string

	// MCPServerURL is the URL of the stapler-squad HTTP MCP endpoint.
	// When set, passed as --mcp-config to claude on session start so no
	// settings-file injection is needed.
	MCPServerURL string `json:"mcp_server_url,omitempty"`

	// AppendSystemPrompt, when non-empty and the program is claude, passes
	// --append-system-prompt to inject extra instructions into the system prompt
	// without modifying any file on disk. Survives context compaction.
	AppendSystemPrompt string `json:"append_system_prompt,omitempty"`

	// AllowedTools, when non-empty, passes --allowedTools to claude to pre-approve
	// specific tool calls without requiring interactive permission prompts.
	// Format: "Bash,Read,Edit" or "Bash(git commit *),Read".
	AllowedTools string `json:"allowed_tools,omitempty"`

	// PermissionMode, when non-empty, passes --permission-mode to claude.
	// Values: "default", "acceptEdits", "bypassPermissions", "auto".
	PermissionMode string `json:"permission_mode,omitempty"`

	// CreationProgress holds a human-readable progress message during Creating state.
	// Set by the async creation goroutine; cleared once the session becomes Active.
	// Not persisted to the database — only meaningful in-memory during startup.
	CreationProgress string `json:"-"`

	// LaunchCommand is the full command passed to tmux on session start, including
	// any injected flags (--resume, --mcp-config, -y, initial prompt). Set once on
	// first start and updated on restart. Empty for external (mux-discovered) sessions.
	LaunchCommand string `json:"launch_command,omitempty"`

	// RateLimitAutoResume controls whether the rate-limit manager will automatically
	// send recovery input when a rate limit expires. Persisted so the setting survives
	// server restarts. Defaults to true (enabled) when zero value.
	RateLimitAutoResume *bool `json:"rate_limit_auto_resume,omitempty"`

	// PauseReason records why this session was paused. Use PauseReason* constants.
	// Empty when session has never been paused.
	PauseReason string `json:"pause_reason,omitempty"`

	// ExitReason records why this session's pane exited when Status == Crashed
	// (e.g. "signal SIGKILL (exit code 137)"). Empty otherwise. Set by
	// SessionHealthChecker when it detects a dead pane (session/health.go).
	ExitReason string `json:"exit_reason,omitempty"`

	// WorkflowID is the UUID of the Workflow that spawned this session.
	// Empty for manually-created sessions.
	WorkflowID string `json:"workflow_id,omitempty"`

	// EnvVars are session-level environment variables injected at tmux session creation.
	EnvVars map[string]string `json:"env_vars,omitempty"`
	// CLIFlags are additional CLI flags appended to the program launch command.
	CLIFlags string `json:"cli_flags,omitempty"`
	// ExtraArgs are additional argv elements appended verbatim (never whitespace-split) after
	// CLIFlags at launch time. Populated by a selected launcher preset's argv[1:]; see
	// buildLaunchCommand in instance_tmux.go for the shell-quoting boundary.
	ExtraArgs []string `json:"extra_args,omitempty"`

	// ArchivedAt is set when the session is archived. Nil means not archived.
	ArchivedAt *time.Time `json:"archived_at,omitempty"`

	// ReviewState holds all review queue and terminal activity timestamps.
	// Fields are embedded (promoted) so external code can still access inst.LastViewed etc.
	// Protected by mu (via sendSyncErr / Snapshot).
	ReviewState

	// Instance type and management metadata
	// InstanceType indicates whether this is a squad-managed or external instance
	InstanceType InstanceType
	// IsManaged is true if this is a squad-managed session (backward compatible helper)
	IsManaged bool
	// ExternalMetadata contains additional information for externally discovered instances
	ExternalMetadata *ExternalInstanceMetadata
	// Permissions defines what operations are allowed on this instance
	Permissions InstancePermissions

	// Artifacts holds structured artifacts extracted from the session's JSONL history.
	// Populated asynchronously by ArtifactExtractor. Protected by mu.
	Artifacts *artifacts.SessionArtifactsBlob
	// contains filtered or unexported fields
}

Instance is a running instance of claude code.

func CreateManagedInstance added in v1.42.0

func CreateManagedInstance(ctx context.Context, params CreateManagedInstanceParams) (*Instance, error)

CreateManagedInstance resolves path/session-type concerns, constructs a real *Instance via NewInstance, registers it in the live-handle registry (if provided), and persists it via Storage.AddInstance. It deliberately does NOT call instance.Start() -- starting tmux/the underlying process is left to the caller, which may want to do so asynchronously (as SessionService.CreateSession's handler does) or under additional preconditions (as CommitImportExternalSession does, e.g. after suspending the original process).

On any failure after a successful Registry.Register, the registration is rolled back via Registry.ForceRelease before returning the error, so no phantom live-handle entry is left behind.

func FromInstanceData

func FromInstanceData(data InstanceData) (*Instance, error)

FromInstanceData creates a new Instance from serialized data FromInstanceData reconstructs an *Instance from persisted data, starting it synchronously (hot-attaching to an already-live tmux session or cold-restoring one) before returning. Use for on-demand single-instance loads (e.g. Registry.Acquire) where the caller needs a ready instance immediately.

Bulk startup loads should use fromInstanceData(data, true) via LoadInstances instead — starting every instance synchronously here is what made server startup block on restoring all sessions (including cold-relaunching every dead one) before the HTTP server could bind. See server/dependencies.go's "Step 6" background goroutine, which already exists to start un-started instances asynchronously once the deferred path skips Start() here.

func NewInstance

func NewInstance(opts InstanceOptions) (*Instance, error)

func NewInstanceWithCleanup

func NewInstanceWithCleanup(opts InstanceOptions) (*Instance, tmux.CleanupFunc, error)

NewInstanceWithCleanup creates a new Instance and returns it along with a cleanup function. Usage: instance, cleanup, err := NewInstanceWithCleanup(opts); if err == nil { defer cleanup() }

func SessionToInstance

func SessionToInstance(s *Session) *Instance

SessionToInstance converts a Session back to the legacy Instance type. This adapter enables interoperability during the migration period. Note: Some Session features (like CloudContext) don't have Instance equivalents.

func (*Instance) AddShellInMemory added in v1.35.0

func (i *Instance) AddShellInMemory(sh *Shell)

AddShellInMemory registers a pre-built Shell directly into the in-memory registry without spawning a tmux process. Used by ReconcileShells and by tests that need to inject shells into an Instance without going through the full SpawnShell / tmux path.

func (*Instance) AddTag

func (i *Instance) AddTag(tag string) error

AddTag adds a tag to the instance. Delegates to TagManager.Add. Returns ErrTagTooLong if the tag exceeds MaxTagLength, or ErrDuplicateTag if it already exists.

func (*Instance) Approve

func (i *Instance) Approve() error

Approve transitions the instance to Active (approval granted). Returns an error if the current state does not allow this transition.

func (*Instance) ArchiveWithStop added in v1.41.0

func (i *Instance) ArchiveWithStop(t time.Time) error

ArchiveWithStop sets ArchivedAt and transitions the instance to Stopped, in a single actor command. ArchiveSession previously only set ArchivedAt, which let a session sit with ArchivedAt set but Status still Active/Paused/Hibernated — archiving is meant to mean "this session is done," so the two must move together.

func (*Instance) Attach

func (i *Instance) Attach() (chan struct{}, error)

Attach attaches to the tmux session and returns a done channel.

func (*Instance) CDPDisplayEnv added in v1.35.0

func (i *Instance) CDPDisplayEnv() []string

CDPDisplayEnv returns the extra environment variable strings to inject into the tmux session for CDP:

  • "CDP_PORT=<N>" — the allocated CDP debugging port
  • "PATH=<wrapperDir>:<original PATH>" — prepends the wrapper script dir so Chrome launcher scripts resolve to our wrappers, not the real binary

Returns nil if CDP is unavailable or if Allocate has not been called yet.

func (*Instance) CDPManager added in v1.35.0

func (i *Instance) CDPManager() CDPStreamManager

CDPManager returns the CDPStreamManager for this instance. Always non-nil after NewInstance() — returns a no-op manager when Chrome is unavailable.

func (*Instance) CaptureCurrentState

func (i *Instance) CaptureCurrentState() error

CaptureCurrentState records the pane's current working directory into WorkingDir. Called during graceful shutdown so cold restore can restart in the right directory. No-op if the session is not started, paused, or the tmux session is dead.

For a worktree session, a captured path outside the worktree is refused rather than persisted (BUG-033): live-confirmed on an autonomous backlog session whose own isolated worktree was created successfully and never touched, while its agent ran real work — two feature commits and a branch checkout — directly in the shared parent repo checkout instead, apparently after `cd`-ing there mid-task. resolveStartPath already had a read-side backstop against a stale out-of-worktree WorkingDir, but that guard only fires when i.gitManager.HasWorktree() happens to already be true at the moment a session (re)starts — not guaranteed on every restart ordering — so a bad path could still be captured here, persisted, and later used unguarded. Gating the write itself closes the gap at its source instead of only defending against it on read.

func (*Instance) CapturePaneContent

func (i *Instance) CapturePaneContent() (string, error)

CapturePaneContent captures the current visible tmux pane content. This is a simple wrapper around TmuxSession.CapturePaneContent() for compatibility with the terminal WebSocket handlers.

func (*Instance) CapturePaneContentPriority added in v1.44.0

func (i *Instance) CapturePaneContentPriority() (string, error)

CapturePaneContentPriority captures the current visible tmux pane content via the resync exec-gate fast lane (Epic 4.2) when the instance is tmux-backed, falling back to the plain CapturePaneContent() call for non-tmux-backed instances (there is no fast lane to reach there, and the plain call is a correct, if unoptimized, behavior).

func (*Instance) CapturePaneContentRaw

func (i *Instance) CapturePaneContentRaw() (string, error)

CapturePaneContentRaw captures pane content with ANSI codes preserved (no line joining). Essential for hybrid streaming where cursor positioning codes must be preserved.

func (*Instance) CleanupWorktree

func (i *Instance) CleanupWorktree() error

CleanupWorktree removes the git worktree, keeping session intact.

func (*Instance) ClearConversationState added in v1.35.0

func (i *Instance) ClearConversationState()

ClearConversationState removes the stored Claude conversation UUID and history file path so that the next Resume starts a fresh conversation rather than attempting --resume with a potentially stale or path-mismatched UUID.

func (*Instance) ClosePR

func (i *Instance) ClosePR() error

ClosePR closes the PR without merging Returns an error if this is not a PR session or if the GitHub API call fails

func (*Instance) CreateCheckpoint

func (i *Instance) CreateCheckpoint(label string, scrollbackSeq uint64) (*Checkpoint, error)

CreateCheckpoint captures a named state bookmark for this session. scrollbackSeq should be the current scrollback high-water mark (from ScrollbackManager); pass 0 if the caller does not have access to scrollback state. Thread-safe: routed through the actor mailbox. Returns an error if the instance is not started.

func (*Instance) CurrentBranch added in v1.35.0

func (i *Instance) CurrentBranch() string

CurrentBranch returns the branch the session is currently on. For worktree sessions, it returns the stored Branch field (set at creation and on worktree changes). For directory sessions, Branch is never stored, so it reads the branch live from the working directory via git. Returns "" if the branch cannot be determined.

func (*Instance) DeleteShell added in v1.35.0

func (i *Instance) DeleteShell(ctx context.Context, shellID string) error

DeleteShell stops a shell (if running), waits for active handlers to drain, then removes it from memory and the database.

func (*Instance) Deny

func (i *Instance) Deny() error

Deny transitions the instance to Paused (approval denied). Returns an error if the current state does not allow this transition.

func (*Instance) Destroy

func (i *Instance) Destroy() error

Destroy completely destroys the instance - both tmux session and worktree. Fires EventStopped unconditionally (even if the instance was never started) so listeners tracking "is this session now gone" — e.g. BacklogLifecycleListener's ItemSession.EndedAt bookkeeping — see every deliberate stop, not just natural exits.

func (*Instance) DetectAndPopulateWorktreeInfo

func (i *Instance) DetectAndPopulateWorktreeInfo() error

DetectAndPopulateWorktreeInfo detects if the instance path is a worktree and populates the IsWorktree, MainRepoPath, GitHubOwner, and GitHubRepo fields. NOTE: This method writes to GitHub fields (i.GitHubOwner, i.GitHubRepo) directly. A future pass could route writes through a setter method for encapsulation. This is useful for sessions created from existing worktrees where we want to display the actual repository information in the UI.

IMPORTANT: For sessions with git worktrees, we check BOTH paths: 1. The worktree path (gitWorktree.GetWorktreePath()) - to detect IsWorktree and MainRepoPath 2. The original path (i.Path) - as fallback for GitHub owner/repo if worktree detection fails

This is necessary because: - i.Path is the main repository path (e.g., ~/Documents/personal-wiki) - gitWorktree.GetWorktreePath() is the actual worktree (e.g., ~/.stapler-squad/worktrees/...) - The main repo has .git as a directory; the worktree has .git as a file pointing to the main repo

func (*Instance) FireLifecycleEventForTest added in v1.35.0

func (i *Instance) FireLifecycleEventForTest(event LifecycleEvent, reason string)

FireLifecycleEventForTest is the exported version of fireLifecycleEvent, used exclusively in cross-package tests that need to simulate an unexpected exit.

func (*Instance) ForceStatus added in v1.35.0

func (i *Instance) ForceStatus(s Status)

ForceStatus sets the instance status directly without state machine validation. Only call from error recovery paths where the normal transition would itself fail (e.g. the async-creation goroutine cannot cleanly call Stop() because the session was never fully started). Callers must hold no locks.

Routes through the actor mailbox (sendCtx) rather than taking i.mu directly: ForceStatus is invoked from ad hoc goroutines outside the actor (e.g. the async CreateSession goroutine in SessionService), not from inside an actor command. Funneling through sendCtx serializes this write with the actor's command loop when the instance is actor-owned (LiveInstance), and falls back to running synchronously in-place when it isn't (e.g. tests constructing a bare *Instance).

The write (loadStatus) and the buildSnapshot read are done under the SAME i.mu.Lock()/Unlock() critical section (not lock-write-then-unlock-then-read, which is what this used to do). buildSnapshot reads every mutable field, including ones mutated directly under i.mu by legacy setters (MarkViewed, SetLastMeaningfulOutput, MarkUserResponded, MarkAcknowledged, RecoverFromStopped) that bypass the actor entirely and run on arbitrary caller goroutines. Calling buildSnapshot() after releasing the lock left a window where one of those setters could mutate fields concurrently with this unguarded read — caught by -race via a concurrent MarkViewed()/ ForceStatus() pairing during CreateSession. See runActor's doc comment in actor.go for the matching fix on the read side.

func (*Instance) ForkFromCheckpoint

func (i *Instance) ForkFromCheckpoint(checkpointID, newTitle string, configDir string) (*Instance, error)

ForkFromCheckpoint creates a new, unstarted Instance that is an independent branch of i, seeded from the state captured at the checkpoint identified by checkpointID.

func (*Instance) GeneratePRContextPrompt

func (i *Instance) GeneratePRContextPrompt() (string, error)

GeneratePRContextPrompt generates a context prompt for Claude based on PR information This can be used to initialize a Claude Code session with comprehensive PR context Returns an error if this is not a PR session or if the GitHub API call fails

func (*Instance) GetCategoryPath

func (i *Instance) GetCategoryPath() []string

GetCategoryPath returns the category path as a slice of strings for nested category support Supports "Work/Frontend" syntax by splitting on "/" delimiter

func (*Instance) GetCheckpoints

func (i *Instance) GetCheckpoints() CheckpointList

GetCheckpoints returns a snapshot copy of the checkpoint list, safe for concurrent reads from outside the instance's lock domain.

func (*Instance) GetClaudeConversationUUID added in v1.35.0

func (i *Instance) GetClaudeConversationUUID() string

GetClaudeConversationUUID returns the stored Claude conversation UUID, empty if none. Thread-safe: acquires stateMutex read lock.

func (*Instance) GetClaudeSession

func (i *Instance) GetClaudeSession() *ClaudeSessionData

GetClaudeSession returns the Claude session data for this instance. Thread-safe: acquires stateMutex read lock.

func (*Instance) GetController

func (i *Instance) GetController() *ClaudeController

GetController returns the ClaudeController if one exists.

func (*Instance) GetConversationUUID

func (i *Instance) GetConversationUUID() string

GetConversationUUID returns the Claude conversation UUID, or "" if not linked. Thread-safe: acquires stateMutex read lock.

func (*Instance) GetCreatedAt added in v1.1.0

func (i *Instance) GetCreatedAt() time.Time

GetCreatedAt returns the time this instance was created. The field is immutable after creation.

func (*Instance) GetCurrentPaneContent

func (i *Instance) GetCurrentPaneContent(lines int) (string, error)

GetCurrentPaneContent captures the current visible tmux pane content. Delegates to processManager.CaptureViewport.

func (*Instance) GetDetectedContext added in v1.35.0

func (i *Instance) GetDetectedContext() string

GetDetectedContext returns the human-readable context string from the terminal detection layer. Returns an empty string when no controller is active or no context is available.

func (*Instance) GetDetectedStatus added in v1.35.0

func (i *Instance) GetDetectedStatus() detection.DetectedStatus

GetDetectedStatus returns the raw DetectedStatus from the terminal detection layer. Returns detection.StatusUnknown when no controller is active or no status has been detected. Use this for sub-status display; do not use for lifecycle decisions.

func (*Instance) GetDiffStats

func (i *Instance) GetDiffStats() *git.DiffStats

GetDiffStats returns the current git diff statistics.

func (*Instance) GetEffectiveRootDir

func (i *Instance) GetEffectiveRootDir() string

GetEffectiveRootDir returns the root directory where this session operates. For worktree sessions, this is the worktree path. For directory sessions, this is Path. Used for injecting configuration files (e.g., .claude/settings.local.json).

This returns the worktree path as recorded, without checking whether it still exists on disk — callers that do path-string correlation (e.g. HistoryLinker matching against ~/.claude/projects/<hashed-path>) need the nominal path regardless of whether the directory is currently present. For callers that need to actually read from the filesystem, use Workspace(), which falls back to the repo root when the worktree is gone.

func (*Instance) GetEffectiveStatus

func (i *Instance) GetEffectiveStatus() Status

GetEffectiveStatus returns the most accurate status for this instance, combining the lifecycle status with real-time terminal detection when available. Unlike Status (which only reflects lifecycle transitions), this consults the ClaudeController's detected terminal state to surface NeedsApproval, Idle, etc.

func (*Instance) GetEscapeParser added in v1.35.0

func (i *Instance) GetEscapeParser() *analytics.EscapeCodeParser

GetEscapeParser returns the escape code parser from the session's response stream. Returns nil if the controller is not running or has no response stream.

func (*Instance) GetExitContent added in v1.15.0

func (i *Instance) GetExitContent() []byte

GetExitContent returns the last terminal bytes captured before the PTY exited. Returns nil if the controller is not running or no exit content was recorded.

func (*Instance) GetGitHubRepoFullName

func (i *Instance) GetGitHubRepoFullName() string

GetGitHubRepoFullName returns "owner/repo" format, or empty string. Delegates to GitHubMetadataView.RepoFullName.

func (*Instance) GetGitWorktree

func (i *Instance) GetGitWorktree() (*git.GitWorktree, error)

GetGitWorktree returns the git worktree for the instance.

func (*Instance) GetLifecycleStatus added in v1.35.0

func (i *Instance) GetLifecycleStatus() Status

GetLifecycleStatus returns the current lifecycle status as a typed Status value.

func (*Instance) GetPRComments

func (i *Instance) GetPRComments() ([]github.PRComment, error)

GetPRComments fetches all comments on the PR Returns an error if this is not a PR session or if the GitHub API call fails

func (*Instance) GetPRDiff

func (i *Instance) GetPRDiff() (string, error)

GetPRDiff fetches the diff for the PR Returns an error if this is not a PR session or if the GitHub API call fails

func (*Instance) GetPRDisplayInfo

func (i *Instance) GetPRDisplayInfo() string

GetPRDisplayInfo returns a human-readable PR description for UI display. Delegates to GitHubMetadataView.PRDisplayInfo.

func (*Instance) GetPTYReader

func (i *Instance) GetPTYReader() (*os.File, error)

GetPTYReader returns the PTY file handle for the tmux session.

func (*Instance) GetPaneCursorPosition

func (i *Instance) GetPaneCursorPosition() (x, y int, err error)

GetPaneCursorPosition gets the current cursor position in the tmux pane. Returns cursor X (column) and Y (row) coordinates, both 0-based.

func (*Instance) GetPaneDimensions

func (i *Instance) GetPaneDimensions() (width, height int, err error)

GetPaneDimensions gets the current dimensions of the tmux pane. Returns width (columns) and height (rows).

func (*Instance) GetPanePID

func (i *Instance) GetPanePID() (int32, error)

GetPanePID returns the PID of the foreground process in the tmux pane. The DoesSessionExist guard is omitted here: TmuxSession.GetPanePID already uses the CM fast path (no subprocess) and falls back to display-message which returns an error if the session is gone. Avoiding a separate list-sessions call per instance keeps this cheap for HistoryLinker.ScanAll, which calls this sequentially (not fanned out) per session anyway -- and the display-message subprocess fallback is itself gated (session/tmux's exec gate), so there's no need for a second guard here even if that ever changes.

func (*Instance) GetPermissions

func (i *Instance) GetPermissions() InstancePermissions

GetPermissions returns the permissions for this instance based on its type.

func (*Instance) GetProgram added in v1.41.0

func (i *Instance) GetProgram() string

GetProgram returns the program this instance runs (e.g. "claude", "aider").

Reads via Snapshot(), not a direct i.Program field access: actor commands (SetProgram/SwitchProgram and friends) write i.Program directly outside i.mu, publishing the change only by atomically storing a fresh snapshot at the end of the mutation — the same reasoning as GetStatus's doc comment. ClaudeController.Start reads this from a different goroutine than whatever last set it, so only the atomic snapshot read is synchronized with that write; a direct field read or an i.mu-guarded read would not be.

func (*Instance) GetRateLimitResetTime added in v1.35.0

func (i *Instance) GetRateLimitResetTime() time.Time

GetRateLimitResetTime returns the time when the rate limit is expected to reset. Returns zero time if no controller is active or no reset time is known.

func (*Instance) GetRateLimitState added in v1.12.0

func (i *Instance) GetRateLimitState() int

GetRateLimitState returns the current rate limit detection state.

func (*Instance) GetReviewItem

func (i *Instance) GetReviewItem() (*ReviewItem, bool)

GetReviewItem returns the review item for this instance if it exists.

func (*Instance) GetReviewQueue

func (i *Instance) GetReviewQueue() *ReviewQueue

GetReviewQueue returns the review queue for this instance.

func (*Instance) GetScrollbackHistory

func (i *Instance) GetScrollbackHistory(startLine, endLine string) (string, error)

GetScrollbackHistory captures scrollback history from tmux using line ranges. Uses tmux's native scrollback capabilities instead of stored sequences. startLine and endLine follow tmux conventions: negative numbers go back from current position, use "-" for the start/end of history.

func (*Instance) GetSessionGoal added in v1.35.0

func (i *Instance) GetSessionGoal() *SessionGoalData

GetSessionGoal returns a thread-safe shallow copy of the current SessionGoalData (nil if not set). A copy is returned so callers cannot mutate the shared struct.

func (*Instance) GetShellExitCh added in v1.35.0

func (i *Instance) GetShellExitCh(shellID string) (<-chan struct{}, bool)

GetShellExitCh returns a channel that is closed when the shell exits. Multiple callers can select on it without coordination (closed-channel fan-out).

func (*Instance) GetShellPTYReader added in v1.35.0

func (i *Instance) GetShellPTYReader(shellID string) (*os.File, error)

GetShellPTYReader returns the PTY for streaming shell output. Lazily attaches if not yet attached (ADR-3: lazy PTY attach).

func (*Instance) GetShellTmuxSessionName added in v1.41.0

func (i *Instance) GetShellTmuxSessionName(shellID string) (string, bool)

GetShellTmuxSessionName returns the sibling tmux session name for a running shell, for callers outside this package (e.g. the WebSocket streaming handler) that need to target the shell's isolated PTY instead of the parent session's.

func (*Instance) GetStableID added in v1.14.0

func (i *Instance) GetStableID() string

GetStableID returns a stable identifier for this instance. If UUID is set, returns it. Falls back to Title for backward compatibility with sessions that pre-date UUID assignment.

func (*Instance) GetStatus added in v1.12.0

func (i *Instance) GetStatus() int

GetStatus returns the current lifecycle status of this instance as an int. This is intentionally returns int to implement the SessionAccessor interface.

Reads via Snapshot(), not i.mu.RLock(): actor commands (transitionToLocked and friends) write i.Status directly while running inside the actor's own serialization, not under i.mu, and only publish the change by atomically storing a fresh snapshot. An RLock here doesn't synchronize with that write at all — caught by -race via a concurrent GetStatus() poll during Start(). Do not call this from within a sendSyncErr/send/sendCtx closure (see Snapshot's reentrancy note).

func (*Instance) GetStatusIconForType

func (i *Instance) GetStatusIconForType() string

GetStatusIconForType returns the appropriate status icon based on instance type.

func (*Instance) GetStatusManager

func (i *Instance) GetStatusManager() *InstanceStatusManager

GetStatusManager returns the status manager.

func (*Instance) GetTags

func (i *Instance) GetTags() []string

GetTags returns a copy of the instance's tags. Delegates to TagManager.All.

func (*Instance) GetTimeSinceLastMeaningfulOutput

func (i *Instance) GetTimeSinceLastMeaningfulOutput() time.Duration

GetTimeSinceLastMeaningfulOutput returns how long ago meaningful output was recorded. Fast path: reads the atomic shadow (no lock) once initialised via SyncAtomicTimestamps or UpdateTimestamps. Fallback: Snapshot() when the atomic is zero (before first write, or in tests that set LastMeaningfulOutput directly) — not a fresh i.mu-guarded read, since i.mu doesn't synchronize with actor commands' direct field writes (see GetStatus's doc comment).

func (*Instance) GetTimeSinceLastTerminalUpdate

func (i *Instance) GetTimeSinceLastTerminalUpdate() time.Duration

GetTimeSinceLastTerminalUpdate delegates to ReviewState.TimeSinceLastTerminalUpdate. Falls back to time since creation if no terminal output has been recorded. Reads via Snapshot(), not i.mu.RLock() — see GetTimeSinceLastMeaningfulOutput.

func (*Instance) GetTitle added in v1.1.0

func (i *Instance) GetTitle() string

GetTitle returns the session title/name.

func (*Instance) GetTmuxSession

func (i *Instance) GetTmuxSession() *tmux.TmuxSession

GetTmuxSession returns the underlying tmux session for direct access. Returns nil if the session hasn't been started yet or if the backend is not tmux.

func (*Instance) GetTmuxSessionName added in v1.15.0

func (i *Instance) GetTmuxSessionName() string

GetTmuxSessionName returns the sanitized tmux session name for reconciliation. Returns empty string for external or uninitialized sessions.

func (*Instance) GetTotalBytesWritten added in v1.35.0

func (i *Instance) GetTotalBytesWritten() int64

GetTotalBytesWritten returns the monotonic PTY byte offset from the session's circular buffer. This is the same counter used by Stage 1 analytics so Stage 2 session_seq values remain stable across WebSocket reconnections. Returns 0 if no controller is active or the buffer is unavailable.

func (*Instance) GetVCSInfo

func (i *Instance) GetVCSInfo() (*VCSInfo, error)

GetVCSInfo returns information about the VCS for this session

func (*Instance) GetWorkingDirectory

func (i *Instance) GetWorkingDirectory() string

GetWorkingDirectory returns the working directory for this instance.

func (*Instance) GitHub

func (i *Instance) GitHub() GitHubMetadataView

GitHub returns a read-only view of the GitHub metadata for this instance.

func (*Instance) HasClaudeSession

func (i *Instance) HasClaudeSession() bool

HasClaudeSession returns true if this instance has Claude session data. Thread-safe: acquires stateMutex read lock.

func (*Instance) HasGitHubPR added in v1.35.0

func (i *Instance) HasGitHubPR() bool

HasGitHubPR reports whether a GitHub PR has been associated with this session. Safe for use from any goroutine.

func (*Instance) HasGitWorktree

func (i *Instance) HasGitWorktree() bool

HasGitWorktree returns true if the instance has a git worktree.

func (*Instance) HasTag

func (i *Instance) HasTag(tag string) bool

HasTag returns true if the instance has the specified tag. Delegates to TagManager.Has.

func (*Instance) HasUpdated

func (i *Instance) HasUpdated() (updated bool, hasPrompt bool)

HasUpdated reports whether terminal content has changed since the last check. Returns (updated, hasPrompt) and side-effects terminal timestamps on change.

func (*Instance) Hibernate added in v1.35.0

func (i *Instance) Hibernate(ctx context.Context) error

Hibernate transitions an Active session to Hibernated. It transitions state and dispatches the heavy I/O to a goroutine.

func (*Instance) Hibernated added in v1.35.0

func (i *Instance) Hibernated() bool

Hibernated returns true if the instance is hibernated.

func (*Instance) IsActive added in v1.35.0

func (i *Instance) IsActive() bool

IsActive returns true if the instance has a live AI process.

func (*Instance) IsCreating added in v1.35.0

func (i *Instance) IsCreating() bool

IsCreating returns true if the instance is in the Creating state.

Reads via Snapshot(), not i.mu.RLock() — see GetStatus's doc comment for why an RLock here doesn't actually synchronize with the actor's status writes.

func (*Instance) IsGitHubSession

func (i *Instance) IsGitHubSession() bool

IsGitHubSession returns true if this session has GitHub owner and repo set. Delegates to GitHubMetadataView.IsGitHubSession.

func (*Instance) IsHibernated added in v1.35.0

func (i *Instance) IsHibernated() bool

IsHibernated returns true if the instance has been hibernated (checkpoint written, tmux killed).

func (*Instance) IsPRSession

func (i *Instance) IsPRSession() bool

IsPRSession returns true if this session was created from a GitHub PR URL. Delegates to GitHubMetadataView.IsPRSession.

func (*Instance) IsPaused added in v1.35.0

func (i *Instance) IsPaused() bool

IsPaused returns true if the instance is paused (worktree removed, branch preserved).

func (*Instance) IsRateLimitEnabled added in v1.12.0

func (i *Instance) IsRateLimitEnabled() bool

IsRateLimitEnabled returns whether rate limit auto-resume is enabled. Returns the persisted RateLimitAutoResume field (default: true when nil).

func (*Instance) IsStopped added in v1.35.0

func (i *Instance) IsStopped() bool

IsStopped returns true if the instance is in the terminal Stopped state.

func (*Instance) Kill

func (i *Instance) Kill() error

Kill terminates the instance and cleans up all resources Kill destroys both tmux session and worktree (legacy method)

func (*Instance) KillExternalSession

func (i *Instance) KillExternalSession() error

KillExternalSession terminates an external mux session by killing its tmux session. This only works for external sessions that were started via ssq-mux with tmux integration. Returns an error if this is not an external instance or lacks tmux session name.

func (*Instance) KillSession

func (i *Instance) KillSession() error

KillSession terminates the tmux session only (leaves worktree intact).

func (*Instance) KillSessionKeepWorktree

func (i *Instance) KillSessionKeepWorktree() error

KillSessionKeepWorktree terminates tmux session but preserves worktree for recovery scenarios.

func (*Instance) LastMeaningfulOutputTime added in v1.1.0

func (i *Instance) LastMeaningfulOutputTime() time.Time

LastMeaningfulOutputTime returns the time of the last meaningful terminal output.

Fast path: the atomic shadow (no lock), same as GetTimeSinceLastMeaningfulOutput. Fallback: Snapshot(), not a fresh i.mu-guarded read — i.mu doesn't synchronize with actor commands' direct field writes (see GetStatus's doc comment).

func (*Instance) ListAvailableTargets

func (i *Instance) ListAvailableTargets() (*AvailableTargets, error)

ListAvailableTargets returns available switch targets (branches, bookmarks, worktrees)

func (*Instance) ListShellsInMemory added in v1.35.0

func (i *Instance) ListShellsInMemory() []*Shell

ListShellsInMemory returns in-memory shells sorted by OrderIndex.

func (*Instance) MarkAcknowledged added in v1.1.0

func (i *Instance) MarkAcknowledged()

MarkAcknowledged records that the user has acknowledged (dismissed) this session from the review queue.

func (*Instance) MarkCrashed added in v1.41.0

func (i *Instance) MarkCrashed(exitReason string) error

MarkCrashed kills the stale dead-pane tmux session and transitions the instance from Active to Crashed with exitReason recorded. Called by SessionHealthChecker when it detects an abnormally-exited dead pane (session/health.go) — remain-on-exit keeps the tmux session/pane around as a placeholder after the wrapped program exits, so TmuxAlive() alone never notices. Returns an error (and leaves the instance's Status/ExitReason untouched) if the instance is not Active when the actor executes this command. KillSession() itself still runs regardless of that check (see below) -- a truly-dead pane being killed is harmless even on the rare race where the status changed concurrently between health.go's detection and this call, and the in-actor guard below prevents that race from ever corrupting Status/ExitReason.

KillSession() runs on the CALLER's goroutine, outside the actor mailbox -- matching how the health checker called it directly before this status existed. KillSession() only touches i.pm() (no actor-owned state), so this is safe, and it keeps a slow/hung `tmux kill-session` from blocking every other operation on this instance's actor (writes, resizes, other RPCs) for its duration; only the transition + ExitReason write need the actor.

Fires EventExited on success (mirroring ReviewQueuePoller.reconcileSessions' "reconcile-session-missing" fire) so already-wired listeners -- the sessionExitedPublisher that pushes the new status to WatchSessions clients, and BacklogLifecycleListener (session/backlog_lifecycle.go) that notifies on EventExited/EventStopped -- pick this up without new plumbing. This is how a backlog automation session that crashes surfaces the failure instead of stalling silently. sessionSummaryListener also fires on this (it excludes only the reconciler's spurious "reconcile-session-missing" reason, per ADR-002 "natural exit and explicit stop dispatch identically") -- a crash is a real, confirmed exit (not a spurious signal), so generating a summary for it is consistent with every other genuine exit reason, not a new cost path.

func (*Instance) MarkExitedNormally added in v1.41.0

func (i *Instance) MarkExitedNormally() error

MarkExitedNormally kills the stale dead-pane tmux session and transitions the instance from Active to Stopped. Called by SessionHealthChecker when it detects a dead pane whose wrapped program exited normally (code 0, no signal) -- not a crash, so it must not be marked Crashed (see MarkCrashed). KillSession() runs outside the actor -- see MarkCrashed's doc comment. Fires EventExited on success -- see MarkCrashed's doc comment.

func (*Instance) MarkNeedsApproval added in v1.1.0

func (i *Instance) MarkNeedsApproval() error

MarkNeedsApproval is a no-op: NeedsApproval is no longer a lifecycle state. Approval state is now tracked as sub-status via the detection layer. Deprecated: do not call from new code.

func (*Instance) MarkUserResponded added in v1.1.0

func (i *Instance) MarkUserResponded() time.Time

MarkUserResponded records that the user has responded to this session. Returns the timestamp that was set so callers can persist it without a second lock acquisition.

func (*Instance) MarkViewed added in v1.1.0

func (i *Instance) MarkViewed()

MarkViewed records that the user has viewed this session.

func (*Instance) MatchesID added in v1.18.0

func (i *Instance) MatchesID(id string) bool

MatchesID reports whether id refers to this instance. Accepts the stable UUID, the legacy Title, or the full tmux session name (e.g. "staplersquad_my-session") so that hook notifications sent from inside managed tmux sessions are correctly attributed to their human-readable session.

func (*Instance) MergePR

func (i *Instance) MergePR(method string) error

MergePR merges the PR using the specified merge method method can be: "merge", "squash", or "rebase" Returns an error if this is not a PR session or if the GitHub API call fails

func (*Instance) NeedsReview

func (i *Instance) NeedsReview() bool

NeedsReview returns true if this session is in the review queue.

func (*Instance) PaneExitInfo added in v1.41.0

func (i *Instance) PaneExitInfo() (dead bool, code int, signal string)

PaneExitInfo reports whether the wrapped program's pane has exited (PaneProcessDead), along with its exit code and signal (empty string if none) when available. Used by SessionHealthChecker to distinguish a normal completion (exit code 0, no signal) from a genuine crash. code/signal are zero-valued when dead is false.

func (*Instance) PaneProcessDead added in v1.37.0

func (i *Instance) PaneProcessDead() bool

PaneProcessDead reports whether the tmux session is alive (TmuxAlive()==true) but the wrapped program running in the pane has already exited. remain-on-exit keeps the tmux session/pane around as a "Pane is dead (signal N, ...)" placeholder after the wrapped program is killed (e.g. OOM SIGKILL) or crashes, rather than tearing the session down -- so TmuxAlive() alone reports this session as healthy forever. Health checks must consult this in addition to TmuxAlive() to detect that failure mode. Returns false for non-tmux backends (e.g. native process manager), which have no equivalent placeholder state.

func (*Instance) Pause

func (i *Instance) Pause() error

Pause stops the tmux session and removes the worktree, preserving the branch.

func (*Instance) Paused

func (i *Instance) Paused() bool

Paused returns true if the instance is paused.

func (*Instance) PostComment

func (i *Instance) PostComment(body string) error

PostComment posts a comment to the PR Returns an error if this is not a PR session or if the GitHub API call fails

func (*Instance) Preview

func (i *Instance) Preview() (string, error)

Preview returns the current visible terminal content. Prefers tmux's own capture-pane (authoritative rendered screen) for tmux-backed instances; falls back to the in-memory PTY buffer from ClaudeController otherwise.

func (*Instance) PreviewFullHistory

func (i *Instance) PreviewFullHistory() (string, error)

PreviewFullHistory captures the entire tmux pane output including full scrollback history.

func (*Instance) ReconcileShells added in v1.35.0

func (i *Instance) ReconcileShells(ctx context.Context)

ReconcileShells is called after an Instance is loaded from ent on startup. It queries ent for shells marked "running" and checks whether their sibling tmux sessions still exist. Live sessions are rebuilt in memory (without PTY attach — lazy). Dead sessions are marked stopped in ent.

The map lock is never held during I/O: all subprocess calls and DB writes happen outside any map operation; each final map insert is an independent Store call that holds only the bucket lock for nanoseconds.

func (*Instance) RecoverFromStopped added in v1.23.1

func (i *Instance) RecoverFromStopped()

RecoverFromStopped resets a stale Stopped status to Creating so the instance can be hot-restored via Start(false). Only call this during startup reconciliation when the tmux session is confirmed alive; it bypasses the state machine intentionally. Deprecated: prefer transitionTo(ctx, Active) on the Stopped→Active path.

func (*Instance) RefreshPRInfo

func (i *Instance) RefreshPRInfo() (*github.PRInfo, error)

RefreshPRInfo fetches the latest PR information from GitHub Returns an error if this is not a PR session or if the GitHub API call fails

func (*Instance) RefreshTmuxClient

func (i *Instance) RefreshTmuxClient() error

RefreshTmuxClient forces the tmux client to refresh, triggering a redraw of the process running inside. This is critical after resizing to ensure cursor positions and line wrapping are recalculated for the new dimensions.

func (*Instance) RefreshTmuxClientPriority added in v1.44.0

func (i *Instance) RefreshTmuxClientPriority() error

RefreshTmuxClientPriority forces the tmux client to refresh via the resync exec-gate fast lane (Epic 4.2) when the instance is tmux-backed, falling back to the plain RefreshTmuxClient() call otherwise. See CapturePaneContentPriority's doc comment for the fallback rationale.

func (*Instance) RegisterLifecycleListener added in v1.15.0

func (i *Instance) RegisterLifecycleListener(l LifecycleListener)

RegisterLifecycleListener adds a listener that will receive EventStarted, EventExited, and EventStopped notifications for this instance. The listener is called synchronously on the goroutine that fires the event; implementations must return quickly (no long blocking operations).

func (*Instance) RegisterStatusChangeCallback added in v1.35.0

func (i *Instance) RegisterStatusChangeCallback(fn func(detection.DetectedStatus, string))

RegisterStatusChangeCallback appends fn to the controller's fan-out listener set. Unlike SetStatusChangeCallback, it does not replace existing listeners. Safe to call before or after the controller is started.

func (*Instance) RemoveTag

func (i *Instance) RemoveTag(tag string)

RemoveTag removes a tag from the instance. Delegates to TagManager.Remove.

func (*Instance) Rename

func (i *Instance) Rename(newTitle string) error

Rename renames this session. Validates title constraints and updates UpdatedAt.

func (*Instance) RepoName

func (i *Instance) RepoName() (string, error)

RepoName returns the name of the git repository. Returns an error if the instance has not been started or has no worktree.

func (*Instance) ResizePTY

func (i *Instance) ResizePTY(cols, rows int) error

ResizePTY resizes the terminal dimensions. This is used when clients resize their terminal windows.

func (*Instance) Restart

func (i *Instance) Restart(preserveOutput bool) error

Restart restarts the session by killing and recreating the tmux session. The git worktree is preserved during restart. If preserveOutput is true, captures terminal output before killing the session. For Claude sessions, uses --resume flag with the stored session ID.

func (*Instance) RestartShell added in v1.35.0

func (i *Instance) RestartShell(ctx context.Context, shellID string) error

RestartShell stops a shell (if running) and relaunches it with the same command and workdir.

func (*Instance) Resume

func (i *Instance) Resume() error

Resume recreates the worktree and restarts the tmux session

func (*Instance) ResumeFromCrash added in v1.41.0

func (i *Instance) ResumeFromCrash(ctx context.Context) error

ResumeFromCrash transitions a Crashed instance back to Active, relaunching the wrapped program. The tmux session was already killed by MarkCrashed, so Start(false) takes the cold-restore path and threads --resume automatically when a conversation UUID is known (see ClaudeCommandBuilder).

func (*Instance) ResumeFromHibernation added in v1.35.0

func (i *Instance) ResumeFromHibernation(ctx context.Context) error

ResumeFromHibernation transitions a Hibernated session back to Active. The actual process re-launch happens asynchronously via resumeFromHibernationLocked.

func (*Instance) RunWithResume added in v1.35.0

func (i *Instance) RunWithResume(ctx context.Context, message string) (string, error)

RunWithResume spawns a new claude subprocess using --resume <uuid> and -p <message>, waits for completion, and returns the result text. Updates ConversationUUID on success.

func (*Instance) SendInputViaControlMode added in v1.35.0

func (i *Instance) SendInputViaControlMode(ctx context.Context, data []byte) error

SendInputViaControlMode sends raw bytes through the existing control mode connection, avoiding the subprocess spawn overhead and timeout risk of exec.CommandContext.

func (*Instance) SendKeys

func (i *Instance) SendKeys(keys string) error

SendKeys sends keys to the tmux session.

func (*Instance) SendPrompt

func (i *Instance) SendPrompt(prompt string) error

SendPrompt sends a prompt to the tmux session. Delegates to processManager.SendPromptWithEnter.

func (*Instance) SetArchivedAt added in v1.35.0

func (i *Instance) SetArchivedAt(t *time.Time)

SetArchivedAt sets or clears the ArchivedAt timestamp atomically. Pass nil to clear (unarchive).

func (*Instance) SetArchivedAtIfNil added in v1.35.0

func (i *Instance) SetArchivedAtIfNil(t time.Time) bool

SetArchivedAtIfNil sets ArchivedAt to t only if it is currently nil. Returns true if the value was set (CAS semantics). Now actor-routed.

func (*Instance) SetArchivedAtIfNilAndStop added in v1.41.0

func (i *Instance) SetArchivedAtIfNilAndStop(t time.Time) bool

SetArchivedAtIfNilAndStop is the CAS counterpart of ArchiveWithStop, used by ArchiveSessionByUUID (which must be safe to call unconditionally from a sweep). Returns true if ArchivedAt was set by this call (i.e. it was previously nil). No-ops the status transition if already Stopped.

func (*Instance) SetArtifacts added in v1.35.0

func (i *Instance) SetArtifacts(blob *artifacts.SessionArtifactsBlob)

SetArtifacts atomically updates the in-memory Artifacts cache.

func (*Instance) SetAutoApprove added in v1.42.0

func (i *Instance) SetAutoApprove(v bool, persist func() error) error

SetAutoApprove sets the AutoApprove flag and, if the session is currently Active, restarts it so the flag takes effect immediately (the flag is baked into the launch command at spawn time, like Program -- not re-checked live, unlike AutonomousMode). persist is called before the restart so a crash between setting and restarting doesn't lose the change; the field mutation and persist survive even if the subsequent restart fails (matching SwitchProgram's documented ordering).

The full set->persist->restart sequence runs under restartTriggerMu, the same lock SwitchProgram holds for its own restart sequence, so a program switch and an auto-approve toggle firing near-simultaneously on the same instance serialize instead of both observing Status == Active and double-restarting the tmux session.

func (*Instance) SetAutoYes added in v1.35.0

func (i *Instance) SetAutoYes(v bool)

SetAutoYes sets the AutoYes flag. Used by daemon.go to opt in automated sessions to non-interactive behaviour.

func (*Instance) SetAutonomousComplete added in v1.35.0

func (i *Instance) SetAutonomousComplete(done bool)

SetAutonomousComplete clears the autonomous-mode flag and turn counters, and records the outcome ("done" or "stuck") atomically.

func (*Instance) SetAutonomousMode added in v1.35.0

func (i *Instance) SetAutonomousMode(mode bool, outcome string)

SetAutonomousMode sets the autonomous mode flag and outcome string atomically. Pass outcome="" to clear it when enabling; the existing value is preserved unless explicitly overwritten by the caller.

func (*Instance) SetAutonomousTurn added in v1.35.0

func (i *Instance) SetAutonomousTurn(turn, maxTurns int32)

SetAutonomousTurn atomically updates the current turn counter and max-turns cap during an active autonomous run.

func (*Instance) SetCategory added in v1.35.0

func (i *Instance) SetCategory(category string)

SetCategory sets the session category.

func (*Instance) SetClaudeConversationUUID added in v1.35.0

func (i *Instance) SetClaudeConversationUUID(uuid string)

SetClaudeConversationUUID stores the Claude conversation UUID so it is used in subsequent --resume flags. Fires the claudeSessionIDSavedCallback if set. No-op (including callback) if uuid is unchanged.

func (*Instance) SetClaudeSession

func (i *Instance) SetClaudeSession(sessionData *ClaudeSessionData)

SetClaudeSession sets the Claude session data for this instance. Thread-safe: acquires stateMutex write lock.

func (*Instance) SetClaudeSessionIDSavedCallback added in v1.35.0

func (i *Instance) SetClaudeSessionIDSavedCallback(fn func())

SetClaudeSessionIDSavedCallback registers a callback that fires when SetClaudeConversationUUID is called. Used by the service layer to trigger a storage save when the session_id is discovered.

func (*Instance) SetCommitStatus added in v1.45.0

func (i *Instance) SetCommitStatus(state github.CommitStatusState, statusContext, description string) error

SetCommitStatus posts a commit status to the PR's current head commit via the GitHub Statuses API. It re-fetches PR info first so the status always lands on the current HEAD SHA rather than a stale one cached before a force-push or rebase. Returns an error if this is not a PR session or if the GitHub API call fails.

func (*Instance) SetCreationProgress added in v1.35.0

func (i *Instance) SetCreationProgress(msg string)

SetCreationProgress sets the human-readable creation progress message.

func (*Instance) SetDirBaseSHA added in v1.37.0

func (i *Instance) SetDirBaseSHA(sha string)

SetDirBaseSHA sets the base commit SHA used to compute diff stats for directory-mode sessions (sessions without an isolated git worktree).

func (*Instance) SetExitReason added in v1.41.0

func (i *Instance) SetExitReason(reason string)

SetExitReason sets (or, passed "", clears) the reason this session's pane crashed.

func (*Instance) SetGitHubPR added in v1.35.0

func (i *Instance) SetGitHubPR(prURL string, prNumber int)

SetGitHubPR atomically sets the GitHub PR URL and PR number discovered after a RunOneShot or PR-discovery poll. Pass prNumber=0 if not yet known.

func (*Instance) SetGitHubPRNumber added in v1.35.0

func (i *Instance) SetGitHubPRNumber(n int)

SetGitHubPRNumber atomically updates the in-memory GitHubPRNumber field. Replaces the stateMutex-based implementation; now actor-routed so it is serialised with buildSnapshot.

func (*Instance) SetGitWorktree

func (i *Instance) SetGitWorktree(worktree *git.GitWorktree)

SetGitWorktree sets the git worktree for testing purposes.

func (*Instance) SetHibernateReason added in v1.35.0

func (i *Instance) SetHibernateReason(reason string)

SetHibernateReason sets the reason string that will be recorded in the checkpoint. Must be called before Hibernate(). Values: "manual", "idle", "resource_pressure".

func (*Instance) SetHistoryInfo

func (i *Instance) SetHistoryInfo(conversationUUID, historyFilePath string)

SetHistoryInfo updates the conversation UUID and history file path. Thread-safe: acquires stateMutex write lock. No-op if the UUID is already set to the same value. Fires the same claudeSessionIDSavedCallback as SetClaudeConversationUUID when the UUID actually changes, so a HistoryLinker-detected UUID is persisted to durable storage immediately rather than waiting on the next incidental full SaveInstances sweep (hibernation sweeper, health check) — a tmux pane killed before that sweep runs would otherwise resume with no conversation UUID to pass to --resume.

func (*Instance) SetLastAddedToQueue added in v1.35.0

func (i *Instance) SetLastAddedToQueue(t time.Time)

SetLastAddedToQueue records when this session was last added to the review queue.

func (*Instance) SetLastMeaningfulOutput added in v1.1.0

func (i *Instance) SetLastMeaningfulOutput(t time.Time)

SetLastMeaningfulOutput sets the time of the last meaningful terminal output.

func (*Instance) SetLastPRStatusCheck added in v1.35.0

func (i *Instance) SetLastPRStatusCheck(t time.Time)

SetLastPRStatusCheck records the time of the most recent PR-status fetch.

func (*Instance) SetMCPServerURL added in v1.35.0

func (i *Instance) SetMCPServerURL(url string)

SetMCPServerURL sets the MCP server URL on this instance.

func (*Instance) SetNote added in v1.41.0

func (i *Instance) SetNote(note string)

SetNote sets the session's free-form markdown note.

func (*Instance) SetPauseReason added in v1.35.0

func (i *Instance) SetPauseReason(reason string)

SetPauseReason sets the reason this session was paused.

func (*Instance) SetPreviewSize

func (i *Instance) SetPreviewSize(width, height int) error

SetPreviewSize sets the detached terminal dimensions for preview rendering.

func (*Instance) SetProgram added in v1.35.0

func (i *Instance) SetProgram(program string)

SetProgram atomically updates the Program field during program-switch.

func (*Instance) SetRateLimitCallbacks added in v1.35.0

func (i *Instance) SetRateLimitCallbacks(
	onDetected func(sessionID string, resetTime time.Time),
	onRecovery func(sessionID string, success bool, errMsg string),
)

SetRateLimitCallbacks registers server-layer callbacks for rate limit events. onDetected is called when a rate limit is detected; onRecovery is called when recovery completes. Both are invoked from goroutines in the ratelimit package. Safe to call before or after the controller is started; callbacks are wired at controller start time via wireRateLimitCallbacks.

func (*Instance) SetRateLimitEnabled added in v1.12.0

func (i *Instance) SetRateLimitEnabled(enabled bool)

SetRateLimitEnabled enables or disables rate limit auto-resume. The setting is persisted in RateLimitAutoResume so it survives restarts, and is applied immediately to the running controller if one exists.

func (*Instance) SetReviewQueue

func (i *Instance) SetReviewQueue(queue *ReviewQueue)

SetReviewQueue sets the review queue for this instance.

func (*Instance) SetSessionGoalCached added in v1.35.0

func (i *Instance) SetSessionGoalCached(g *SessionGoalData)

SetSessionGoalCached atomically updates the in-memory sessionGoal cache.

func (*Instance) SetShellRepository added in v1.35.0

func (i *Instance) SetShellRepository(repo ShellRepository)

SetShellRepository injects the shell persistence backend. Called by Storage after loading or creating an instance. Pass nil to disable persistence (e.g., in tests).

func (*Instance) SetStatusChangeCallback added in v1.35.0

func (i *Instance) SetStatusChangeCallback(fn func(detection.DetectedStatus, string))

SetStatusChangeCallback registers fn to be called on every terminal status change detected by the ClaudeController. Safe to call before or after the controller is started; the callback is wired at controller start time via wireStatusChangeCallback.

func (*Instance) SetStatusManager

func (i *Instance) SetStatusManager(manager *InstanceStatusManager)

SetStatusManager sets the status manager for idle detection.

func (*Instance) SetTags

func (i *Instance) SetTags(tags []string) error

SetTags replaces all tags with a new deduplicated set. Delegates to TagManager.Set. Returns ErrTagTooLong on the first tag that exceeds MaxTagLength.

func (*Instance) SetTitle

func (i *Instance) SetTitle(title string) error

SetTitle sets the title of the instance. Returns an error if the instance has started. We can't change the title once it's been used for a tmux session etc.

func (*Instance) SetTitleDirect added in v1.35.0

func (i *Instance) SetTitleDirect(title string)

SetTitleDirect sets the Title field directly without tmux-session constraints. Use only from RPC handlers that have already validated uniqueness and title constraints (e.g. UpdateSession, RenameSession rollback).

func (*Instance) SetTmuxSession

func (i *Instance) SetTmuxSession(session *tmux.TmuxSession)

SetTmuxSession sets the tmux session for testing purposes.

func (*Instance) SetWindowSize

func (i *Instance) SetWindowSize(cols, rows int) error

SetWindowSize propagates window size changes to the tmux session. This enables proper terminal resizing in environments like IntelliJ where SIGWINCH doesn't work.

func (*Instance) SetWorkingDir added in v1.35.0

func (i *Instance) SetWorkingDir(dir string)

SetWorkingDir sets the working directory for this session.

func (*Instance) Snapshot added in v1.35.0

func (i *Instance) Snapshot() *InstanceSnapshot

Snapshot returns the most recently published atomic snapshot of this Instance's mutable fields. The returned pointer is never nil. Callers must not mutate the returned struct.

On the first call for an Instance that bypassed finishInstanceConstruction (e.g. struct literals in tests), the snapshot is built lazily under stateMutex and stored via CAS so concurrent first-callers converge on one value.

func (*Instance) SpawnShell added in v1.35.0

func (i *Instance) SpawnShell(ctx context.Context, req SpawnShellRequest) (*Shell, error)

SpawnShell creates and starts a new shell as an independent sibling tmux session. It persists the shell to the ent repository, registers it in memory, and launches the watchShellExit goroutine.

func (*Instance) Start

func (i *Instance) Start(firstTimeSetup bool) error

Start starts the instance by routing through the actor mailbox. firstTimeSetup is true if this is a new instance. Otherwise, it's one loaded from storage.

func (*Instance) StartControlMode added in v1.15.0

func (i *Instance) StartControlMode() error

StartControlMode starts the control mode stream on the underlying tmux session.

func (*Instance) StartController

func (i *Instance) StartController() error

StartController creates and starts a ClaudeController for this instance. The controller enables automated idle detection and queue management.

func (*Instance) StartWithCleanup

func (i *Instance) StartWithCleanup(firstTimeSetup bool) (tmux.CleanupFunc, error)

StartWithCleanup starts the instance and returns a cleanup function. Usage: cleanup, err := instance.StartWithCleanup(firstTimeSetup); if err == nil { defer cleanup() }

func (*Instance) Started

func (i *Instance) Started() bool

Started returns true if the instance has been started.

func (*Instance) StopControlMode added in v1.15.0

func (i *Instance) StopControlMode() error

StopControlMode stops the control mode stream.

func (*Instance) StopController

func (i *Instance) StopController()

StopController stops and cleans up the ClaudeController for this instance.

func (*Instance) StopShell added in v1.35.0

func (i *Instance) StopShell(ctx context.Context, shellID string) error

StopShell stops a running shell by setting status first (stop-while-streaming guard), then closing the handle.

func (*Instance) SubscribeControlModeUpdates added in v1.15.0

func (i *Instance) SubscribeControlModeUpdates() (string, <-chan []byte)

SubscribeControlModeUpdates returns a subscriber ID and a read-only output channel. Returns a pre-closed channel if the tmux session is not available.

func (*Instance) SwitchProgram added in v1.37.0

func (i *Instance) SwitchProgram(ctx context.Context, rawProgram string, persist func() error) (changed bool, resolvedProgram string, err error)

SwitchProgram atomically switches this instance's Program to rawProgram (resolving an empty string to the configured default), porting Claude<->Antigravity conversation history when crossing between those two and clearing stale conversation linkage (ClearConversationState) when the switch leaves that family entirely. If persist is non-nil it runs after the field mutation but before an Active-session restart, so callers can make the new program durable even if the subsequent restart fails.

The whole operation runs under a per-instance lock (restartTriggerMu, shared with SetAutoApprove) so a manual program-switch request, an automatic capacity-monitor fallback, and a post-creation auto-approve toggle firing near-simultaneously serialize instead of double-restarting or double-porting history. This is the single implementation shared by the UpdateSession RPC handler and the capacity-monitor auto-fallback path (SessionService.UpdateSessionProgram) so the two entry points can't drift.

changed reports whether the resolved program actually differed from the current one; a no-op skips persist/restart entirely. err is only ever a Restart failure — persist failures are logged, not returned, matching the pre-existing best-effort save semantics.

func (*Instance) SwitchWorkspace

func (i *Instance) SwitchWorkspace(req WorkspaceSwitchRequest) (*WorkspaceSwitchResult, error)

SwitchWorkspace switches the session's workspace according to the request. For directory changes, this is a simple cd operation. For revision/worktree switches, this restarts Claude with --resume to preserve conversation.

func (*Instance) TapEnter

func (i *Instance) TapEnter()

TapEnter sends an enter key press to the tmux session if AutoYes is enabled.

func (*Instance) TmuxAlive

func (i *Instance) TmuxAlive() bool

TmuxAlive returns true if the tmux session is alive. This is a sanity check before attaching. TmuxAlive intentionally does not special-case Hibernated or Crashed here (unlike Paused/Stopped): both rely on their tmux session having actually been killed (Hibernate()/MarkCrashed) to make !i.pm().HasSession() true. If that kill ever fails, TmuxAlive() can still report true for either status -- ReviewQueuePoller's reconcileSessions Hibernated-but-alive and Crashed-but-alive cases exist as the safety net for exactly that scenario.

func (*Instance) TmuxSessionExists added in v1.23.1

func (i *Instance) TmuxSessionExists() bool

TmuxSessionExists reports whether the underlying tmux session is currently alive. Used at startup to reconcile stale Stopped status against live tmux sessions.

func (*Instance) ToInstanceData

func (i *Instance) ToInstanceData() InstanceData

ToInstanceData converts an Instance to its serializable form

Builds a fresh snapshot via sendSyncErr rather than reading the cached Snapshot(): callers like Storage.UpdateInstance/SaveInstancesSync routinely mutate exported fields (Tags, Category, ...) directly and expect the very next ToInstanceData() call to reflect that — Snapshot()'s cache only refreshes when an actor command republishes it, so those direct-mutation callers would see stale data (caught by TestStorage_UpdateInstance / TestStorage_SaveInstancesSync). Routing the build through sendSyncErr gets a fresh buildSnapshot(s.inst) (current field values, actor-mutation or not) while still serializing against concurrent actor commands: with a live actor this blocks until the mailbox delivers it, matching every other actor command's ordering; with no live actor (tests, pre-NewLiveInstance) it runs synchronously on the calling goroutine, same as before. Do not call from within a sendSyncErr/send/sendCtx closure — see actor.go. LaunchCommand is not in the snapshot (set once during Start) and is read directly. gitManager and claudeSession sub-objects have their own synchronisation.

func (*Instance) ToSession

func (i *Instance) ToSession() *Session

ToSession converts this Instance to the new Session type. This is a convenience method that wraps InstanceToSession.

func (*Instance) UnsubscribeControlModeUpdates added in v1.15.0

func (i *Instance) UnsubscribeControlModeUpdates(id string)

UnsubscribeControlModeUpdates removes a subscriber by ID.

func (*Instance) UpdateDiffStats

func (i *Instance) UpdateDiffStats() error

UpdateDiffStats updates the git diff statistics for this instance. Performs I/O (git diff) outside the lock, then updates state under the write lock.

func (*Instance) UpdatePRStatus added in v1.12.0

func (i *Instance) UpdatePRStatus(state, priority, checkConclusion string, approvedCount, changesReqCount int, isDraft, terminal bool) prUpdateResult

UpdatePRStatus atomically updates the PR status fields on this instance. Called by PRStatusPoller on each successful fetch. Returns prUpdateResult indicating whether the priority changed.

func (*Instance) UpdateTerminalTimestamps

func (i *Instance) UpdateTerminalTimestamps(content string, forceUpdate bool)

UpdateTerminalTimestamps is a coordinator method that bridges ProcessManager (I/O) with ReviewState (timestamp recording). It:

  1. Calls processManager.FilterBanners/HasMeaningfulContent (I/O-ish, done before touching the actor — same "no I/O inside the command" discipline as the other *Locked helpers)
  2. Routes the actual field mutation through the actor's send() — this is called from the PTY-read hot path (server/services/session_service.go's StreamTerminal), exactly the "callback that must not block the caller" case send() exists for (see actor.go's doc comment). Routing through the actor instead of i.mu means this mutation is serialized against every other actor command (transitionToLocked et al.), which don't take i.mu either — caught by -race via a concurrent StreamTerminal + StartController flow.
  3. Delegates to ReviewState.UpdateTimestamps

This method intentionally stays on Instance because it coordinates two sub-managers. The forceUpdate parameter bypasses meaningful content checking for user-initiated interactions.

func (*Instance) VNCDisplayEnv added in v1.35.0

func (i *Instance) VNCDisplayEnv() string

VNCDisplayEnv returns the DISPLAY environment variable assignment for this session's display, e.g. "DISPLAY=:101" or "DISPLAY=:0". Returns "" if no display is available (VNC unavailable or StartDisplay not yet called).

func (*Instance) VNCManager added in v1.35.0

func (i *Instance) VNCManager() VNCProcessManager

VNCManager returns the VNCProcessManager for this instance. Always non-nil — returns a no-op manager on unsupported platforms.

func (*Instance) Workspace added in v1.12.0

func (i *Instance) Workspace() Workspace

Workspace returns where this session is operating. Use this as the single source of truth for path resolution instead of accessing inst.Path directly, which is wrong for worktree sessions.

Falls back to RepoRoot if the worktree path no longer exists on disk (e.g. a paused session's worktree was removed while its branch/metadata persisted) — otherwise filesystem-reading callers like ListFiles would try to read a directory that's gone and surface a bare "directory not found: ." with no indication why.

func (*Instance) WorkspaceKey added in v1.41.0

func (i *Instance) WorkspaceKey() string

WorkspaceKey returns this instance's workspace identity. See the package-level WorkspaceKey function for the derivation rules.

func (*Instance) WriteToPTY

func (i *Instance) WriteToPTY(data []byte) (int, error)

WriteToPTY writes data to the PTY, sending input to the terminal session. This is used for forwarding client input to the tmux session.

type InstanceAcquirer added in v1.35.0

type InstanceAcquirer interface {
	Acquire(sessionID string) (*LiveInstance, ReleaseFunc, error)
}

InstanceAcquirer is the narrowest interface for callers that only ever call Acquire. WorkspaceService, MCP tool handlers, and most RPC handlers should be typed against this rather than *Registry (Interface Segregation — matches WorkspaceService's existing LiveInstanceFinder convention).

type InstanceContext added in v1.1.0

type InstanceContext interface {
	GetTitle() string
	GetStableID() string
	GetPTYReader() (*os.File, error)
	Preview() (string, error)
	LastMeaningfulOutputTime() time.Time
	GetCreatedAt() time.Time
	SetLastMeaningfulOutput(t time.Time)
	GetStatus() int
	WriteToPTY(data []byte) (int, error)
	GetProgram() string
}

InstanceContext is the narrow interface ClaudeController needs from its owning Instance. Using an interface breaks the bidirectional Instance ↔ ClaudeController dependency.

type InstanceData

type InstanceData struct {
	Title         string    `json:"title"`
	UUID          string    `json:"uuid,omitempty"`
	Path          string    `json:"path"`
	WorkingDir    string    `json:"working_dir"`
	Branch        string    `json:"branch"`
	Status        Status    `json:"status"`
	Height        int       `json:"height"`
	Width         int       `json:"width"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
	AutoYes       bool      `json:"auto_yes"`
	AutoApprove   bool      `json:"auto_approve"`
	Prompt        string    `json:"prompt"`
	InitialPrompt string    `json:"initial_prompt,omitempty"`

	Program          string          `json:"program"`
	ExistingWorktree string          `json:"existing_worktree,omitempty"`
	Worktree         GitWorktreeData `json:"worktree"`
	DiffStats        DiffStatsData   `json:"diff_stats"`

	// New fields for session organization and grouping
	Category   string   `json:"category,omitempty"`
	Note       string   `json:"note,omitempty"`
	IsExpanded bool     `json:"is_expanded,omitempty"`
	Tags       []string `json:"tags,omitempty"` // Multi-valued tags for flexible organization

	// Session type determines the workflow (directory, new_worktree, existing_worktree)
	SessionType SessionType `json:"session_type,omitempty"`

	// GitHub integration fields for PR/URL-based session creation
	GitHubPRNumber  int    `json:"github_pr_number,omitempty"`
	GitHubPRURL     string `json:"github_pr_url,omitempty"`
	GitHubOwner     string `json:"github_owner,omitempty"`
	GitHubRepo      string `json:"github_repo,omitempty"`
	GitHubSourceRef string `json:"github_source_ref,omitempty"`
	ClonedRepoPath  string `json:"cloned_repo_path,omitempty"`
	// Worktree detection fields
	MainRepoPath string `json:"main_repo_path,omitempty"` // Path to main repo when this is a worktree
	IsWorktree   bool   `json:"is_worktree,omitempty"`    // True if path is a git worktree
	GitHubIsFork bool   `json:"github_is_fork,omitempty"` // True when remote repo is a fork
	// PR status fields — populated by PRStatusPoller
	GitHubPRState          string    `json:"github_pr_state,omitempty"`
	GitHubPRIsDraft        bool      `json:"github_pr_is_draft,omitempty"`
	GitHubPRPriority       string    `json:"github_pr_priority,omitempty"`
	GitHubApprovedCount    int       `json:"github_approved_count,omitempty"`
	GitHubChangesReqCount  int       `json:"github_changes_req_count,omitempty"`
	GitHubCheckConclusion  string    `json:"github_check_conclusion,omitempty"`
	GitHubPRStatusTerminal bool      `json:"github_pr_status_terminal,omitempty"`
	LastPRStatusCheck      time.Time `json:"last_pr_status_check,omitempty"`
	// Crew autonomy mode — when true, the Fixer injects correction prompts without user confirmation.
	AutonomousMode bool `json:"autonomous_mode,omitempty"`

	// Claude Code session persistence
	ClaudeSession ClaudeSessionData `json:"claude_session,omitempty"`
	// Tmux session prefix for isolation
	TmuxPrefix string `json:"tmux_prefix,omitempty"`
	// Tmux server socket name for isolation (used with tmux -L flag)
	TmuxServerSocket string `json:"tmux_server_socket,omitempty"`

	// Terminal update timestamps for activity tracking
	LastTerminalUpdate   time.Time `json:"last_terminal_update,omitempty"`
	LastMeaningfulOutput time.Time `json:"last_meaningful_output,omitempty"`

	// Content signature for detecting actual terminal changes vs restarts
	// This is a SHA256 hash of the terminal content used to prevent false "new activity"
	// notifications when app restarts but terminal content hasn't changed
	LastOutputSignature string `json:"last_output_signature,omitempty"`

	// Review queue spam prevention
	LastAddedToQueue time.Time `json:"last_added_to_queue,omitempty"`

	// User interaction tracking
	// LastViewed tracks when the user last viewed this session (terminal, session details, etc.)
	// Used for smarter review queue notifications (don't notify if just viewed)
	LastViewed time.Time `json:"last_viewed,omitempty"`

	// Review queue snooze tracking
	// LastAcknowledged tracks when the user last dismissed this session from review queue
	// Sessions acknowledged after their last update won't appear in the queue until they update again
	LastAcknowledged time.Time `json:"last_acknowledged,omitempty"`

	// Prompt detection and interaction tracking for smart review queue behavior
	LastPromptDetected   time.Time `json:"last_prompt_detected,omitempty"`
	LastPromptSignature  string    `json:"last_prompt_signature,omitempty"`
	LastUserResponse     time.Time `json:"last_user_response,omitempty"`
	ProcessingGraceUntil time.Time `json:"processing_grace_until,omitempty"`

	// Checkpoint metadata for session state bookmarking (session resumption)
	Checkpoints      CheckpointList `json:"checkpoints,omitempty"`
	ActiveCheckpoint string         `json:"active_checkpoint,omitempty"`
	ForkedFromID     string         `json:"forked_from_id,omitempty"`

	// History file linkage for cold restore
	HistoryFilePath string `json:"history_file_path,omitempty"`

	// OneShot runs claude in -p mode; session exits after task completes.
	OneShot bool `json:"one_shot,omitempty"`

	// Hidden excludes this session from the default session list and review queue.
	Hidden bool `json:"hidden,omitempty"`

	// ProjectID is the optional project this session belongs to.
	ProjectID string `json:"project_id,omitempty"`

	// LaunchCommand is the full command passed to tmux on session start, including
	// any injected flags (--resume, --mcp-config, -y, initial prompt).
	LaunchCommand string `json:"launch_command,omitempty"`

	// MCPServerURL is the stapler-squad HTTP MCP endpoint passed to claude via
	// --mcp-config on session start. Persisted so restarts re-inject the flag.
	MCPServerURL string `json:"mcp_server_url,omitempty"`

	// PauseReason records why this session was paused.
	// Values: "manual", "auto:inactivity", "auto:session_limit", "auto:resource".
	// Empty when session has never been paused.
	PauseReason string `json:"pause_reason,omitempty"`

	// ExitReason records why this session's pane crashed (Status == Crashed).
	// Empty otherwise. Set by SessionHealthChecker (session/health.go).
	ExitReason string `json:"exit_reason,omitempty"`

	// WorkflowID is the UUID of the Workflow that spawned this session.
	// Empty for manually-created sessions.
	WorkflowID string `json:"workflow_id,omitempty"`

	// ArchivedAt is set when the session is archived. Nil means not archived.
	ArchivedAt *time.Time `json:"archived_at,omitempty"`
}

InstanceData represents the serializable data of an Instance

func (InstanceData) GetStableID added in v1.35.0

func (d InstanceData) GetStableID() string

GetStableID mirrors Instance.GetStableID for InstanceData: returns UUID when set, Title otherwise. Used by Registry.AcquireAll and ListInstanceIDs to produce stable per-session keys without constructing live Instance objects.

func (InstanceData) MatchesID added in v1.35.0

func (d InstanceData) MatchesID(id string) bool

MatchesID reports whether id refers to this InstanceData. Unlike Instance.MatchesID, there is no tmux-name arm because InstanceData has no GetTmuxSessionName (that method requires the live processManager). For tmux-name matching, call Instance.MatchesID.

func (InstanceData) WorkspaceKey added in v1.41.0

func (d InstanceData) WorkspaceKey() string

WorkspaceKey returns this instance data's workspace identity. See the package-level WorkspaceKey function for the derivation rules.

type InstanceOptions

type InstanceOptions struct {
	// Title is the title of the instance.
	Title string
	// Path is the path to the workspace repository root.
	Path string
	// WorkingDir is the directory within the repository to start in.
	// If empty, defaults to repository root.
	WorkingDir string
	// Branch is the git branch name to use when creating a new worktree.
	// If empty and SessionType is SessionTypeNewWorktree, a branch name is derived from the title.
	Branch string
	// Program is the program to run in the instance (e.g. "claude", "aider --model ollama_chat/gemma3:1b")
	Program string
	// If AutoYes is true, automatically accept prompts
	AutoYes bool
	// AutoApprove mirrors Instance.AutoApprove — see its doc comment.
	AutoApprove bool
	// Prompt is passed as a CLI argument at process-spawn time — only takes effect on a fresh
	// spawn or OneShot. See InitialPrompt for the tmux-typed alternative; the two are independent
	// and may both be set (see Instance.Prompt/Instance.InitialPrompt for the full explanation).
	Prompt string
	// InitialPrompt, when non-empty, is typed into the tmux pane once the session reaches Ready state,
	// replacing the static "Please proceed..." fallback. Use for resume/attach flows where a CLI
	// arg can no longer be injected.
	InitialPrompt string
	// ExistingWorktree is an optional path to an existing worktree to reuse
	ExistingWorktree string
	// Category is used for organizing sessions into groups
	Category string
	// Note is a user-authored free-form markdown note attached to this session.
	Note string
	// Tags are multi-valued labels for flexible organization
	Tags []string
	// SessionType determines the session workflow (directory, new_worktree, existing_worktree)
	SessionType SessionType
	// TmuxPrefix is the prefix to use for tmux session names (e.g., "staplersquad_")
	TmuxPrefix string
	// TmuxServerSocket is the server socket name for tmux isolation (used with -L flag)
	// If empty, uses the default tmux server. For complete isolation (e.g., testing),
	// set to a unique value like "test" or "teatest_123" to create separate tmux servers.
	TmuxServerSocket string
	// GitHub integration fields for PR/URL-based session creation
	GitHubPRNumber  int    // PR number if created from PR URL
	GitHubPRURL     string // Full URL to the PR
	GitHubOwner     string // Repository owner
	GitHubRepo      string // Repository name
	GitHubSourceRef string // Original URL/reference used to create session
	ClonedRepoPath  string // Path where repo was cloned (if cloned)

	// ResumeId is the Claude conversation ID to resume (from history browser).
	// When set, the session will start with --resume <id> flag.
	ResumeId string

	// OneShot runs claude in -p mode; the session exits after the task completes.
	OneShot bool

	// Hidden excludes the session from the default session list and review queue.
	Hidden bool

	// ProjectID associates the session with a project.
	ProjectID string

	// MCPServerURL, when non-empty and the program is claude, passes
	// --mcp-config '{"stapler-squad":{"type":"http","url":"<MCPServerURL>"}}' so the
	// session can call back into stapler-squad without any file injection.
	MCPServerURL string

	// AppendSystemPrompt, when non-empty and the program is claude, passes
	// --append-system-prompt so extra instructions are injected into the system
	// prompt without touching any file on disk.
	AppendSystemPrompt string

	// AllowedTools pre-approves specific Claude Code tool calls (--allowedTools).
	AllowedTools string
	// PermissionMode sets Claude Code's permission handling mode (--permission-mode).
	PermissionMode string

	// CreateIfMissing: when SessionTypeDirectory, create the directory and run git init
	// if the path does not exist. Only set when the user has confirmed the action.
	CreateIfMissing bool

	// AutonomousMode, when true, starts an AutonomousDriver after session creation
	// so the session runs to completion without manual steering.
	AutonomousMode bool

	// WorkflowID is the UUID of the Workflow that spawned this session.
	// Set by the scheduler; empty for manually-created sessions.
	WorkflowID string

	// EnvVars are session-level environment variables injected at tmux session creation time.
	EnvVars map[string]string
	// CLIFlags are additional CLI flags appended to the program launch command.
	CLIFlags string
	// ExtraArgs are additional argv elements appended verbatim (never whitespace-split) after
	// CLIFlags at launch time.
	ExtraArgs []string
}

Options for creating a new instance

type InstancePermissions

type InstancePermissions struct {
	// View operations
	CanView bool

	// Attach to the terminal session
	CanAttach bool

	// Send commands to the terminal
	CanSendCommand bool

	// Pause the session (stop tmux, keep worktree)
	CanPause bool

	// Resume a paused session
	CanResume bool

	// Destroy the session completely
	CanDestroy bool

	// Perform git operations (commit, push, worktree management)
	CanModifyGit bool

	// Add to review queue
	CanAddToQueue bool

	// RequiresConfirmation maps operation names to whether they need confirmation
	// Used for high-risk operations on external instances
	RequiresConfirmation map[string]bool
}

InstancePermissions defines what operations are allowed on an instance

func GetExternalPermissions

func GetExternalPermissions(allowAttach bool) InstancePermissions

GetExternalPermissions returns limited permissions for external instances allowAttach controls whether attach operations are permitted (power user mode)

func GetManagedPermissions

func GetManagedPermissions() InstancePermissions

GetManagedPermissions returns full permissions for squad-managed instances

func GetMuxExternalPermissions

func GetMuxExternalPermissions() InstancePermissions

GetMuxExternalPermissions returns permissions for mux-enabled external instances. Mux instances support full bidirectional terminal access and can be destroyed since they're explicitly opted-in by launching through ssq-mux with tmux session.

type InstanceReader added in v1.35.0

type InstanceReader interface {
	// Identity
	GetTitle() string
	GetStableID() string

	// Descriptive metadata
	GetWorkingDirectory() string

	// GetStatus returns the current lifecycle status as int.
	// Deprecated: use GetLifecycleStatus() or the typed predicates below.
	GetStatus() int

	// GetLifecycleStatus returns the current lifecycle status as a typed Status value.
	GetLifecycleStatus() Status

	// Typed state predicates — prefer these over comparing GetStatus() against constants.
	IsActive() bool
	IsPaused() bool
	IsHibernated() bool
	IsStopped() bool

	// Git / diff
	GetDiffStats() *git.DiffStats

	// Activity timestamps
	GetTimeSinceLastMeaningfulOutput() time.Duration
}

InstanceReader exposes a minimal read-only view of an Instance for server-layer code that only needs to observe session state. It is not yet used at every call site (some helpers still take *Instance directly for field access); adopt it incrementally as call sites are converted to use getter methods.

*Instance satisfies this interface automatically. Use it to supply lightweight test doubles without starting a real tmux session.

type InstanceSnapshot added in v1.35.0

type InstanceSnapshot struct {
	// Identity / config
	ID               string
	UUID             string
	Title            string
	Path             string
	WorkingDir       string
	Branch           string
	CreatedAt        time.Time
	UpdatedAt        time.Time
	Status           Status
	Program          string
	Height           int
	Width            int
	AutoYes          bool
	AutoApprove      bool
	IsExpanded       bool
	Prompt           string
	InitialPrompt    string
	Category         string
	Note             string
	SessionType      SessionType
	TmuxPrefix       string
	TmuxServerSocket string
	Tags             []string // defensive deep copy — see buildSnapshot

	// Autonomous mode (grouped — access as snap.Autonomous.AutonomousMode)
	Autonomous AutonomousModeState

	// GitHub PR / URL integration (grouped — access as snap.GitHub.GitHubPRURL)
	GitHub GitHubIntegration

	// Checkpoints
	Checkpoints      CheckpointList // defensive deep copy — see buildSnapshot
	ActiveCheckpoint string
	ForkedFromID     string

	// Misc config
	OneShot             bool
	Hidden              bool
	ProjectID           string
	HistoryFilePath     string
	MCPServerURL        string
	AppendSystemPrompt  string
	AllowedTools        string
	PermissionMode      string
	RateLimitAutoResume *bool // copy of pointee — see buildSnapshot
	PauseReason         string
	ExitReason          string
	WorkflowID          string
	EnvVars             map[string]string // defensive deep copy — see buildSnapshot
	CLIFlags            string
	ArchivedAt          *time.Time // copy of pointee — see buildSnapshot

	// Review queue / activity state (embedded value — copied by value)
	ReviewState

	// Instance type and management metadata
	InstanceType     InstanceType
	IsManaged        bool
	ExternalMetadata *ExternalInstanceMetadata // copy of pointee — see buildSnapshot
	Permissions      InstancePermissions       // RequiresConfirmation map deep-copied
	Artifacts        *artifacts.SessionArtifactsBlob
}

InstanceSnapshot is a point-in-time, read-safe copy of all mutable Instance fields. Published via Instance.snapshot (atomic.Pointer) inside stateMutex at the end of every mutator so lock-free readers always see consistent state.

Excluded: manager/dependency objects (gitManager, vncManager, cdpManager, processManager, controllerManager, tagManager, shellRepo, historyDetector) and callback registrations (lifecycleListeners, onRateLimitDetected, onStatusChange). Those are behavior, not data; callers needing them go through dedicated accessors or mailbox round-trips (Epic 3).

type InstanceStatusInfo

type InstanceStatusInfo struct {
	BasicStatus        Status                   // Creating, Active, Paused, Stopped, Hibernated
	ClaudeStatus       detection.DetectedStatus // If ClaudeController is active
	StatusContext      string                   // Context/details about current status (e.g., error message)
	PendingApprovals   int                      // Number of pending approvals
	QueuedCommands     int                      // Number of queued commands
	LastCommandStatus  string                   // Status of last command
	IsControllerActive bool                     // Whether ClaudeController is running
	IdleState          detection.IdleStateInfo  // NEW: Idle state information
	// SubagentCount is the count of background agents/shells/monitors from the
	// WaitingForAgent detector; 0 unless ClaudeStatus == detection.StatusWaitingForAgent.
	SubagentCount int
}

InstanceStatusInfo provides extended status information for an instance.

func (InstanceStatusInfo) GetColorCode

func (info InstanceStatusInfo) GetColorCode() string

GetColorCode returns a color code for the status (for lipgloss styling).

func (InstanceStatusInfo) GetStatusDescription

func (info InstanceStatusInfo) GetStatusDescription() string

GetStatusDescription returns a human-readable status description.

func (InstanceStatusInfo) GetStatusIcon

func (info InstanceStatusInfo) GetStatusIcon() string

GetStatusIcon returns an icon representing the instance status.

func (InstanceStatusInfo) HasPendingWork

func (info InstanceStatusInfo) HasPendingWork() bool

HasPendingWork returns true if the instance has pending commands or approvals.

func (InstanceStatusInfo) IsWaitingForUser

func (info InstanceStatusInfo) IsWaitingForUser() bool

IsWaitingForUser returns true if the instance is waiting for user input.

func (InstanceStatusInfo) NeedsAttention

func (info InstanceStatusInfo) NeedsAttention() bool

NeedsAttention returns true if the instance requires user attention.

type InstanceStatusManager

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

InstanceStatusManager manages status information for instances.

func NewInstanceStatusManager

func NewInstanceStatusManager() *InstanceStatusManager

NewInstanceStatusManager creates a new status manager.

func (*InstanceStatusManager) GetAllControllers

func (ism *InstanceStatusManager) GetAllControllers() map[string]*ClaudeController

GetAllControllers returns all registered controllers.

func (*InstanceStatusManager) GetController

func (ism *InstanceStatusManager) GetController(instanceTitle string) (*ClaudeController, bool)

GetController retrieves a controller for an instance.

func (*InstanceStatusManager) GetStatus

func (ism *InstanceStatusManager) GetStatus(instance *Instance) InstanceStatusInfo

GetStatus retrieves comprehensive status for an instance.

func (*InstanceStatusManager) RegisterController

func (ism *InstanceStatusManager) RegisterController(instanceTitle string, controller *ClaudeController)

RegisterController registers a controller for an instance.

func (*InstanceStatusManager) UnregisterController

func (ism *InstanceStatusManager) UnregisterController(instanceTitle string)

UnregisterController removes a controller for an instance.

type InstanceStore added in v1.1.0

type InstanceStore interface {
	LoadInstances() ([]*Instance, error)
	// ListInstanceData returns raw persisted InstanceData without constructing Instance
	// objects or spawning PTY processes. Use this for read-only existence/title checks
	// where calling LoadInstances() would create unnecessary side effects.
	ListInstanceData() ([]InstanceData, error)
	SaveInstances([]*Instance) error
	AddInstance(*Instance) error
	DeleteInstance(title string) error
	UpdateInstanceLastUserResponse(title string, t time.Time) error
	UpdateInstanceMetadata(currentTitle string, newTitle, category, note, workingDir *string) error
}

InstanceStore is the minimal interface the server layer needs for session persistence. Defining it here (alongside the concrete Storage) allows test fakes to be built without depending on the full Storage implementation.

type InstanceType

type InstanceType int

InstanceType represents the type of session instance

const (
	// InstanceTypeManaged represents a session fully managed by stapler-squad
	// with complete lifecycle control, git worktrees, and all features
	InstanceTypeManaged InstanceType = iota

	// InstanceTypeExternal represents a Claude instance discovered externally
	// (not created by stapler-squad) with limited interaction capabilities
	InstanceTypeExternal
)

func (InstanceType) String

func (it InstanceType) String() string

type ItemChangePublisher added in v1.41.0

type ItemChangePublisher interface {
	PublishItemChanged(item *BacklogItemData, change BacklogItemChange)
}

ItemChangePublisher publishes a backlog item mutation to interested subscribers (typically the event bus). Implemented outside this package (typically a thin adapter over the event bus, in server/services) since this package cannot import pkg/events directly — pkg/events imports session, so the reverse import would be a cycle. Mirrors the Notifier interface's cross-package adapter pattern (see Notifier, above in backlog_lifecycle.go).

Publish is always best-effort: implementations must never block or panic into the caller, and callers must nil-check before invoking since a publisher may not be wired (e.g. in tests, or a repository that hasn't had SetItemChangePublisher called on it).

type ItemSessionBacklogEntry added in v1.37.0

type ItemSessionBacklogEntry struct {
	SessionUUID string
	SessionRole string
	ItemID      string
	ItemTitle   string
	ItemStatus  string
}

ItemSessionBacklogEntry is a lightweight join record linking a tmux session UUID to its parent backlog item's metadata. Returned by GetAllItemSessionsWithBacklogInfo.

type ItemSessionData added in v1.35.0

type ItemSessionData struct {
	ItemID      string // BacklogItem UUID
	SessionUUID string
	SessionRole string
	AcSnapshot  AcCriteriaJSON
	// PipelineModeSnapshot/PipelineModeSnapshotHash freeze the resolved
	// PipelineMode slug and its content hash at the moment this session
	// first starts — see ItemSessionSummary.PipelineModeSnapshot(Hash).
	PipelineModeSnapshot     string
	PipelineModeSnapshotHash string
	TriageResult             string
	VerificationNotes        string  // Freeform verification evidence reported via request_review
	EstimatedCostUsd         float64 // Only set for headless sessions where cost is known at creation time
	// ClaimantHostID is the claiming/attaching process's own stable host identifier
	// (Config.GetOrCreateClaimantHostID), never anything derived from the session being
	// claimed/attached. See ItemSession.claimant_host_id's schema comment for the full
	// disambiguation against STAPLER_SQUAD_INSTANCE and session/contexts.go's CloudContext.InstanceID.
	ClaimantHostID string
}

ItemSessionData is the input data for creating a new ItemSession.

type ItemSessionSummary added in v1.37.0

type ItemSessionSummary struct {
	ID                       string
	BacklogItemID            string
	SessionUUID              string
	Role                     string
	AcSnapshot               AcCriteriaJSON
	PipelineModeSnapshot     string
	PipelineModeSnapshotHash string
	// BaseCommitSha is the worktree's pre-work HEAD, captured once at spawn —
	// the base of the review gate's base..HEAD diff, and by construction always
	// already an ancestor of main. Never use it as evidence that this session's
	// work shipped; that is LastCommitSha's job. See the ItemSession ent
	// schema's field comments for the full BUG-047 rationale.
	BaseCommitSha string
	// LastCommitSha is the session's current tip commit, refreshed each
	// reconciliation tick while the session is active (see
	// BacklogLifecycleListener.refreshWorkSessionGitActivity).
	LastCommitSha         string
	LastCommitMessage     string
	CommitCountSinceSpawn int
	StartedAt             *time.Time
	EndedAt               *time.Time
	EndReason             string // set alongside EndedAt for a headless call; see ItemSession.end_reason schema comment
	FailureCapturePath    string // absolute path to a durable raw-output capture; see ItemSession.failure_capture_path schema comment
	LastCommitAt          *time.Time
	LastFileTouchAt       *time.Time
	LastProgressAt        *time.Time
	CreatedAt             time.Time
	EstimatedCostUsd      float64
	TriageResult          string // raw JSON stored in triage_result column
	TriageResultSummary   string // summary field parsed from TriageResult
	VerificationNotes     string // freeform verification evidence reported via request_review
	OverallOutcome        string // from linked review_verdict (empty if none)
	ReviewVerdict         *ReviewVerdictSummary
	// ClaimantHostID identifies the physical stapler-squad process/host that claimed or
	// attached this session. See ItemSession.claimant_host_id's schema comment for the
	// full disambiguation against STAPLER_SQUAD_INSTANCE and CloudContext.InstanceID.
	ClaimantHostID string
}

ItemSessionSummary is the domain DTO replacing *ent.ItemSession in Storage returns. Note: item_sessions table has NO status, triage_result_summary, or overall_outcome columns.

  • EndedAt == nil means the session is still running
  • TriageResultSummary: parsed from the triage_result JSON column
  • OverallOutcome: from the review_verdicts table (populated via ReviewVerdict edge)
  • ReviewVerdict: eagerly loaded when the query uses WithReviewVerdict()

func MostRecentCompletedWorkSession added in v1.42.0

func MostRecentCompletedWorkSession(sessions []ItemSessionSummary) *ItemSessionSummary

MostRecentCompletedWorkSession returns the most recent completed (EndedAt != nil) work-role ItemSession from sessions, or nil if none exists. sessions must be ordered oldest-first, as Storage.ListItemSessions returns. Exported: called from server/services at review-verdict-save time to resolve which work session's diff a given verdict is reviewing (feeds the DiffHash computation IsFlakyVerdictFlipFlop consumes), and by the stuck-item reconciler to build IsTestOnlyReworkCycle's per-attempt file lists. Only considers completed sessions — reading a still-in-progress session's commit range risks racing an in-flight write (see validation.md).

func RecordDegradedReviewVerdict added in v1.38.0

func RecordDegradedReviewVerdict(storage *Storage, itemID string, acSnapshot AcCriteriaJSON, uuidPrefix, summary string) (ItemSessionSummary, error)

RecordDegradedReviewVerdict persists a synthetic UNVERIFIABLE verdict for a review that could not actually be attempted or completed (capability self-check failure, codebase-read timeout/cancellation). Thin wrapper around recordTerminalReviewVerdict that fixes the outcome to UNVERIFIABLE and the session UUID convention (uuidPrefix + a fresh random UUID) shared by every "degraded, not a real failure" call site — see recordTerminalReviewVerdict's doc comment for the full rationale.

type ItemSourceData added in v1.35.0

type ItemSourceData struct {
	ID                    string
	PluginID              string
	DisplayName           string
	Config                string // JSON, may contain encrypted token
	Enabled               bool
	ForwardSyncEnabled    bool
	BackwardSyncEnabled   bool
	ForwardSyncCloseLabel string
	TokenConfigured       bool
	LastSyncedAt          *time.Time
	CreatedAt             time.Time
	UpdatedAt             time.Time
}

ItemSourceData is the domain model for an external item source.

type ItemSourcePlugin added in v1.35.0

type ItemSourcePlugin interface {
	// PluginID returns the unique identifier for this plugin (e.g., "github_issues").
	PluginID() string
	// Fetch retrieves new and updated items since the cursor. Returns items and the new cursor.
	Fetch(ctx context.Context, config PluginConfig, cursor string) ([]ExternalItem, string, error)
	// MapToBacklogItem converts an external item to a BacklogItemData.
	MapToBacklogItem(item ExternalItem, sourceID string) BacklogItemData
}

ItemSourcePlugin is the interface all external source integrations must implement.

type ItemSourceUpdate added in v1.35.0

type ItemSourceUpdate struct {
	DisplayName           *string
	Enabled               *bool
	ForwardSyncEnabled    *bool
	BackwardSyncEnabled   *bool
	ForwardSyncCloseLabel *string
	Config                *string
}

ItemSourceUpdate carries the mutable fields for UpdateItemSource.

type KillOutcome added in v1.42.0

type KillOutcome struct {
	Status KillOutcomeStatus
	Err    error
}

KillOutcome is the domain-level result of KillExternalOriginalProcess.

func KillExternalOriginalProcess added in v1.42.0

func KillExternalOriginalProcess(checker AliveChecker, pid int32, createTimeMs int64, tmuxSession string) KillOutcome

KillExternalOriginalProcess re-verifies pid/createTimeMs are still the same process (guarding against PID reuse in the window between commit and this call) and, if so, resumes it (it was SIGSTOP'd at commit time -- a stopped process ignores signals sent by "tmux kill-session" until it's running again) and kills its tmux session via a throwaway InstanceTypeExternal Instance's KillExternalSession. On success, the caller is responsible for removing the SuspendedProcessRecord -- this function only performs the resume+kill, it does not touch persisted state.

type KillOutcomeStatus added in v1.42.0

type KillOutcomeStatus int

KillOutcomeStatus enumerates the possible results of ConfirmKillExternalSession. This is a Go domain type, not a proto message -- the RPC handler maps it onto sessionv1.KillStatus.

const (
	// KillOutcomeUnspecified is the zero value and is never returned by
	// KillExternalOriginalProcess.
	KillOutcomeUnspecified KillOutcomeStatus = iota
	// KillOutcomeKilled means the tmux session was killed successfully.
	KillOutcomeKilled
	// KillOutcomeAlreadyGone means IsAlive re-verification failed (PID reuse
	// or the process already exited) -- no signal was sent.
	KillOutcomeAlreadyGone
	// KillOutcomeFailed means the kill primitive itself (tmux kill-session)
	// failed. The original process is left SIGSTOP'd; it is never
	// auto-resumed on this path.
	KillOutcomeFailed
)

type LifecycleEvent added in v1.15.0

type LifecycleEvent int

LifecycleEvent is a notification type emitted by an Instance when key state transitions occur (e.g., the session starts, or the program exits unexpectedly).

const (
	// EventStarted fires at the end of start() when the instance has successfully
	// transitioned to Running and the controller is up.
	EventStarted LifecycleEvent = iota
	// EventExited fires when the underlying program exits unexpectedly (not via an
	// operator-initiated Kill/Stop). Callers may use this to drive auto-restart logic.
	EventExited
	// EventStopped fires when Destroy() tears down the instance via an explicit
	// operator-initiated Kill/Stop (e.g. the stop_session MCP tool, DeleteSession
	// RPC, or backlog stale-work remediation). Kept distinct from EventExited so a
	// future auto-restart listener can still ignore deliberate stops, while
	// listeners that only care "is this session now gone" (e.g.
	// BacklogLifecycleListener's ItemSession.EndedAt bookkeeping) can subscribe to
	// both.
	EventStopped
)

type LifecycleListener added in v1.15.0

type LifecycleListener interface {
	OnLifecycleEvent(event LifecycleEvent, reason string)
}

LifecycleListener is implemented by any component that wants to receive Instance lifecycle notifications. Implementations must be non-blocking; use a goroutine or channel if the handler needs to do significant work.

type LiveInstance added in v1.35.0

type LiveInstance struct {
	*Instance
	// contains filtered or unexported fields
}

LiveInstance is the actor-owning handle for a session. It wraps *Instance with lifecycle fields for the actor goroutine (IAC Epic 3). Supported construction paths from outside this package:

  • Registry.Acquire(sessionID) — load-or-construct for an existing persisted session
  • Registry.Register(inst) — for brand-new sessions in CreateSession (R2.18a)
  • NewLiveInstance(inst) — direct wrap when the caller already holds *Instance

The actor goroutine (runActor in actor.go) is started by NewLiveInstance via finishLiveInstanceConstruction and exits when Stop()/stopActor() cancels the ctx.

func NewLiveInstance added in v1.35.0

func NewLiveInstance(inst *Instance) *LiveInstance

NewLiveInstance wraps an already-constructed *Instance in a LiveInstance and starts its actor goroutine. Use Registry.Acquire or Registry.Register where possible; call this directly only when the caller already holds a freshly- constructed *Instance (e.g. CreateSession, which builds its own via NewInstance and then passes it to Registry.Register).

func (*LiveInstance) Stop added in v1.35.0

func (l *LiveInstance) Stop()

Stop signals this instance's actor to exit and waits for it to drain. Idempotent: safe to call multiple times; the second and subsequent calls return immediately once the first call's <-done wait completes.

type LiveInstancesProvider added in v1.35.0

type LiveInstancesProvider interface {
	GetInstances() []*Instance
}

LiveInstancesProvider is satisfied by ReviewQueuePoller. It returns the live in-memory instances without constructing new Instance objects or spawning PTY processes. HibernationSweeper uses this as a fast path to avoid LoadInstances().

type LoadOptions

type LoadOptions struct {
	// LoadWorktree controls whether git worktree data is loaded
	LoadWorktree bool

	// LoadDiffStats controls whether diff statistics (added/removed counts) are loaded
	LoadDiffStats bool

	// LoadDiffContent controls whether full diff content is loaded
	// Note: This implies LoadDiffStats=true, as we need counts to interpret content
	LoadDiffContent bool

	// LoadTags controls whether session tags are loaded
	LoadTags bool

	// LoadClaudeSession controls whether Claude Code session data is loaded
	LoadClaudeSession bool
}

LoadOptions controls what child data is loaded for sessions. This allows selective loading to optimize performance by avoiding unnecessary data retrieval.

func (LoadOptions) WithDiffContent

func (o LoadOptions) WithDiffContent() LoadOptions

WithDiffContent returns a copy of options with diff content loading enabled.

func (LoadOptions) WithTags

func (o LoadOptions) WithTags() LoadOptions

WithTags returns a copy of options with tag loading enabled.

func (LoadOptions) WithoutDiffContent

func (o LoadOptions) WithoutDiffContent() LoadOptions

WithoutDiffContent returns a copy of options with diff content loading disabled.

func (LoadOptions) WithoutTags

func (o LoadOptions) WithoutTags() LoadOptions

WithoutTags returns a copy of options with tag loading disabled.

type Locked added in v1.35.0

type Locked[T any] struct {
	// contains filtered or unexported fields
}

Locked bundles a value T with a RWMutex, enforcing lock discipline by only exposing the value through Read/Write callbacks.

This is the Go equivalent of Rust's RwLock<T>: the data and its lock are inseparable, making it structurally impossible to access the value without correct lock discipline. Instead of a mutex sitting next to a field (which the compiler cannot enforce is held on access), callers receive or mutate the value only through Read or Write.

var listeners Locked[[]StatusChangeListener]

// Add a listener — write lock taken automatically
listeners.Write(func(ls *[]StatusChangeListener) {
    *ls = append(*ls, fn)
})

// Read all listeners — read lock taken automatically
var snapshot []StatusChangeListener
listeners.Read(func(ls []StatusChangeListener) {
    snapshot = append(snapshot, ls...)
})

func (*Locked[T]) Read added in v1.35.0

func (l *Locked[T]) Read(fn func(T))

Read calls fn with a read-only copy of the value, holding the read lock for the duration. Multiple goroutines may call Read concurrently. Errors from fn should be captured via closure variables.

func (*Locked[T]) Write added in v1.35.0

func (l *Locked[T]) Write(fn func(*T))

Write calls fn with a pointer to the value, holding the exclusive write lock for the duration. fn may mutate the value freely. Errors from fn should be captured via closure variables.

type MemoryCacheReader added in v1.35.0

type MemoryCacheReader interface {
	GetCachedRSSMB(sessionUUID string) int64
	SystemMemoryPct() (float64, error)
}

MemoryCacheReader is implemented by HibernationSweeper so SessionService can read cached RSS values without importing the sweeper concretely.

type MigrationOptions

type MigrationOptions struct {
	// JSONPath is the path to the existing JSON state file
	JSONPath string

	// SQLitePath is the path where the SQLite database will be created
	SQLitePath string

	// BackupPath is the path where the JSON backup will be saved
	BackupPath string

	// ForceOverwrite allows overwriting existing SQLite database
	ForceOverwrite bool

	// DryRun performs validation without actually migrating
	DryRun bool
}

MigrationOptions configures the migration from JSON to SQLite

type MigrationResult

type MigrationResult struct {
	TotalSessions      int
	MigratedSessions   int
	SkippedSessions    int
	Errors             []string
	Duration           time.Duration
	BackupCreated      bool
	BackupPath         string
	SQLiteDatabasePath string
}

MigrationResult contains the results of the migration process

func MigrateJSONToEnt

func MigrateJSONToEnt(opts MigrationOptions) (*MigrationResult, error)

MigrateJSONToEnt migrates session data from JSON to Ent ORM storage.

type NativeProcessManager added in v1.35.0

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

NativeProcessManager implements ProcessManager using a raw PTY and process supervision. It launches the configured program directly under a PTY master fd (via creack/pty) and restarts it with exponential backoff when it exits unexpectedly.

Phase 2 implementation: Start(), Close(), IsAlive(), GetPTY(), GetPanePID(), GetSessionIdentifier(), SetWindowSize(), GetPaneDimensions(), SendKeys(), TapEnter(), SetOnExitCallback(), SubscribeToControlModeUpdates(), and GetCurrentWorkingDirectory() are fully functional. Content capture (CapturePaneContent variants) and precise CWD via lsof/proc are deferred to Phase 3.

func NewNativeProcessManager added in v1.35.0

func NewNativeProcessManager(opts ProcessManagerOptions) *NativeProcessManager

NewNativeProcessManager creates a NativeProcessManager with the given options. Call Start() to launch the process.

func (*NativeProcessManager) Attach added in v1.35.0

func (n *NativeProcessManager) Attach() (chan struct{}, error)

Attach is not supported for the native backend; returns an error. Interactive TUI attach requires a proper terminal multiplexer.

func (*NativeProcessManager) CapturePaneContent added in v1.35.0

func (n *NativeProcessManager) CapturePaneContent() (string, error)

CapturePaneContent returns an empty string until scrollback capture is implemented.

func (*NativeProcessManager) CapturePaneContentRaw added in v1.35.0

func (n *NativeProcessManager) CapturePaneContentRaw() (string, error)

CapturePaneContentRaw returns an empty string until scrollback capture is implemented.

func (*NativeProcessManager) CapturePaneContentWithOptions added in v1.35.0

func (n *NativeProcessManager) CapturePaneContentWithOptions(_, _ string) (string, error)

CapturePaneContentWithOptions returns an empty string until scrollback capture is implemented.

func (*NativeProcessManager) CaptureViewport added in v1.35.0

func (n *NativeProcessManager) CaptureViewport(_ int) (string, error)

CaptureViewport returns an empty string until scrollback capture is implemented.

func (*NativeProcessManager) Close added in v1.35.0

func (n *NativeProcessManager) Close() error

Close terminates the supervised process and stops the restart loop. Implements NM-3 (SIGTERM before context cancel) and NM-5 (goroutines exit).

func (*NativeProcessManager) DetachSafely added in v1.35.0

func (n *NativeProcessManager) DetachSafely() error

DetachSafely is a no-op for the native backend.

func (*NativeProcessManager) FilterBanners added in v1.35.0

func (n *NativeProcessManager) FilterBanners(content string) (string, int)

FilterBanners returns content unchanged; banner detection is tmux-specific.

func (*NativeProcessManager) GetCurrentWorkingDirectory added in v1.35.0

func (n *NativeProcessManager) GetCurrentWorkingDirectory() (string, error)

GetCurrentWorkingDirectory returns the directory passed to the most recent Start() call. Phase 3 follow-on: replace with /proc/<pid>/cwd on Linux or lsof on macOS for the true current working directory of the running process.

func (*NativeProcessManager) GetCursorPosition added in v1.35.0

func (n *NativeProcessManager) GetCursorPosition() (x, y int, err error)

GetCursorPosition returns (0, 0) for the native backend. There are zero callers in the server/ package that require real cursor position from the native backend (confirmed in plan.md).

func (*NativeProcessManager) GetPTY added in v1.35.0

func (n *NativeProcessManager) GetPTY() (*os.File, error)

GetPTY returns the PTY master file descriptor.

func (*NativeProcessManager) GetPaneDimensions added in v1.35.0

func (n *NativeProcessManager) GetPaneDimensions() (width, height int, err error)

GetPaneDimensions returns the last window size set via SetWindowSize. Tracks the value in memory to avoid a TIOCGWINSZ syscall on the hot path (GetPaneDimensions is called 5× per resize event in connectrpc_websocket.go).

func (*NativeProcessManager) GetPanePID added in v1.35.0

func (n *NativeProcessManager) GetPanePID() (int32, error)

GetPanePID returns the PID of the supervised process.

func (*NativeProcessManager) GetSessionIdentifier added in v1.35.0

func (n *NativeProcessManager) GetSessionIdentifier() string

GetSessionIdentifier returns the stable session name set at construction.

func (*NativeProcessManager) HasMeaningfulContent added in v1.35.0

func (n *NativeProcessManager) HasMeaningfulContent(_ string) bool

HasMeaningfulContent always returns false until content analysis is implemented.

func (*NativeProcessManager) HasSession added in v1.35.0

func (n *NativeProcessManager) HasSession() bool

HasSession reports whether a process has been started at least once. Alias for IsAlive() on the native backend.

func (*NativeProcessManager) HasUpdated added in v1.35.0

func (n *NativeProcessManager) HasUpdated() (updated bool, hasPrompt bool, content string)

HasUpdated always returns (false, false, "") until content diffing is implemented.

func (*NativeProcessManager) IsAlive added in v1.35.0

func (n *NativeProcessManager) IsAlive() bool

IsAlive reports whether the supervised process is currently running.

func (*NativeProcessManager) RefreshClient added in v1.35.0

func (n *NativeProcessManager) RefreshClient() error

RefreshClient is a no-op for the native backend (no tmux client to refresh).

func (*NativeProcessManager) ResetExitOnce added in v1.35.0

func (n *NativeProcessManager) ResetExitOnce()

ResetExitOnce is a no-op for the native backend; the restart loop does not use a sync.Once guard.

func (*NativeProcessManager) RestoreWithWorkDir added in v1.35.0

func (n *NativeProcessManager) RestoreWithWorkDir(_ string) error

RestoreWithWorkDir is a no-op for the native backend; the process is already running after Start() and does not need re-attachment.

func (*NativeProcessManager) SendInputViaControlMode added in v1.35.0

func (n *NativeProcessManager) SendInputViaControlMode(_ context.Context, data []byte) error

SendInputViaControlMode writes raw bytes directly to the PTY master. The native backend has no concept of tmux control mode; bytes are written directly.

func (*NativeProcessManager) SendKeys added in v1.35.0

func (n *NativeProcessManager) SendKeys(keys string) (int, error)

SendKeys writes the given string to the PTY master.

func (*NativeProcessManager) SendPromptWithEnter added in v1.35.0

func (n *NativeProcessManager) SendPromptWithEnter(prompt string) error

SendPromptWithEnter sends text followed by Enter.

func (*NativeProcessManager) SetDetachedSize added in v1.35.0

func (n *NativeProcessManager) SetDetachedSize(width, height int, _ string) error

SetDetachedSize updates the stored window size without requiring an active PTY. The instanceTitle parameter is ignored; it exists only for interface compatibility.

func (*NativeProcessManager) SetOnExitCallback added in v1.35.0

func (n *NativeProcessManager) SetOnExitCallback(fn func(string))

SetOnExitCallback registers a callback invoked when the supervised process exits unexpectedly (before the restart loop relaunches it).

func (*NativeProcessManager) SetWindowSize added in v1.35.0

func (n *NativeProcessManager) SetWindowSize(cols, rows int) error

SetWindowSize resizes the PTY to the given columns and rows.

func (*NativeProcessManager) Start added in v1.35.0

func (n *NativeProcessManager) Start(dir string) error

Start launches the configured program under a PTY in the given directory. If the process is already running, Start is a no-op. Start resets the stop signal so it is safe to call after Close().

func (*NativeProcessManager) StartControlMode added in v1.35.0

func (n *NativeProcessManager) StartControlMode() error

StartControlMode is a no-op for the native backend; raw PTY reads replace control mode.

func (*NativeProcessManager) StopControlMode added in v1.35.0

func (n *NativeProcessManager) StopControlMode() error

StopControlMode is a no-op for the native backend.

func (*NativeProcessManager) SubscribeToControlModeUpdates added in v1.35.0

func (n *NativeProcessManager) SubscribeToControlModeUpdates() (string, chan []byte)

SubscribeToControlModeUpdates adds a subscriber that receives raw PTY output bytes. Returns the subscription ID and a channel that receives byte slices.

func (*NativeProcessManager) TapEnter added in v1.35.0

func (n *NativeProcessManager) TapEnter() error

TapEnter sends a carriage return + newline sequence to the PTY.

func (*NativeProcessManager) UnsubscribeFromControlModeUpdates added in v1.35.0

func (n *NativeProcessManager) UnsubscribeFromControlModeUpdates(id string)

UnsubscribeFromControlModeUpdates removes a subscriber by ID and closes its channel.

type NotificationDecisionLister added in v1.41.0

type NotificationDecisionLister interface {
	ListDecisionRecords(ctx context.Context, sessionID string) ([]DecisionRecord, error)
}

NotificationDecisionLister is a small consumer-defined interface, scoped to exactly what BuildDecisionsSnapshot needs, satisfied by a thin adapter over *notifications.NotificationHistoryStore.List (wired in Phase 2). Defined here, next to its consumer, per .claude/rules/interface-pollution-checklist.md's "define the interface where it's consumed".

type Notifier added in v1.37.0

type Notifier interface {
	Notify(itemID, title, message string, notificationType, priority int32)
}

Notifier publishes an operator-facing notification. Implemented outside this package (typically a thin adapter over the event bus) since this package cannot import pkg/events directly — pkg/events imports session, so the reverse import would be a cycle. notificationType and priority are int32 values matching sessionv1.NotificationType / sessionv1.NotificationPriority; this package stays free of the proto dependency and just passes the raw values through.

type OneShotShipRunner added in v1.39.0

type OneShotShipRunner interface {
	RunOneShotForSession(ctx context.Context, sessionID, prompt string, timeoutSeconds int32) (string, error)
}

OneShotShipRunner runs a one-shot LLM prompt against a session's worktree, returning the PR URL the prompt produced (or "" if none was found in its output). Defined here — the consumer — per this repo's anti-interface- pollution convention (.claude/rules/interface-pollution-checklist.md); *services.SessionService satisfies it via RunOneShotForSession, wired in production via SetOneShotShipRunner from server/dependencies.go. Mirrors services.PRRunner (server/services/backlog_service_ship.go), which the same method also satisfies for the manual "Ship PR" self-service action — intentionally not shared/exported from that package, since importing it here would pull server/services (which imports this package) into an import cycle.

Used by shipViaAgentOrFallback to close the gap flagged in PR #189's "deliberately out of scope" section: when the work session that earned a PASS verdict has already exited, the only PR-creation mechanism available was pushAndCreatePR's mechanical `git push` + `gh pr create` — no CI reaction, no merge-conflict resolution. RunOneShotForSession lets us run the same agent-driven ship flow a still-live session would have run itself (see /backlog/ship's ship.md, which drives /github:pr-ship) as a headless one-shot against the ended session's worktree — it only needs the session's Instance/worktree to still be resolvable, not a live tmux process (see RunOneShot's use of findInstance + GetEffectiveRootDir).

type OpenStuckStateData added in v1.38.0

type OpenStuckStateData struct {
	ID                  string
	ItemID              string
	Reason              domain.StuckReason
	FirstDetectedAt     time.Time
	LastCheckedAt       time.Time
	NotifiedAt          *time.Time
	Context             string
	ItemTitle           string
	ItemStatus          BacklogStatus
	PrNumber            int
	PrURL               string
	RemediationAttempts int32
	NextRemediationAt   *time.Time
	GraceBootTime       *time.Time
	PlanArtifactsPath   string
}

OpenStuckStateData is a projected, already-filtered (open + un-snoozed) BacklogStuckState row joined with its parent item's rendering-relevant fields. Returned only by FindOpenStuckStates, which applies the "open" (resolved_at IS NULL) and "not currently snoozed" filters at the query boundary — callers never need to re-check ResolvedAt/SnoozedUntil nullability themselves (parse-don't-validate at the repository boundary).

type OutputConsumer

type OutputConsumer func(data []byte)

OutputConsumer is a callback that receives terminal output from external sessions.

type PRFixSpawner added in v1.37.0

type PRFixSpawner interface {
	AutoReopenForPRFix(ctx context.Context, itemID string, fixContext string) error
}

PRFixSpawner can reopen a pr_pending item for rework when CI checks fail or reviewers request changes. The fixContext string contains a summary of the failures/comments to pass as context to the new work session.

type PRReassignmentGuard added in v1.42.0

type PRReassignmentGuard struct {
	// OverrideReason must be non-empty — the caller's own already-validated
	// reason for the reassignment.
	OverrideReason string
	// CurrentPRMerged must reflect the caller's verified state of the
	// CURRENTLY tracked PR. true hard-blocks the reassignment
	// unconditionally — a merged PR's association must never be silently
	// swapped, even with OverrideReason set.
	CurrentPRMerged bool
	// NewPRAuthorVerified must be true only when the caller has verified the
	// new PR's GitHub author matches the caller's own verified identity.
	NewPRAuthorVerified bool
}

PRReassignmentGuard carries the caller-verified preconditions required before SetBacklogItemPRAndTransition (session/storage.go) will accept a reassignment — a call where the observed item is already pr_pending with a DIFFERENT PR number than the one being recorded now. This function itself never calls GitHub; a caller supplies this guard to attest it already did that verification. A caller with no way to produce a valid guard (e.g. the manual-override RPC in server/services/backlog_service_lifecycle.go, which by design never calls GitHub) passes nil and gets a clear rejection instead of silently reassigning an unverified PR.

type PRStatusPoller added in v1.12.0

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

PRStatusPoller polls GitHub PR status for all sessions at a shared interval. Uses a single workspace-level ticker (not per-session goroutines) and an ETag cache so unchanged PRs return HTTP 304 and cost zero rate-limit quota.

func NewPRStatusPoller added in v1.12.0

func NewPRStatusPoller(storage *Storage) *PRStatusPoller

NewPRStatusPoller creates a new poller with default configuration.

func NewPRStatusPollerWithConfig added in v1.12.0

func NewPRStatusPollerWithConfig(storage *Storage, config PRStatusPollerConfig) *PRStatusPoller

NewPRStatusPollerWithConfig creates a poller with custom configuration.

func (*PRStatusPoller) AddInstance added in v1.12.0

func (p *PRStatusPoller) AddInstance(inst *Instance)

AddInstance adds a single instance to monitor.

func (*PRStatusPoller) ETagCache added in v1.42.0

func (p *PRStatusPoller) ETagCache() *github.ETagCache

ETagCache returns the poller's shared *github.ETagCache, so other pollers (e.g. WorktreePRPoller) reuse the same conditional-request cache instead of each maintaining their own — per ADR-022, a separate cache would double GitHub API call volume for repos both pollers hit.

func (*PRStatusPoller) GetInstances added in v1.35.0

func (p *PRStatusPoller) GetInstances() []*Instance

GetInstances returns a defensive copy of the currently monitored instances. Callers must not modify the returned slice elements.

func (*PRStatusPoller) PollInterval added in v1.41.0

func (p *PRStatusPoller) PollInterval() time.Duration

PollInterval returns the poller's configured poll interval, so other components (e.g. ApprovalHandler's CI-status staleness guard) can bound freshness against the same live-configured value rather than a disconnected duplicate literal.

func (*PRStatusPoller) RemoveInstance added in v1.12.0

func (p *PRStatusPoller) RemoveInstance(title string)

RemoveInstance removes an instance from monitoring.

func (*PRStatusPoller) SetInstances added in v1.12.0

func (p *PRStatusPoller) SetInstances(instances []*Instance)

SetInstances replaces the full list of monitored instances.

func (*PRStatusPoller) SetOnUpdated added in v1.12.0

func (p *PRStatusPoller) SetOnUpdated(fn func(*Instance))

SetOnUpdated registers a callback called when a session's PR priority changes. The callback is invoked from a goroutine; it must be concurrency-safe.

func (*PRStatusPoller) Start added in v1.12.0

func (p *PRStatusPoller) Start(ctx context.Context)

Start begins the polling loop. Safe to call multiple times; subsequent calls are no-ops.

func (*PRStatusPoller) Stop added in v1.12.0

func (p *PRStatusPoller) Stop()

Stop gracefully shuts down the poller and waits for in-flight requests.

type PRStatusPollerConfig added in v1.12.0

type PRStatusPollerConfig struct {
	// PollInterval controls how often all sessions are checked.
	PollInterval time.Duration
	// ConcurrentFetches limits simultaneous gh CLI calls (respects secondary rate limits).
	ConcurrentFetches int
	// CallTimeout is the maximum time for a single gh API call.
	CallTimeout time.Duration
	// AuthCacheDuration controls how long a successful auth check is cached.
	AuthCacheDuration time.Duration
	// NoPRBackoff is how long to wait before re-checking a session after ErrNoPR.
	// Zero disables the backoff (always re-check).
	NoPRBackoff time.Duration
}

PRStatusPollerConfig contains configuration for the PR status poller.

func DefaultPRStatusPollerConfig added in v1.12.0

func DefaultPRStatusPollerConfig() PRStatusPollerConfig

DefaultPRStatusPollerConfig returns sensible defaults.

type PTYAccess

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

PTYAccess provides thread-safe access to a tmux session's PTY for reading and writing. It wraps the PTY file descriptor with synchronization primitives to enable concurrent access from multiple goroutines (e.g., command execution, response streaming, status monitoring).

func NewPTYAccess

func NewPTYAccess(sessionName string, pty *os.File, buffer *CircularBuffer) *PTYAccess

NewPTYAccess creates a new PTYAccess wrapper for a PTY file descriptor. The buffer parameter specifies the circular buffer for storing PTY output history.

func (*PTYAccess) Close

func (p *PTYAccess) Close() error

Close marks the PTY access as closed and prevents further operations. It does NOT close the underlying PTY file descriptor - that's handled by the tmux session.

func (*PTYAccess) GetBuffer

func (p *PTYAccess) GetBuffer() []byte

GetBuffer returns the most recent output from the circular buffer. This provides access to historical PTY output without blocking. Returns a copy of the buffer contents to prevent concurrent modification issues.

func (*PTYAccess) GetFile added in v1.35.0

func (p *PTYAccess) GetFile() (*os.File, bool)

GetFile returns the underlying PTY *os.File and whether the PTY has been closed. Returns (f, false) when open, (nil, false) when not yet initialized, (nil, true) when closed. The returned file must not be used after a subsequent UpdatePTY or Close call.

func (*PTYAccess) GetRecentHash added in v1.37.0

func (p *PTYAccess) GetRecentHash(n int) (uint64, bool)

GetRecentHash returns the murmur3-64 hash of the last n bytes without copying. Returns (0, false) when no data is available.

func (*PTYAccess) GetRecentOutput

func (p *PTYAccess) GetRecentOutput(n int) []byte

GetRecentOutput returns the last n bytes from the circular buffer. This is useful for status detection and response streaming.

func (*PTYAccess) GetRecentOutputInto added in v1.37.0

func (p *PTYAccess) GetRecentOutputInto(dst []byte, n int) int

GetRecentOutputInto copies the last n bytes into dst and returns the number of bytes written. dst must have length >= n. Prefer over GetRecentOutput when the caller can provide a pooled buffer.

func (*PTYAccess) GetSessionName

func (p *PTYAccess) GetSessionName() string

GetSessionName returns the name of the session this PTY access is for.

func (*PTYAccess) IsClosed

func (p *PTYAccess) IsClosed() bool

IsClosed returns whether the PTY access has been closed.

func (*PTYAccess) Read

func (p *PTYAccess) Read(buf []byte) (int, error)

Read reads data from the PTY in a thread-safe manner. This is a blocking call that will wait for data to be available. Returns the number of bytes read and any error encountered.

func (*PTYAccess) UpdatePTY

func (p *PTYAccess) UpdatePTY(pty *os.File) error

UpdatePTY updates the underlying PTY file descriptor. This is used when the PTY needs to be refreshed (e.g., after detach/reattach).

func (*PTYAccess) Write

func (p *PTYAccess) Write(data []byte) (int, error)

Write writes data to the PTY in a thread-safe manner. Returns the number of bytes written and any error encountered.

type PTYCategory

type PTYCategory int

PTYCategory represents grouping of PTYs

const (
	PTYCategorySquad    PTYCategory = iota // Squad-managed sessions
	PTYCategoryOrphaned                    // Unmanaged Claude instances
	PTYCategoryOther                       // Other tools (aider, etc.)
)

func (PTYCategory) String

func (c PTYCategory) String() string

type PTYConnection

type PTYConnection struct {
	Path         string            // /dev/pts/12
	PID          int               // Process ID
	Command      string            // "claude" or "aider"
	SessionName  string            // Associated squad session (if any)
	Status       PTYStatus         // Current status
	LastActivity time.Time         // Last activity timestamp
	Controller   *ClaudeController // Connected controller (if any)

	// Ownership and management metadata
	IsManaged       bool   // True if this is a squad-managed session
	TmuxSocket      string // Which tmux server socket (empty = default)
	TmuxSessionName string // Full tmux session name
	CanAttach       bool   // Whether attach operations are allowed
	CanDestroy      bool   // Whether destroy operations are allowed
	Owner           string // "squad" for managed, "external" for discovered
}

PTYConnection represents a discovered PTY

func (*PTYConnection) GetDisplayName

func (conn *PTYConnection) GetDisplayName() string

GetDisplayName returns a human-readable name for the PTY

func (*PTYConnection) GetPTYBasename

func (conn *PTYConnection) GetPTYBasename() string

GetPTYBasename returns just the PTY number (e.g., "12" from "/dev/pts/12")

func (*PTYConnection) GetStatusColor

func (conn *PTYConnection) GetStatusColor() string

GetStatusColor returns a color code for PTY status

func (*PTYConnection) GetStatusIcon

func (conn *PTYConnection) GetStatusIcon() string

GetStatusIcon returns a visual indicator for PTY status

type PTYDiscovery

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

PTYDiscovery manages PTY discovery and monitoring

func NewPTYDiscovery

func NewPTYDiscovery(opts ...PTYDiscoveryOption) *PTYDiscovery

NewPTYDiscovery creates a new PTY discovery service with default configuration. Optional PTYDiscoveryOption values are applied after initialization.

func NewPTYDiscoveryWithConfig

func NewPTYDiscoveryWithConfig(config PTYDiscoveryConfig, opts ...PTYDiscoveryOption) *PTYDiscovery

NewPTYDiscoveryWithConfig creates a new PTY discovery service with custom configuration. Optional PTYDiscoveryOption values are applied after initialization.

func (*PTYDiscovery) GetConnection

func (pd *PTYDiscovery) GetConnection(path string) *PTYConnection

GetConnection returns a specific PTY connection by path

func (*PTYDiscovery) GetConnections

func (pd *PTYDiscovery) GetConnections() []*PTYConnection

GetConnections returns all discovered PTY connections

func (*PTYDiscovery) GetConnectionsByCategory

func (pd *PTYDiscovery) GetConnectionsByCategory() map[PTYCategory][]*PTYConnection

GetConnectionsByCategory returns PTYs grouped by category

func (*PTYDiscovery) Refresh

func (pd *PTYDiscovery) Refresh() error

Refresh performs a full PTY discovery scan

func (*PTYDiscovery) SetSessions

func (pd *PTYDiscovery) SetSessions(sessions []*Instance)

SetSessions updates the session map for correlation

func (*PTYDiscovery) Start

func (pd *PTYDiscovery) Start()

Start begins PTY discovery monitoring

func (*PTYDiscovery) Stop

func (pd *PTYDiscovery) Stop()

Stop halts PTY discovery monitoring and blocks until monitorLoop has fully exited, so callers (notably tests tearing down a t.TempDir()-backed config dir) can rely on monitorLoop no longer touching any shared state — e.g. the tmux exec-gate directory — once Stop returns. The wait is bounded: a monitorLoop stuck mid-refresh (blocked on a contended exec-gate slot) logs a warning and lets the caller proceed rather than hanging teardown indefinitely. stopOnce guards against a double-close panic if Stop is ever called more than once on the same instance.

type PTYDiscoveryConfig

type PTYDiscoveryConfig struct {
	// Primary tmux server socket for squad-managed sessions
	// Empty string means use the default tmux server
	PrimarySocket string

	// ExternalSockets are additional tmux servers to scan for external instances
	// Only used when Mode is Extended or Full
	ExternalSockets []string

	// Mode controls discovery scope and permissions
	Mode DiscoveryMode

	// ManagedPrefix is the tmux session prefix for squad-managed sessions
	// Default: "staplersquad_"
	ManagedPrefix string

	// DiscoverExternal enables discovery of non-prefixed Claude instances
	// Automatically enabled for Extended and Full modes
	DiscoverExternal bool

	// AllowExternalAttach permits attaching to external instances
	// Only effective in Full mode
	AllowExternalAttach bool

	// RequireConfirmation requires user confirmation for external operations
	// Recommended to keep true for safety
	RequireConfirmation bool

	// DiscoveryInterval controls how often to refresh discovery
	DiscoveryInterval time.Duration

	// ParallelDiscovery enables parallel scanning of multiple tmux servers
	ParallelDiscovery bool
}

PTYDiscoveryConfig controls PTY discovery scope and behavior

func DefaultPTYDiscoveryConfig

func DefaultPTYDiscoveryConfig() PTYDiscoveryConfig

DefaultPTYDiscoveryConfig returns the default discovery configuration

func (*PTYDiscoveryConfig) CanAttachExternal

func (c *PTYDiscoveryConfig) CanAttachExternal() bool

CanAttachExternal returns true if attaching to external instances is allowed

func (*PTYDiscoveryConfig) ShouldDiscoverExternal

func (c *PTYDiscoveryConfig) ShouldDiscoverExternal() bool

ShouldDiscoverExternal returns true if external instances should be discovered

type PTYDiscoveryOption added in v1.18.0

type PTYDiscoveryOption func(*PTYDiscovery)

PTYDiscoveryOption is a functional option for PTYDiscovery construction.

func WithSessionLister added in v1.18.0

func WithSessionLister(l tmux.SessionLister) PTYDiscoveryOption

WithSessionLister injects a SessionLister; used in tests to avoid exec.Command forks.

type PTYStatus

type PTYStatus int

PTYStatus represents the current state of a PTY

const (
	PTYReady PTYStatus = iota // Waiting for input
	PTYBusy                   // Executing command
	PTYIdle                   // No activity
	PTYError                  // Error state
)

func (PTYStatus) String

func (s PTYStatus) String() string

type PTYSubscriber added in v1.35.0

type PTYSubscriber interface {
	// Push appends data to the buffer. Must be goroutine-safe and never block.
	// Returns ErrSubscriberFull if the buffer is at capacity; the caller should
	// then close the subscriber and force the consumer to reconnect.
	Push(data []byte) error
	// Chan returns the receive-only channel from which the consumer reads buffered
	// data. The channel is closed when Close is called and all queued data is drained.
	Chan() <-chan []byte
	// Close signals that no more data will be pushed and releases resources.
	Close()
}

PTYSubscriber is a lossless, ordered buffer for raw PTY bytes from a single session. fanOut calls Push; the consumer reads from Chan. Implementations must be goroutine-safe.

The interface is intentionally minimal so that alternative backends (e.g. a memory-mapped circular file for large or persistent sessions) can be substituted without changing callers.

type PaginatedFetcher added in v1.41.0

type PaginatedFetcher interface {
	// FetchAll retrieves items across multiple pages up to an
	// implementation-defined cap, returning the aggregated items, the newest
	// cursor value seen, and possiblyIncomplete=true if the cap was hit
	// while more results may still exist beyond it.
	FetchAll(ctx context.Context, config PluginConfig, cursor string) (items []ExternalItem, newCursor string, possiblyIncomplete bool, err error)
}

PaginatedFetcher is an optional capability an ItemSourcePlugin can implement for retrieving its complete result set across all pages, rather than Fetch's single-page/incremental-sync behavior. Consumers that need the full current state regardless of page size (e.g. SyncLoop.PreviewBackwardSyncImpact) should type-assert for this interface and prefer FetchAll when a plugin implements it, falling back to a plain Fetch call otherwise.

type PendingApproval

type PendingApproval struct {
	Request      *detection.ApprovalRequest
	Decision     *PolicyDecision
	ReceivedAt   time.Time
	ExpiresAt    time.Time
	Status       PendingApprovalStatus
	UserResponse *detection.ApprovalResponse
}

PendingApproval represents an approval request awaiting action.

type PendingApprovalStatus

type PendingApprovalStatus string

PendingApprovalStatus tracks the state of a pending approval.

const (
	PendingStatusAwaiting  PendingApprovalStatus = "awaiting"
	PendingStatusProcessed PendingApprovalStatus = "processed"
	PendingStatusExpired   PendingApprovalStatus = "expired"
	PendingStatusCancelled PendingApprovalStatus = "cancelled"
)

type PipelineEngine added in v1.38.0

type PipelineEngine interface {
	// SlashCommandSet returns the filename→rendered-content map that
	// WriteSlashCommands writes to .claude/commands/backlog/ for item.
	SlashCommandSet(item *BacklogItemData) (map[string]string, error)
	// TriagePromptFor builds the headless-triage prompt for item.
	TriagePromptFor(item *BacklogItemData, artifactAbsPath string) string
	// ReviewPromptFor builds the headless-review prompt for item.
	ReviewPromptFor(item *BacklogItemData, acSnapshot []AcCriterion, diff string, diffTruncated bool, verificationNotes string, extras ReviewContextExtras) string
	// InteractiveReviewPromptFor builds the tool-call-style review prompt used
	// by the automatic review gate's real, hidden session.Instance (see
	// ReviewGateRunner.Run) — the review path most items actually go through.
	// Unlike ReviewPromptFor (JSON-output style, for headless callers with no
	// tool access), this asks the reviewer to call submit_review_verdict.
	// PipelineModeDefault renders BuildReviewPrompt; a resolved custom mode
	// renders the same ReviewPromptTemplate field ReviewPromptFor uses — mode
	// authors must include verdict-tool-call instructions in that template if
	// they want it to drive the real review gate too.
	InteractiveReviewPromptFor(item *BacklogItemData, acSnapshot []AcCriterion, diff string, diffTruncated bool, itemSessionID string, verificationNotes string) string
	// InitialPromptFor builds the interactive/autonomous session's initial
	// prompt (inst.Prompt).
	InitialPromptFor(item *BacklogItemData, priorSessions []ItemSessionSummary) string
	// ContentHashFor returns the content hash of a resolved mode's 9 raw
	// content-template fields. ok is false for PipelineModeDefault (code-
	// backed content can't drift without a redeploy — nothing to hash) or an
	// unresolved slug.
	ContentHashFor(mode PipelineMode) (hash string, ok bool)
}

PipelineEngine is the narrow (5-method) seam described in the package doc comment above. CachingPipelineEngine is its single concrete implementation.

The method count (5) intentionally exceeds the usual 1-3-method interface- segregation guidance: each method shares the same resolve-and-render mechanism and has a genuine, independently-verified caller elsewhere in the codebase (see plan.md's Pattern Decisions row on method count) — splitting them into multiple interfaces would be exactly the kind of speculative interface-pollution .claude/rules/interface-pollution-checklist.md warns against, not less of it.

type PipelineMode added in v1.38.0

type PipelineMode string

PipelineMode identifies which PipelineMode definition (by slug) drives a backlog item's triage/work/review content.

const PipelineModeDefault PipelineMode = ""

PipelineModeDefault is the sentinel PipelineMode value meaning "no mode chosen — use the pre-existing hardcoded pipeline." Resolving this value is guaranteed, by construction, to never touch pipelineModeCache or PipelineModeRepository — this is the concrete mechanism that keeps "no uncached DB read on the hot path for the common case" true without needing runtime feature-flagging.

type PipelineModeContentFields added in v1.38.0

type PipelineModeContentFields struct {
	Slug         string
	ValidateSlug bool

	StatusCommandTemplate string
	DoneCommandTemplate   string
	FailCommandTemplate   string
	ReviewCommandTemplate string
	ShipCommandTemplate   string
	HelpCommandTemplate   string
	TriagePromptTemplate  string
	ReviewPromptTemplate  string
	InitialPromptTemplate string
}

PipelineModeContentFields groups the slug and the 9 content-template fields ValidatePipelineModeContent checks.

ValidateSlug should be true only for a Create-style call: slug is required and format-checked there. Update never sets it, because UpdatePipelineModeRequest has no slug field at all — slug is immutable after creation (see proto/session/v1/backlog.proto) — so there is nothing for an Update call to validate.

Any content-template field left as "" (e.g. a field a partial Update request didn't set) trivially passes both content checks below (an empty string contains no shell metacharacters and no placeholder tokens), so callers building this struct for an Update only need to populate whichever fields the request actually sets — omitted fields never fail validation they didn't ask for.

type PipelineModeCreateInput added in v1.38.0

type PipelineModeCreateInput struct {
	Slug        string
	Name        string
	Description string
	Enabled     bool

	StatusCommandTemplate string
	DoneCommandTemplate   string
	FailCommandTemplate   string
	ReviewCommandTemplate string
	ShipCommandTemplate   string
	HelpCommandTemplate   string
	TriagePromptTemplate  string
	ReviewPromptTemplate  string
	InitialPromptTemplate string
}

PipelineModeCreateInput holds the fields for creating a new pipeline mode.

type PipelineModeRepository added in v1.38.0

type PipelineModeRepository interface {
	Create(ctx context.Context, m PipelineModeCreateInput) (*ent.PipelineMode, error)
	Update(ctx context.Context, id uuid.UUID, m PipelineModeUpdateInput) (*ent.PipelineMode, error)
	Delete(ctx context.Context, id uuid.UUID) error
	GetByID(ctx context.Context, id uuid.UUID) (*ent.PipelineMode, error)
	GetBySlug(ctx context.Context, slug string) (*ent.PipelineMode, error)
	ListAll(ctx context.Context) ([]*ent.PipelineMode, error)
	ListEnabled(ctx context.Context) ([]*ent.PipelineMode, error)
}

PipelineModeRepository defines persistence operations for pipeline mode definitions.

type PipelineModeUpdateInput added in v1.38.0

type PipelineModeUpdateInput struct {
	Name        *string
	Description *string
	Enabled     *bool

	StatusCommandTemplate *string
	DoneCommandTemplate   *string
	FailCommandTemplate   *string
	ReviewCommandTemplate *string
	ShipCommandTemplate   *string
	HelpCommandTemplate   *string
	TriagePromptTemplate  *string
	ReviewPromptTemplate  *string
	InitialPromptTemplate *string
}

PipelineModeUpdateInput holds optional fields for updating an existing pipeline mode. Pointer fields are only applied when non-nil (partial update).

type PluginConfig added in v1.35.0

type PluginConfig struct {
	Raw string // JSON
}

PluginConfig is opaque config passed to a plugin. Plugins decode their own fields.

type PluginRegistry added in v1.35.0

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

PluginRegistry holds registered source plugins.

func NewDefaultRegistry added in v1.35.0

func NewDefaultRegistry() *PluginRegistry

NewDefaultRegistry returns a registry with all built-in plugins registered.

func NewPluginRegistry added in v1.35.0

func NewPluginRegistry() *PluginRegistry

NewPluginRegistry creates a new empty PluginRegistry.

func (*PluginRegistry) Get added in v1.35.0

Get retrieves a plugin by ID.

func (*PluginRegistry) Register added in v1.35.0

func (r *PluginRegistry) Register(p ItemSourcePlugin)

Register adds a plugin to the registry.

type PolicyAction

type PolicyAction string

PolicyAction specifies what to do when a policy matches.

const (
	ActionAutoApprove PolicyAction = "auto_approve"
	ActionAutoReject  PolicyAction = "auto_reject"
	ActionPrompt      PolicyAction = "prompt"
	ActionLog         PolicyAction = "log_only"
)

type PolicyAuditEntry

type PolicyAuditEntry struct {
	Timestamp      time.Time                  `json:"timestamp"`
	RequestID      string                     `json:"request_id"`
	PolicyID       string                     `json:"policy_id"`
	PolicyName     string                     `json:"policy_name"`
	Action         PolicyAction               `json:"action"`
	MatchedRequest *detection.ApprovalRequest `json:"matched_request"`
	Reason         string                     `json:"reason"`
}

PolicyAuditEntry records policy evaluation results.

type PolicyCondition

type PolicyCondition struct {
	Field    string `json:"field"`    // Field to check (e.g., "command", "file_path")
	Operator string `json:"operator"` // "equals", "contains", "regex", "not_contains"
	Value    string `json:"value"`    // Value to compare against
	// contains filtered or unexported fields
}

PolicyCondition represents a single condition that must be met.

type PolicyDecision

type PolicyDecision struct {
	Request       *detection.ApprovalRequest `json:"request"`
	Timestamp     time.Time                  `json:"timestamp"`
	Decision      PolicyAction               `json:"decision"`
	Matched       bool                       `json:"matched"`
	MatchedPolicy *ApprovalPolicy            `json:"matched_policy,omitempty"`
	Reason        string                     `json:"reason"`
}

PolicyDecision represents the result of policy evaluation.

type PolicyEngine

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

PolicyEngine manages approval policies and evaluates approval requests.

func NewPolicyEngine

func NewPolicyEngine() *PolicyEngine

NewPolicyEngine creates a new approval policy engine.

func (*PolicyEngine) AddPolicy

func (pe *PolicyEngine) AddPolicy(policy *ApprovalPolicy) error

AddPolicy adds a new approval policy.

func (*PolicyEngine) ClearAuditLog

func (pe *PolicyEngine) ClearAuditLog()

ClearAuditLog removes all audit log entries.

func (*PolicyEngine) Evaluate

func (pe *PolicyEngine) Evaluate(request *detection.ApprovalRequest) (*PolicyDecision, error)

Evaluate evaluates an approval request against all policies.

func (*PolicyEngine) GetAuditLog

func (pe *PolicyEngine) GetAuditLog(limit int) []PolicyAuditEntry

GetAuditLog returns recent audit log entries.

func (*PolicyEngine) GetPolicy

func (pe *PolicyEngine) GetPolicy(id string) *ApprovalPolicy

GetPolicy retrieves a policy by ID.

func (*PolicyEngine) GetStatistics

func (pe *PolicyEngine) GetStatistics() PolicyStatistics

GetStatistics returns statistics about policy usage.

func (*PolicyEngine) ListPolicies

func (pe *PolicyEngine) ListPolicies() []*ApprovalPolicy

ListPolicies returns all policies, sorted by priority.

func (*PolicyEngine) RemovePolicy

func (pe *PolicyEngine) RemovePolicy(id string) bool

RemovePolicy removes a policy by ID.

func (*PolicyEngine) SetMaxAuditLog

func (pe *PolicyEngine) SetMaxAuditLog(max int)

SetMaxAuditLog sets the maximum number of audit log entries to keep.

func (*PolicyEngine) UpdatePolicy

func (pe *PolicyEngine) UpdatePolicy(updated *ApprovalPolicy) error

UpdatePolicy updates an existing policy.

type PolicyStatistics

type PolicyStatistics struct {
	TotalPolicies     int
	EnabledPolicies   int
	TotalEvaluations  int
	AutoApprovals     int
	AutoRejections    int
	PromptedApprovals int
	LoggedOnly        int
}

PolicyStatistics provides summary statistics.

type Priority

type Priority = queue.Priority

Priority re-export

type ProcessFileInspector

type ProcessFileInspector interface {
	OpenFiles(pid int32) ([]string, error)
	IsAlive(pid int32, expectedCreateTimeMs int64) bool
}

ProcessFileInspector is the interface used by HistoryFileDetector. This allows mocking in tests.

type ProcessManager added in v1.35.0

type ProcessManager interface {
	// Lifecycle
	Start(dir string) error
	RestoreWithWorkDir(workDir string) error
	Close() error
	IsAlive() bool

	// Identification
	GetSessionIdentifier() string

	// Existence / state
	HasSession() bool

	// Working directory (via pane or process introspection)
	GetCurrentWorkingDirectory() (string, error)

	// Terminal I/O
	GetPTY() (*os.File, error)
	SendKeys(keys string) (int, error)
	TapEnter() error
	SendPromptWithEnter(prompt string) error
	SendInputViaControlMode(ctx context.Context, data []byte) error

	// Terminal state
	CapturePaneContent() (string, error)
	CapturePaneContentRaw() (string, error)
	CapturePaneContentWithOptions(startLine, endLine string) (string, error)
	CaptureViewport(lines int) (string, error)
	GetCursorPosition() (x, y int, err error)
	GetPaneDimensions() (width, height int, err error)
	SetWindowSize(cols, rows int) error
	SetDetachedSize(width, height int, instanceTitle string) error
	RefreshClient() error

	// Process metadata
	GetPanePID() (int32, error)

	// Content helpers
	HasUpdated() (updated bool, hasPrompt bool, content string)
	FilterBanners(content string) (string, int)
	HasMeaningfulContent(content string) bool

	// Streaming (control mode)
	StartControlMode() error
	StopControlMode() error
	// SubscribeToControlModeUpdates returns a subscription ID and a bidirectional channel.
	// The channel must be bidirectional (chan []byte, not <-chan []byte) because some callers
	// write synthetic frames for testing. Implementations must not write to the channel themselves.
	SubscribeToControlModeUpdates() (string, chan []byte)
	UnsubscribeFromControlModeUpdates(id string)

	// Attach (interactive TUI)
	Attach() (chan struct{}, error)
	DetachSafely() error

	// Exit notifications
	SetOnExitCallback(fn func(string))
	ResetExitOnce()
}

ProcessManager abstracts terminal process lifecycle and I/O. Implementations: TmuxBackend (wraps TmuxProcessManager), NativeProcessManager (Phase 2).

func NewProcessManager added in v1.35.0

func NewProcessManager(_ context.Context, defaultBackend ProcessManagerBackend, opts ProcessManagerOptions) ProcessManager

NewProcessManager returns the ProcessManager implementation selected by the registered backend. Falls back to TmuxBackend for unknown values.

type ProcessManagerBackend added in v1.35.0

type ProcessManagerBackend string

ProcessManagerBackend identifies the backend implementation.

const (
	BackendTmux   ProcessManagerBackend = "tmux"
	BackendNative ProcessManagerBackend = "native"
)

type ProcessManagerOptions added in v1.35.0

type ProcessManagerOptions struct {
	SessionName  string
	Prefix       string
	ServerSocket string
	Program      string
	Args         []string
}

ProcessManagerOptions holds constructor parameters for NewProcessManager.

type ProgressNoteData added in v1.38.0

type ProgressNoteData struct {
	ID             string
	CriterionIndex int
	Note           string
	Status         string
	CreatedAt      time.Time
}

ProgressNoteData is the domain DTO replacing *ent.BacklogProgressNote in Storage returns. Unlike the current-note-per-criterion stored on BacklogItem.AcceptanceCriteria, this represents a single append-only history entry from one report_progress call.

type ProjectData added in v1.23.0

type ProjectData struct {
	// ID is the unique project name (used as string external identifier)
	ID          string
	Name        string
	Description string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

ProjectData is the domain model for a project that groups sessions.

type QueueDequeuer added in v1.41.0

type QueueDequeuer interface {
	DequeueNextQueuedItems(ctx context.Context) error
}

QueueDequeuer claims and spawns as many queued (and, by default, "ready" — config.Config.AutoSpawnReadyItemsOrDefault) backlog items as there are free WIP slots, highest-priority first. Called the moment a slot frees up (onSessionExited) and by the periodic ReconcileStuck sweep as a safety net for a missed exit hook, a concurrency limit raised while items were waiting, or an item reaching "ready" between ticks.

type Registry added in v1.35.0

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

Registry owns the sessionID → live-actor mapping plus refcounts. Its mutex guards map membership only — not per-field Instance state. Construct with NewRegistry; the zero value is not usable.

func NewRegistry added in v1.35.0

func NewRegistry(storage *Storage, onConstruct func(*LiveInstance)) *Registry

NewRegistry constructs a Registry. onConstruct may be nil (e.g. daemon.go's own Registry, which has no SessionService to wire callbacks for); Acquire nil-checks before calling it.

func (*Registry) Acquire added in v1.35.0

func (r *Registry) Acquire(sessionID string) (*LiveInstance, ReleaseFunc, error)

Acquire returns the live handle for sessionID, constructing its actor on first access. On success, the caller MUST call the returned ReleaseFunc exactly once — prefer WithInstance for synchronous single-call-stack callers to avoid forgetting it.

Three outcomes:

  1. Not in storage → ErrSessionNotFound
  2. Not in map, construction succeeds → new entry, refcount=1
  3. Already in map (or races with concurrent Acquire) → refcount++

func (*Registry) AcquireAll added in v1.35.0

func (r *Registry) AcquireAll() ([]*LiveInstance, ReleaseFunc, error)

AcquireAll acquires every session known to Storage in one call; returns one release closing over all of them. Sugar for sweep-style callers (health.go, hibernation_sweeper.go). Sessions that fail to Acquire are logged and skipped, not returned.

func (*Registry) Count added in v1.35.0

func (r *Registry) Count() int

Count returns the number of live entries.

func (*Registry) ForceRelease added in v1.35.0

func (r *Registry) ForceRelease(sessionID string)

ForceRelease tears down sessionID's actor and map entry immediately, regardless of refcount (R2.18 — DeleteSession's force-invalidate; also used by CreateSession to abort a Register()'d entry when the immediately-following storage.AddInstance fails).

Other holders' *LiveInstance pointers stay valid Go values; their next command must return a typed error (Story 2.5.9c's contract, implemented in Epic 3), never hang.

For CreateSession's abort path: use ForceRelease (not the release() closure Register returned) because a concurrent Acquire racing between Register and the abort would bump refcount to 2, making plain release() decrement 2→1 and leave the phantom entry alive. ForceRelease deletes unconditionally, regardless of current refcount.

func (*Registry) List added in v1.35.0

func (r *Registry) List() []*LiveInstance

List returns a snapshot of all currently-live instances, holding the lock only for the copy.

func (*Registry) Register added in v1.35.0

func (r *Registry) Register(instance *LiveInstance) (ReleaseFunc, error)

Register is the construction-time counterpart to Acquire (R2.18a). CreateSession builds a brand-new *LiveInstance via NewInstance (no persisted row exists yet for Acquire to look up) and hands it to Register before calling storage.AddInstance.

Register deliberately does NOT invoke onConstruct: CreateSession already performs its own explicit post-construction wiring, so routing Register through onConstruct would wire the same callbacks twice. onConstruct exists solely to backfill wiring for the Acquire-from-storage path (sessions loaded on server restart), which has no other caller positioned to do it.

No double-checked locking here (unlike Acquire): Register has no storage I/O to release the lock around, so the whole check-then-insert runs under one lock acquisition.

func (*Registry) Shutdown added in v1.35.0

func (r *Registry) Shutdown()

Shutdown force-stops every actor regardless of refcount. Register this as a shutdownHooks entry (Story 2.5.5d) so it fires on server shutdown.

func (*Registry) Storage added in v1.35.0

func (r *Registry) Storage() *Storage

Storage returns the Registry's backing storage. Used by callers (e.g. daemon.go) that need access to storage through a Registry reference.

func (*Registry) WithInstance added in v1.35.0

func (r *Registry) WithInstance(ctx context.Context, sessionID string, fn func(*LiveInstance) error) error

WithInstance is the preferred entry point for synchronous, single-call-stack callers (RPC handlers, one-shot lookups) where forgetting release() is the common failure mode. Reserve raw Acquire/release() for genuinely long-lived holders (WebSocket streams, poller caches, background goroutines).

type RegistryInspector added in v1.35.0

type RegistryInspector interface {
	List() []*LiveInstance
	Count() int
}

RegistryInspector is the narrowest interface for callers that only enumerate live instances without acquiring individual handles.

type ReleaseFunc added in v1.35.0

type ReleaseFunc func()

ReleaseFunc is the refcount-gated teardown closure returned by Acquire and Register. It is idempotent (safe to call more than once via an internal sync.Once) and must be called exactly once per successful Acquire/Register to avoid refcount leaks. Distinct from ForceReleaseFunc so that future callers storing it generically cannot silently conflate refcount-gated and unconditional teardown (type-driven-audit finding B).

type RepoPathManager

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

RepoPathManager handles GOPATH-style repository path management. Repositories are stored in a consistent location based on their URL:

  • ~/.stapler-squad/repos/github.com/owner/repo (main clone)
  • Worktrees are created relative to the main repo as needed

func NewRepoPathManager

func NewRepoPathManager() *RepoPathManager

NewRepoPathManager creates a new RepoPathManager with the default base directory.

func NewRepoPathManagerWithBase

func NewRepoPathManagerWithBase(baseDir string) *RepoPathManager

NewRepoPathManagerWithBase creates a RepoPathManager with a custom base directory.

func (*RepoPathManager) EnsureRepoCloned

func (m *RepoPathManager) EnsureRepoCloned(ctx context.Context, ref *GitHubRef) (string, error)

EnsureRepoCloned ensures the repository is cloned to the local path. If already cloned, it fetches the latest changes. Returns the path to the cloned repository. ctx bounds the underlying git fetch/clone subprocess (via safeexec.CommandContext) with a hard per-operation timeout, so callers that cancel ctx actually kill the subprocess rather than merely abandoning the RPC while it keeps running.

func (*RepoPathManager) GetCloneURL

func (m *RepoPathManager) GetCloneURL(ref *GitHubRef) string

GetCloneURL returns the git clone URL for a GitHub ref, injecting a stored keychain token for the ref's host when one is available (required for private repos on GitHub Enterprise hosts, where an unauthenticated clone would otherwise fail).

func (*RepoPathManager) GetRepoPath

func (m *RepoPathManager) GetRepoPath(ref *GitHubRef) string

GetRepoPath returns the local path where a GitHub repo should be stored. Format: ~/.stapler-squad/repos/<host>/owner/repo

func (*RepoPathManager) ResolveGitHubInput

func (m *RepoPathManager) ResolveGitHubInput(input string) (localPath string, ref *GitHubRef, err error)

ResolveGitHubInput takes a GitHub URL/shorthand and returns a resolved path. It clones the repo if necessary and returns the local path. Also returns the parsed GitHubRef for storing metadata.

func (*RepoPathManager) ResolveGitHubInputCtx added in v1.41.0

func (m *RepoPathManager) ResolveGitHubInputCtx(ctx context.Context, input string) (localPath string, ref *GitHubRef, err error)

ResolveGitHubInputCtx takes a GitHub URL/shorthand and returns a resolved path, threading ctx down to EnsureRepoCloned so the underlying git clone/fetch subprocess is actually cancelled if ctx is cancelled or times out.

func (*RepoPathManager) ResolveGitHubInputCtxWithHosts added in v1.41.0

func (m *RepoPathManager) ResolveGitHubInputCtxWithHosts(ctx context.Context, input string, enterpriseHosts []string) (localPath string, ref *GitHubRef, err error)

ResolveGitHubInputCtxWithHosts takes a GitHub URL/shorthand and returns a resolved path, recognizing URLs against the given GitHub Enterprise hostnames in addition to github.com, and threading ctx down to EnsureRepoCloned so the underlying git clone/fetch subprocess is actually cancelled if ctx is cancelled or times out.

type Repository

type Repository interface {
	// Create inserts a new session into storage
	Create(ctx context.Context, data InstanceData) error

	// Update modifies an existing session in storage
	Update(ctx context.Context, data InstanceData) error

	// Delete removes a session from storage by title
	Delete(ctx context.Context, title string) error

	// Get retrieves a single session by title with full child data
	// For selective loading, use GetWithOptions instead
	Get(ctx context.Context, title string) (*InstanceData, error)

	// GetWithOptions retrieves a single session with selective child data loading
	// Use LoadOptions presets (LoadMinimal, LoadSummary, LoadFull) or custom options
	GetWithOptions(ctx context.Context, title string, options LoadOptions) (*InstanceData, error)

	// List retrieves all sessions with summary child data (no diff content)
	// For selective loading, use ListWithOptions instead
	List(ctx context.Context) ([]InstanceData, error)

	// ListWithOptions retrieves all sessions with selective child data loading
	// Use LoadOptions presets (LoadMinimal, LoadSummary, LoadFull) or custom options
	ListWithOptions(ctx context.Context, options LoadOptions) ([]InstanceData, error)

	// ListByStatus retrieves sessions filtered by status with summary child data
	// For selective loading, use ListByStatusWithOptions instead
	ListByStatus(ctx context.Context, status Status) ([]InstanceData, error)

	// ListByStatusWithOptions retrieves sessions filtered by status with selective loading
	ListByStatusWithOptions(ctx context.Context, status Status, options LoadOptions) ([]InstanceData, error)

	// ListByTag retrieves sessions with a specific tag with summary child data
	// For selective loading, use ListByTagWithOptions instead
	ListByTag(ctx context.Context, tag string) ([]InstanceData, error)

	// ListByTagWithOptions retrieves sessions with a specific tag with selective loading
	ListByTagWithOptions(ctx context.Context, tag string, options LoadOptions) ([]InstanceData, error)

	// UpdateTimestamps efficiently updates only timestamp fields for a session
	// This is optimized for frequent updates from WebSocket terminal streaming
	UpdateTimestamps(ctx context.Context, title string, lastTerminalUpdate, lastMeaningfulOutput time.Time, lastOutputSignature string) error

	// UpdateReviewQueueState efficiently updates the review-queue interaction fields
	// (LastUserResponse, ProcessingGraceUntil, LastPromptDetected, LastPromptSignature)
	// without the read-modify-write overhead of a full Get+Update cycle.
	UpdateReviewQueueState(ctx context.Context, title string, lastUserResponse, processingGraceUntil, lastPromptDetected time.Time, lastPromptSignature string) error

	// UpdateLastAddedToQueue sets only the last_added_to_queue field for a session.
	// Issues a single UPDATE WHERE title=? without a prior SELECT.
	UpdateLastAddedToQueue(ctx context.Context, title string, t time.Time) error

	// UpdateLastAcknowledged sets only the last_acknowledged field for a session.
	// Issues a single UPDATE WHERE title=? without a prior SELECT.
	UpdateLastAcknowledged(ctx context.Context, title string, t time.Time) error

	// UpdateLastViewed sets only the last_viewed field for a session.
	// Issues a single UPDATE WHERE title=? without a prior SELECT.
	UpdateLastViewed(ctx context.Context, title string, t time.Time) error

	// UpdateSessionMetadata efficiently updates only title/category/note/working_dir
	// fields for a session, issuing a single UPDATE WHERE title=? without a prior SELECT
	// and without touching worktree/diffstats/tags/claude_session rows (unlike Update).
	// currentTitle must be the row's title from before any rename applied in this same
	// call — see the EntRepository implementation for why. A nil field pointer leaves
	// that field untouched; Note is written whenever non-nil (including "") since an
	// empty note is a meaningful cleared state, not "unset".
	UpdateSessionMetadata(ctx context.Context, currentTitle string, newTitle, category, note, workingDir *string) error

	// Close performs cleanup and releases resources
	Close() error

	// GetSession retrieves a session using the new Session domain model.
	// Use ContextOptions to control which optional contexts are loaded.
	// Returns nil if session not found.
	GetSession(ctx context.Context, title string, opts ContextOptions) (*Session, error)

	// ListSessions retrieves all sessions using the new Session domain model.
	// Use ContextOptions to control which optional contexts are loaded.
	ListSessions(ctx context.Context, opts ContextOptions) ([]*Session, error)

	// CreateSession creates a new session from the Session domain model.
	CreateSession(ctx context.Context, session *Session) error

	// UpdateSession updates an existing session using the Session domain model.
	UpdateSession(ctx context.Context, session *Session) error

	// AllRules returns all auto-approval rules.
	AllRules(ctx context.Context) ([]ApprovalRuleData, error)
	// UpsertRule creates or updates an auto-approval rule.
	UpsertRule(ctx context.Context, rule ApprovalRuleData) error
	// DeleteRule removes an auto-approval rule by ID.
	DeleteRule(ctx context.Context, id string) error

	// RecordAnalytics logs a classification decision.
	RecordAnalytics(ctx context.Context, data AnalyticsData) error
	// ListAnalytics retrieves recent classification decisions.
	ListAnalytics(ctx context.Context, limit int) ([]AnalyticsData, error)

	// ListAnalyticsSince retrieves analytics entries with created_at >= since.
	// Replaces the in-Go date filter in LoadWindow. Implements AC-1.
	// Pass limit=0 for no limit.
	ListAnalyticsSince(ctx context.Context, since time.Time, limit int) ([]AnalyticsData, error)

	// ListAnalyticsByProgramSince retrieves entries for a specific program since a time.
	// Uses the compound index (command_program, created_at). Implements AC-3.
	// Pass limit=0 for no limit.
	ListAnalyticsByProgramSince(ctx context.Context, program string, since time.Time, limit int) ([]AnalyticsData, error)

	// GetSubcommandBreakdown returns per-(subcommand, decision) counts for a program
	// in the given time window. Uses SQL GROUP BY via ent Aggregate. Implements AC-4.
	GetSubcommandBreakdown(ctx context.Context, program string, since time.Time) ([]SubcommandDecisionCount, error)

	// ListRecentCommandsByProgram returns the most recent n command_preview strings
	// for (program, subcommand). Pass subcommand="" to match all subcommands.
	// Implements AC-5.
	ListRecentCommandsByProgram(ctx context.Context, program, subcommand string, since time.Time, n int) ([]string, error)

	// GetSubcommandTrend returns raw analytics rows for (program, subcommand) since
	// a given time. The caller buckets these using ComputeDailyBuckets. Implements AC-6.
	// Pass subcommand="" to match all subcommands for the program.
	GetSubcommandTrend(ctx context.Context, program, subcommand string, since time.Time) ([]AnalyticsData, error)

	// CreateProject inserts a new project.
	CreateProject(ctx context.Context, data ProjectData) (*ProjectData, error)
	// ListProjects returns all projects.
	ListProjects(ctx context.Context) ([]ProjectData, error)
	// UpdateProject modifies an existing project.
	UpdateProject(ctx context.Context, data ProjectData) (*ProjectData, error)
	// DeleteProject removes a project by name; sessions are unassigned.
	DeleteProject(ctx context.Context, name string) error
	// AssignSessionsToProject links sessions (by title) to a project (by name).
	AssignSessionsToProject(ctx context.Context, projectName string, sessionTitles []string) error

	// CreateBacklogItem inserts a new backlog item.
	CreateBacklogItem(ctx context.Context, data BacklogItemData) (*BacklogItemData, error)
	// GetBacklogItem retrieves a backlog item by UUID string.
	GetBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)
	// ListBacklogItems returns backlog items with optional filtering.
	ListBacklogItems(ctx context.Context, filter BacklogItemFilter) ([]BacklogItemData, error)
	// UpdateBacklogItem modifies an existing backlog item with optional precondition check.
	UpdateBacklogItem(ctx context.Context, id string, update BacklogItemUpdate, precondition *BacklogItemPrecondition) (*BacklogItemData, error)
	// ArchiveBacklogItem sets the archived_at timestamp on a backlog item.
	ArchiveBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)
	// UnarchiveBacklogItem clears archived_at and restores the item to "idea".
	UnarchiveBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)
	// DeleteBacklogItem permanently removes an item and all its child records.
	DeleteBacklogItem(ctx context.Context, id string) error
	// TransitionBacklogItemStatus changes the status of a backlog item with optional precondition.
	// triggeredBy records who/what caused the transition (TriggeredByUser or TriggeredBySystem)
	// in the resulting BacklogStatusEvent audit row.
	TransitionBacklogItemStatus(ctx context.Context, id string, toStatus BacklogStatus, precondition *BacklogItemPrecondition, triggeredBy string) (*BacklogItemData, error)
	// GetAllItemSessionsWithBacklogInfo returns all item sessions joined with their parent backlog item metadata.
	// Used by the Insights dashboard to annotate sessions with backlog context.
	GetAllItemSessionsWithBacklogInfo(ctx context.Context) ([]ItemSessionBacklogEntry, error)
	// ListBacklogItemSummaries returns lightweight summaries for the list view.
	// Unlike ListBacklogItems it omits Description/plan fields and eagerly loads
	// ItemSessions (with ReviewVerdict) without over-fetching status events.
	ListBacklogItemSummaries(ctx context.Context, filter BacklogItemFilter) ([]BacklogItemSummary, error)
	// AddBacklogItemDependency records that edge.BlockedID may not be
	// dequeued/started until edge.BlockerID reaches a resolved status
	// (done). Upserts against the unique (blocker_id, blocked_id) index —
	// adding an existing pair is a no-op. Returns an error if the new edge
	// would create a cycle.
	AddBacklogItemDependency(ctx context.Context, edge BacklogItemDependencyEdge) error
	// UnresolvedBlockerItemIDs returns the subset of itemIDs that have at
	// least one BacklogItemDependency whose blocker has not reached done.
	// Batched by blocked_id so callers (DequeueNextQueuedItems,
	// transitionWithGuard) avoid an N+1 per-candidate query.
	UnresolvedBlockerItemIDs(ctx context.Context, itemIDs []string) (map[string]bool, error)
	// UnresolvedBlockerIDs returns the specific blocker item IDs still
	// unresolved for a single blocked item, for stuck-reason messaging.
	UnresolvedBlockerIDs(ctx context.Context, itemID string) ([]string, error)

	// CreateItemSource registers a new external item source.
	CreateItemSource(ctx context.Context, data ItemSourceData) (*ItemSourceData, error)
	// ListItemSources returns all registered item sources.
	ListItemSources(ctx context.Context) ([]ItemSourceData, error)
	// UpdateItemSource modifies an existing item source.
	UpdateItemSource(ctx context.Context, id string, update ItemSourceUpdate) (*ItemSourceData, error)
	// DeleteItemSource removes an item source by UUID string.
	DeleteItemSource(ctx context.Context, id string) error
}

Repository defines the interface for session persistence operations. This abstraction allows multiple storage backends (SQLite, JSON, etc.) while maintaining a consistent API for session management.

type RepositoryOption

type RepositoryOption func(interface{}) error

RepositoryOption is a function that configures a repository

func WithDatabasePath

func WithDatabasePath(path string) RepositoryOption

WithDatabasePath sets the database file path for the repository

type ResponseChunk

type ResponseChunk struct {
	Data      []byte
	Timestamp time.Time
	Error     error
}

ResponseChunk represents a chunk of output from the Claude instance.

type ResponseStream

type ResponseStream struct {
	OnEOF func() // Called when the PTY exits unexpectedly (program exit, not Stop())
	// contains filtered or unexported fields
}

ResponseStream manages real-time streaming of Claude instance responses to multiple subscribers. It reads from the PTY access layer and broadcasts output to all active subscribers.

func NewResponseStream

func NewResponseStream(sessionName string, ptyAccess *PTYAccess) *ResponseStream

NewResponseStream creates a new response stream for the given session. The bufferSize parameter determines how many chunks can be buffered per subscriber.

func NewResponseStreamWithBuffer

func NewResponseStreamWithBuffer(sessionName string, ptyAccess *PTYAccess, bufferSize int) *ResponseStream

NewResponseStreamWithBuffer creates a response stream with a custom buffer size.

func (*ResponseStream) GetBufferSize

func (rs *ResponseStream) GetBufferSize() int

GetBufferSize returns the current buffer size setting.

func (*ResponseStream) GetEscapeParser added in v1.35.0

func (rs *ResponseStream) GetEscapeParser() *analytics.EscapeCodeParser

GetEscapeParser returns the escape code parser for this stream. Used by the WebSocket handler for Stage 2 analytics observations. Returns nil if no parser is configured.

func (*ResponseStream) GetExitTail added in v1.15.0

func (rs *ResponseStream) GetExitTail() []byte

GetExitTail returns the last exitTailSize bytes from the circular buffer. The circular buffer already holds this data; no separate rolling copy is needed.

func (*ResponseStream) GetSubscriberCount

func (rs *ResponseStream) GetSubscriberCount() int

GetSubscriberCount returns the number of active subscribers.

func (*ResponseStream) GetSubscriberIDs

func (rs *ResponseStream) GetSubscriberIDs() []string

GetSubscriberIDs returns the IDs of all active subscribers.

func (*ResponseStream) GetSubscriberInfo

func (rs *ResponseStream) GetSubscriberInfo(subscriberID string) (created time.Time, exists bool)

GetSubscriberInfo returns information about a specific subscriber.

func (*ResponseStream) GetTotalBytesWritten added in v1.35.0

func (rs *ResponseStream) GetTotalBytesWritten() int64

GetTotalBytesWritten returns the monotonic PTY byte offset from the circular buffer. This is the same counter used by Stage 1 (Parse) so Stage 2 (ParseStage2) session_seq values are stable across WebSocket reconnections. Returns 0 if no buffer is available.

func (*ResponseStream) IsStarted

func (rs *ResponseStream) IsStarted() bool

IsStarted returns whether the stream is currently active.

func (*ResponseStream) SetBufferSize

func (rs *ResponseStream) SetBufferSize(size int)

SetBufferSize sets the buffer size for future subscribers. Does not affect existing subscribers.

func (*ResponseStream) SetOnOutput added in v1.9.0

func (rs *ResponseStream) SetOnOutput(fn func())

SetOnOutput registers a callback invoked each time PTY bytes arrive. Used by ClaudeController to drive event-based activity tracking in IdleDetector. Must be called before Start().

func (*ResponseStream) SetStableSessionID added in v1.35.0

func (rs *ResponseStream) SetStableSessionID(id string)

SetStableSessionID switches the escape parser's recorded session identifier from the tmux session name (used at construction time, before the owning Instance's stable UUID is available) to the stable UUID. This only affects how escape_event rows are tagged — it does not change rs.sessionName, which is still used for logging, PTY naming, and history keyed off the tmux name.

func (*ResponseStream) Start

func (rs *ResponseStream) Start(ctx context.Context) error

Start begins streaming responses from the PTY to all subscribers. This is a non-blocking call that starts a background goroutine. Use the provided context to stop the stream.

func (*ResponseStream) Stop

func (rs *ResponseStream) Stop() error

Stop stops the response stream and closes all subscriber channels. This is a blocking call that waits for the streaming goroutine to finish.

func (*ResponseStream) Subscribe

func (rs *ResponseStream) Subscribe(subscriberID string) (<-chan ResponseChunk, error)

Subscribe registers a new subscriber and returns a channel for receiving response chunks. The subscriber ID should be unique. Returns an error if the ID is already in use.

func (*ResponseStream) Unsubscribe

func (rs *ResponseStream) Unsubscribe(subscriberID string) error

Unsubscribe removes a subscriber and closes their channel.

type RestartState

type RestartState struct {
	// Working directory to restore
	WorkingDir string
	// Claude session ID for --resume flag
	ClaudeSessionID string
	// Environment variables to restore
	Environment map[string]string
	// Original command/program
	Program string
	// AutoYes flag
	AutoYes bool
	// Original prompt
	Prompt string
}

RestartState holds the state needed to restart a session

type ReviewContextExtras added in v1.38.0

type ReviewContextExtras struct {
	// PriorSessions is the full ItemSession history for this backlog item (as returned
	// by Storage.ListItemSessions). Used to render "## Prior Review Attempts" — only
	// review-role sessions with a non-nil ReviewVerdict contribute to that section.
	PriorSessions []ItemSessionSummary
	// ProgressNotes is the full append-only report_progress history for this item (as
	// returned by Storage.ListProgressNotesForItem). Used to render "## Full Notes
	// History", which supersedes the single latest-note-per-criterion view already
	// shown in "## Acceptance Criteria" with the complete timeline.
	ProgressNotes []ProgressNoteData
	// ItemDescription is the backlog item's Description, rendered in "## Item Context"
	// alongside StatusEvents. Kept separate from the *BacklogItemData already passed to
	// BuildHeadlessReviewPrompt because that pointer may not have StatusEvents loaded —
	// callers typically populate both fields together from a single freshly-loaded
	// GetBacklogItem(..., WithStatusEvents) call.
	ItemDescription string
	// StatusEvents is the item's status transition history, used to render "## Item
	// Context" alongside ItemDescription.
	StatusEvents []BacklogStatusEventData
	// TranscriptRelPath is the path (relative to codebaseWorkDir) of a searchable
	// session transcript file written by WriteReviewTranscriptFile, or "" when no
	// transcript is available (e.g. scrollback fetch failed or was empty — best-effort
	// enrichment, never required). Rendered as an instruction in "## Session
	// Transcript".
	TranscriptRelPath string
}

ReviewContextExtras bundles the additional context sources available to the empty-diff codebase-read review path (prior review verdicts, full progress-notes history, item goal/status history, and a searchable session transcript file). Passed as a single struct to BuildHeadlessReviewPrompt rather than as separate positional parameters because Go has no named/optional parameters and this prompt builder already has five — the same rationale headless.CallOptions uses for its own set of optional per-call knobs. Every field is a zero-value-safe optional: an unset field simply omits the corresponding prompt section rather than requiring a distinct code path per caller. Only rendered when diff == "" (see BuildHeadlessReviewPrompt) — this is deliberately more expensive context than the normal diff-review path carries.

type ReviewGateRunner added in v1.37.0

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

ReviewGateRunner encapsulates the spawnReviewGate logic into a testable value type. BacklogLifecycleListener holds one as a field and delegates to it.

getAutoReopener, getNotifier, and getSessionCreator are getter functions rather than stored values so that the runner always observes the latest reopener/notifier/spawner even when SetAutoReopener / SetNotifier / SetSessionCreator are called after construction.

func NewReviewGateRunner added in v1.37.0

func NewReviewGateRunner(
	storage *Storage,
	getAutoReopener func() AutoReopenSpawner,
	getNotifier func() Notifier,
	getSessionCreator func() ReviewGateSpawner,
	pipelineEngine PipelineEngine,
) *ReviewGateRunner

NewReviewGateRunner constructs a ReviewGateRunner. getAutoReopener, getNotifier, and getSessionCreator are getter functions (typically method values from BacklogLifecycleListener) so the runner sees the latest values when dynamic setters are called after construction. pipelineEngine may be nil — see the field's doc comment for the fallback.

func (*ReviewGateRunner) Run added in v1.37.0

Run executes the review gate for a backlog item session. ctx should be the listener's shutdownCtx so long-running calls are cancelled on shutdown. onPass is retained for signature compatibility with existing callers (BacklogLifecycleListener.pushAndCreatePR) but is no longer invoked directly from Run: since review now always happens in a real, hidden session.Instance, the PASS/FAIL/PARTIAL/UNVERIFIABLE outcome is only known once that session exits and calls submit_review_verdict — handled by BacklogLifecycleListener.handleReviewSessionExited, not here.

type ReviewGateSpawner added in v1.35.0

type ReviewGateSpawner interface {
	// SpawnReviewSession creates a one-shot review session for item using prompt.
	// itemSessionID is the UUID of the work ItemSession being reviewed.
	SpawnReviewSession(ctx context.Context, item *BacklogItemData, itemSessionID string, prompt string) (*Instance, error)
}

ReviewGateSpawner can create a short-lived review session for a backlog item. Deprecated: use headless.Pool via NewBacklogLifecycleListenerWithSpawner instead. Retained for backward compatibility with existing tests and callers.

type ReviewItem

type ReviewItem = queue.ReviewItem

ReviewItem re-export

type ReviewOutcome added in v1.37.0

type ReviewOutcome = domain.ReviewOutcome

ReviewOutcome is a typed verdict outcome value (PASS, FAIL, PARTIAL, UNVERIFIABLE). Type alias — session.ReviewOutcome and domain.ReviewOutcome are identical types.

type ReviewQueue

type ReviewQueue = queue.ReviewQueue

ReviewQueue re-export

func NewReviewQueue

func NewReviewQueue() *ReviewQueue

NewReviewQueue creates a new review queue.

type ReviewQueueLookup added in v1.41.0

type ReviewQueueLookup interface {
	// ReviewQueueResolvedCount returns the count of resolved and still-open review
	// queue items linked to sessionID. Returns (0, 0, nil) if no linked backlog item
	// exists (FR-6's "no backlog item" first-class empty case).
	ReviewQueueResolvedCount(ctx context.Context, sessionID string) (resolved, stillOpen int, err error)
}

ReviewQueueLookup is a small consumer-defined interface, scoped to exactly what BuildDecisionsSnapshot needs, satisfied by existing ItemSession/ReviewVerdict query code. Defined here, next to its consumer, per .claude/rules/interface-pollution-checklist.md's "define the interface where it's consumed".

type ReviewQueueObserver

type ReviewQueueObserver = queue.ReviewQueueObserver

ReviewQueueObserver re-export

type ReviewQueuePoller

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

ReviewQueuePoller automatically monitors sessions and adds them to the review queue when they become idle or need attention.

func NewReviewQueuePoller

func NewReviewQueuePoller(queue *ReviewQueue, statusManager StatusProvider, storage *Storage) *ReviewQueuePoller

NewReviewQueuePoller creates a new poller for automatically managing the review queue. The storage parameter is optional (can be nil) but required for persisting LastAddedToQueue timestamps.

func NewReviewQueuePollerWithConfig

func NewReviewQueuePollerWithConfig(queue *ReviewQueue, statusManager StatusProvider, storage *Storage, config ReviewQueuePollerConfig) *ReviewQueuePoller

NewReviewQueuePollerWithConfig creates a poller with custom configuration. The storage parameter is optional (can be nil) but required for persisting LastAddedToQueue timestamps.

func (*ReviewQueuePoller) AddInstance

func (rqp *ReviewQueuePoller) AddInstance(instance *Instance)

AddInstance adds a single instance to monitor.

func (*ReviewQueuePoller) CheckSession

func (rqp *ReviewQueuePoller) CheckSession(inst *Instance)

CheckSession checks a single session immediately (exported for ReactiveQueueManager). This allows external components to trigger immediate re-evaluation without waiting for the next poll cycle, providing <100ms feedback on user interactions. Fetches a fresh pane activity snapshot for accurate cache invalidation.

func (*ReviewQueuePoller) FindInstance

func (rqp *ReviewQueuePoller) FindInstance(sessionID string) *Instance

FindInstance finds an instance by session ID (exported for ReactiveQueueManager). Returns nil if the instance is not found in the monitored list.

func (*ReviewQueuePoller) ForceReconcile added in v1.24.0

func (rqp *ReviewQueuePoller) ForceReconcile()

ForceReconcile immediately runs session reconciliation outside the normal 30s cadence. Safe to call concurrently; typically used by the fork pressure monitor to rapidly clean up dead sessions when subprocess failures indicate stale Active states.

func (*ReviewQueuePoller) GetConfig

func (rqp *ReviewQueuePoller) GetConfig() ReviewQueuePollerConfig

GetConfig returns the current configuration.

func (*ReviewQueuePoller) GetInstances

func (rqp *ReviewQueuePoller) GetInstances() []*Instance

GetInstances returns a snapshot of all live in-memory instances held by the poller. Use this instead of LoadInstances() for read-only operations to avoid the side effect of FromInstanceData() calling Start() on every non-paused instance.

func (*ReviewQueuePoller) GetMonitoredCount

func (rqp *ReviewQueuePoller) GetMonitoredCount() int

GetMonitoredCount returns the number of instances being monitored.

func (*ReviewQueuePoller) IsRunning

func (rqp *ReviewQueuePoller) IsRunning() bool

IsRunning returns true if the poller is currently running.

func (*ReviewQueuePoller) RemoveInstance

func (rqp *ReviewQueuePoller) RemoveInstance(instanceTitle string)

RemoveInstance removes an instance from monitoring.

func (*ReviewQueuePoller) SetActivityChannel added in v1.23.0

func (rqp *ReviewQueuePoller) SetActivityChannel(ch <-chan struct{})

SetActivityChannel wires an external signal channel to the poll loop. When a signal arrives on ch, the loop snaps back to the fast interval (PollInterval). Must be called before Start(); subsequent calls have no effect once the loop is running.

func (*ReviewQueuePoller) SetApprovalProvider

func (rqp *ReviewQueuePoller) SetApprovalProvider(provider ApprovalMetadataProvider)

SetApprovalProvider sets the approval metadata provider for enriching review queue items.

func (*ReviewQueuePoller) SetInstances

func (rqp *ReviewQueuePoller) SetInstances(instances []*Instance)

SetInstances sets the list of instances to monitor.

func (*ReviewQueuePoller) Start

func (rqp *ReviewQueuePoller) Start(ctx context.Context)

Start begins polling for idle sessions.

func (*ReviewQueuePoller) Stop

func (rqp *ReviewQueuePoller) Stop()

Stop stops the poller.

func (*ReviewQueuePoller) UpdateConfig

func (rqp *ReviewQueuePoller) UpdateConfig(config ReviewQueuePollerConfig)

UpdateConfig updates the poller configuration.

type ReviewQueuePollerConfig

type ReviewQueuePollerConfig struct {
	PollInterval       time.Duration // How often to check sessions (fast path, default 2s)
	SlowPollInterval   time.Duration // Interval when review queue is empty (default 8s); 0 = no backoff
	IdleThreshold      time.Duration // Duration before considering session idle and adding to queue
	InputWaitDuration  time.Duration // Time waiting for input before flagging
	StalenessThreshold time.Duration // Duration since last meaningful output before considering stale
	ReconcileInterval  time.Duration // How often to reconcile in-memory state against tmux reality (0 = disabled)
}

ReviewQueuePollerConfig contains configuration for the review queue poller.

func DefaultReviewQueuePollerConfig

func DefaultReviewQueuePollerConfig() ReviewQueuePollerConfig

DefaultReviewQueuePollerConfig returns sensible defaults for polling.

type ReviewQueueStatistics

type ReviewQueueStatistics = queue.ReviewQueueStatistics

ReviewQueueStatistics re-export

type ReviewQueueWriter added in v1.35.0

type ReviewQueueWriter interface {
	Add(item *ReviewItem) bool
}

ReviewQueueWriter is the write-side interface for the review queue. It is satisfied by *ReviewQueue and can be used in place of the concrete type wherever only Add is required, making it easy to supply a test double.

type ReviewRespawner added in v1.39.0

type ReviewRespawner interface {
	AutoRespawnReview(ctx context.Context, itemID string) error
}

ReviewRespawner can automatically re-trigger the review gate for a backlog item stuck in review with no active session in flight (the StuckReasonAbandonedReview condition — see markAbandonedReview). Before this existed, such items were detected and notified but nothing ever respawned work on them, so they sat forever until a human noticed (see docs/tasks/backlog-feature-improvement.md, 2026-07-17 update — 4 real items went stale this way, several with nearly all acceptance criteria already marked complete, just never actually re-reviewed).

type ReviewState

type ReviewState struct {
	// LastAcknowledged tracks when the user last acknowledged this session in the review queue.
	// Sessions acknowledged after their last update won't appear in the queue until they update again.
	LastAcknowledged time.Time

	// LastAddedToQueue tracks when this session was last added to the review queue.
	// Used to prevent notification spam by enforcing a minimum re-add interval.
	LastAddedToQueue time.Time

	// LastTerminalUpdate is the timestamp of the last output received from the terminal (any output).
	LastTerminalUpdate time.Time

	// LastMeaningfulOutput is the timestamp of the last meaningful output (excludes tmux status banners).
	// Used by the review queue to determine session staleness.
	LastMeaningfulOutput time.Time

	// LastOutputSignature is a hash of the terminal content, used to detect actual changes
	// vs app restarts with unchanged content (prevents false "new activity" notifications).
	LastOutputSignature string

	// LastViewed tracks when the user last interacted with this session
	// (viewing the terminal, attaching via tmux, or viewing session details).
	// Used for smarter review queue notifications (don't notify if just viewed).
	LastViewed time.Time

	// LastPromptDetected is the timestamp when we last detected a prompt requiring user input.
	// Used to distinguish new prompts from the same prompt re-appearing.
	LastPromptDetected time.Time

	// LastPromptSignature is a hash of the prompt content (last 10 lines before cursor).
	// Used to determine if this is the same prompt or a new one.
	LastPromptSignature string

	// LastUserResponse is the timestamp when the user last provided input/interaction.
	// Used to determine if user responded AFTER a prompt was detected.
	LastUserResponse time.Time

	// ProcessingGraceUntil is the deadline for waiting for the session to respond after
	// user interaction. If the session shows no activity by this time, it may be re-added
	// to the review queue.
	ProcessingGraceUntil time.Time
	// contains filtered or unexported fields
}

ReviewState holds all timestamps and state related to the review queue and terminal activity tracking for a session. It is embedded in Instance so all field accesses remain unchanged.

Fields are NOT protected by Instance.mu. Mutation is serialized through the actor's send()/sendSyncErr() closures instead (see UpdateTerminalTimestamps in instance_approval.go, which routes through i.send() rather than i.mu.Lock()) - the same "no locking, serialize via the actor's own command queue" discipline used by transitionToLocked et al. Methods on ReviewState are intentionally non-locking - callers must be running inside an actor command closure (or otherwise be the sole writer) if concurrent access is possible.

Direct field access via Go embedding promotion (inst.LastMeaningfulOutput etc.) is used by:

  • session/review_queue_poller.go: reads LastMeaningfulOutput, LastAcknowledged, LastAddedToQueue, ProcessingGraceUntil, LastPromptDetected, LastPromptSignature, LastUserResponse, LastViewed, LastTerminalUpdate, LastOutputSignature
  • server/dependencies.go: reads LastMeaningfulOutput, LastTerminalUpdate, LastAddedToQueue, LastAcknowledged
  • server/adapters/instance_adapter.go: reads LastTerminalUpdate, LastMeaningfulOutput
  • server/review_queue_manager.go: writes LastUserResponse directly

All access is either within the session package (via the actor's serialized closures) or through Instance methods that route through i.send()/sendSyncErr().

TODO: Migrate cross-package field accesses (server/) to accessor methods to enable future encapsulation of ReviewState as a composed (non-embedded) field.

func (*ReviewState) ComputePromptSignature

func (rs *ReviewState) ComputePromptSignature(content string) string

ComputePromptSignature computes a hash of the prompt content using the last 10 lines. Returns "" if content is empty. Caller may call this without holding any lock.

func (*ReviewState) DetectAndTrackPrompt

func (rs *ReviewState) DetectAndTrackPrompt(content string, statusInfo InstanceStatusInfo, sessionTitle string) bool

DetectAndTrackPrompt detects whether the current status represents a new user-facing prompt and records it. Returns true only when a NEW prompt is detected (signature changed or first). Caller must hold Instance.mu when writing prompt fields.

func (*ReviewState) IsAcknowledgedAfterOutput

func (rs *ReviewState) IsAcknowledgedAfterOutput() bool

IsAcknowledgedAfterOutput returns true if the user acknowledged this session more recently than the last meaningful terminal output — meaning no new output has occurred since the user last dismissed the session from the review queue. Returns false when no meaningful output has been recorded yet: the acknowledgment cannot logically be "after" output that never happened, so the session is not snoozed.

Lock-free: reads both atomic shadows (lastMeaningfulOutputNs, lastAcknowledgedNs) rather than the plain LastMeaningfulOutput/LastAcknowledged fields, so this method is safe to call from any goroutine — including outside the actor's serialized command closures (e.g. review_queue_determiner.go's Determine(), called directly on the live *Instance from ReviewQueuePoller's own independent background goroutine).

func (*ReviewState) IsInProcessingGracePeriod

func (rs *ReviewState) IsInProcessingGracePeriod() bool

IsInProcessingGracePeriod returns true if the session is within its processing grace window. Caller must hold the relevant mutex if concurrent access is possible.

func (*ReviewState) SyncAtomicTimestamps added in v1.35.0

func (rs *ReviewState) SyncAtomicTimestamps()

SyncAtomicTimestamps initialises atomic shadow fields from their time.Time counterparts. Must be called once after constructing ReviewState from persisted or restored data so that lock-free readers see the correct initial value immediately.

func (*ReviewState) TimeSinceLastMeaningfulOutput

func (rs *ReviewState) TimeSinceLastMeaningfulOutput(createdAt time.Time) time.Duration

TimeSinceLastMeaningfulOutput returns how long ago meaningful terminal output was received. If no meaningful output has been recorded yet, returns the duration since the given createdAt time.

Lock-free: reads the atomic shadow (lastMeaningfulOutputNs) via loadLastMeaningfulOutputNs() rather than the plain LastMeaningfulOutput field, so this method is safe to call from any goroutine — including outside the actor's serialized command closures (e.g. HibernationSweeper.sweep(), which runs on its own independent background goroutine).

func (*ReviewState) TimeSinceLastTerminalUpdate

func (rs *ReviewState) TimeSinceLastTerminalUpdate(createdAt time.Time) time.Duration

TimeSinceLastTerminalUpdate returns how long ago any terminal output was received. If LastTerminalUpdate is zero, returns the duration since the given createdAt time. Caller must hold the relevant mutex if concurrent access is possible.

func (*ReviewState) UpdateTimestamps

func (rs *ReviewState) UpdateTimestamps(rawContent, filteredContent string, shouldUpdateMeaningful bool, sessionTitle string) bool

UpdateTimestamps updates terminal activity timestamps based on processed content.

  • rawContent: original captured output, used for the LastTerminalUpdate non-blank check.
  • filteredContent: rawContent with tmux banners stripped, used for signature computation.
  • shouldUpdateMeaningful: true when the content carries meaningful signal (not just banners).
  • sessionTitle: used only for structured debug logging.

Caller must be running inside the actor's serialized command closure (via i.send()/ sendSyncErr()), not holding Instance.mu - see UpdateTerminalTimestamps in instance_approval.go, the sole caller. Returns true when any field was updated (caller should rebuild the snapshot).

func (*ReviewState) UserRespondedAfterPrompt

func (rs *ReviewState) UserRespondedAfterPrompt() bool

UserRespondedAfterPrompt returns true if the user responded (LastUserResponse) after a prompt was detected (LastPromptDetected), indicating the session is no longer waiting. Caller must hold the relevant mutex if concurrent access is possible.

type ReviewVerdictData added in v1.35.0

type ReviewVerdictData struct {
	ItemSessionID  string
	OverallOutcome ReviewOutcome
	PerCriterion   string // JSON
	Summary        string
	DiffHash       string
	PromptHash     string
	DiffTokenCount int
	DiffTruncated  bool
	OverrideBy     string
	OverrideReason string
	OverrideAt     *time.Time
}

ReviewVerdictData is the input data for saving a ReviewVerdict.

type ReviewVerdictSummary added in v1.37.0

type ReviewVerdictSummary struct {
	ID             string
	OverallOutcome string
	PerCriterion   string // JSON []CriterionVerdict
	Summary        string
	DiffHash       string
	DiffTokenCount int
	DiffTruncated  bool
	OverrideBy     string
	OverrideReason string
	OverrideAt     *time.Time
	CreatedAt      time.Time
}

ReviewVerdictSummary is a domain DTO for a review verdict embedded in ItemSessionSummary.

type RevisionTarget

type RevisionTarget struct {
	ID          string
	ShortID     string
	Description string
	Author      string
	Timestamp   time.Time
	IsCurrent   bool
}

RevisionTarget represents a revision as a switch target

type ReworkBlockStaleResolver added in v1.41.0

type ReworkBlockStaleResolver interface {
	// ResolveReworkBlockedStaleIfRecovered no-ops (nil error) if the item
	// still has an open, still-stale blocking work session. Best-effort: a
	// storage error is logged by the caller, never returned to the reconcile
	// tick as a hard failure.
	ResolveReworkBlockedStaleIfRecovered(ctx context.Context, itemID string) error
}

ReworkBlockStaleResolver re-checks whether a review-status item's open StuckReasonReworkBlockedStale row (server/services/backlog_service_triage.go's notifyIfActiveWorkSessionStale) should resolve — because the blocking work session has produced output again, has ended, or the item has left review — and clears the row via storage.ResolveStuck if so. Implemented outside this package (BacklogService owns the live SessionStopper needed to re-check liveness/staleness) and wired via SetReworkBlockStaleResolver, mirroring StaleWorkRemediator/SetStaleWorkRemediator exactly: session-package orchestration (reconcileReworkBlockedStaleResolution) needs a server/services-layer, liveness-aware action, and this narrow interface — not a direct sessionStopper-shaped dependency added to BacklogLifecycleListener — is this codebase's established pattern for that. No automated remediation counterpart exists for this reason (unlike StaleWorkRemediator's RemediateStaleWorkSession) — see StuckReasonReworkBlockedStale's doc comment (session/domain/backlog.go) for why that's intentional, not a gap.

type Session

type Session struct {
	// Identity
	ID        string    `json:"id"`
	Title     string    `json:"title"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`

	// Process
	Status  Status `json:"status"`
	Program string `json:"program"`

	// Configuration
	AutoYes bool   `json:"auto_yes,omitempty"`
	Prompt  string `json:"prompt,omitempty"`

	// Optional contexts (nil = not loaded or not applicable)
	Git        *GitContext        `json:"git,omitempty"`
	Filesystem *FilesystemContext `json:"filesystem,omitempty"`
	Terminal   *TerminalContext   `json:"terminal,omitempty"`
	UI         *UIPreferences     `json:"ui,omitempty"`
	Activity   *ActivityTracking  `json:"activity,omitempty"`
	Cloud      *CloudContext      `json:"cloud,omitempty"`
}

Session represents the core domain entity for an AI agent session. It contains only universally required fields, with optional contexts for deployment-specific functionality.

Context types are defined in contexts.go: - GitContext: Git repository, branch, PR integration - FilesystemContext: Paths, working directories, worktree detection - TerminalContext: Terminal dimensions, tmux configuration - UIPreferences: Categories, tags, display preferences - ActivityTracking: Timestamps, output signatures, queue tracking - CloudContext: Cloud provider, API configuration

func InstanceToSession

func InstanceToSession(i *Instance) *Session

InstanceToSession converts a legacy Instance to the new Session type. This adapter enables gradual migration while maintaining backward compatibility. It populates all relevant contexts from the Instance fields.

func NewSession

func NewSession(title, program string) *Session

NewSession creates a new Session with the required fields. Optional contexts can be added using the With* methods.

func (*Session) GetBranch

func (s *Session) GetBranch() string

GetBranch returns the Git branch name, or empty string if no Git context.

func (*Session) GetCategory

func (s *Session) GetCategory() string

GetCategory returns the UI category, or empty string if no UI preferences.

func (*Session) GetLastMeaningfulOutput

func (s *Session) GetLastMeaningfulOutput() time.Time

GetLastMeaningfulOutput returns when the session had meaningful output, or zero time if no activity tracking.

func (*Session) GetLastViewed

func (s *Session) GetLastViewed() time.Time

GetLastViewed returns when the session was last viewed, or zero time if no activity tracking.

func (*Session) GetPath

func (s *Session) GetPath() string

GetPath returns the filesystem project path, or empty string if no filesystem context.

func (*Session) GetTags

func (s *Session) GetTags() []string

GetTags returns the UI tags, or empty slice if no UI preferences.

func (*Session) GetTerminalDimensions

func (s *Session) GetTerminalDimensions() (width, height int)

GetTerminalDimensions returns the terminal width and height, or 0,0 if no terminal context.

func (*Session) GetTmuxSessionName

func (s *Session) GetTmuxSessionName() string

GetTmuxSessionName returns the tmux session name, or empty string if no terminal context.

func (*Session) GetWorkingDir

func (s *Session) GetWorkingDir() string

GetWorkingDir returns the working directory, or empty string if no filesystem context.

func (*Session) HasActivityTracking

func (s *Session) HasActivityTracking() bool

HasActivityTracking returns true if activity tracking is available.

func (*Session) HasCloudContext

func (s *Session) HasCloudContext() bool

HasCloudContext returns true if cloud context is available.

func (*Session) HasFilesystemContext

func (s *Session) HasFilesystemContext() bool

HasFilesystemContext returns true if filesystem context is available.

func (*Session) HasGitContext

func (s *Session) HasGitContext() bool

HasGitContext returns true if Git context is available.

func (*Session) HasTerminalContext

func (s *Session) HasTerminalContext() bool

HasTerminalContext returns true if terminal context is available.

func (*Session) HasUIPreferences

func (s *Session) HasUIPreferences() bool

HasUIPreferences returns true if UI preferences are available.

func (*Session) IsCloudConfigured

func (s *Session) IsCloudConfigured() bool

IsCloudConfigured returns true if the cloud context is properly configured.

func (*Session) NeedsReviewQueueAttention

func (s *Session) NeedsReviewQueueAttention() bool

NeedsReviewQueueAttention returns true if session has unacknowledged output.

func (*Session) WithActivityTracking

func (s *Session) WithActivityTracking(activity *ActivityTracking) *Session

WithActivityTracking adds activity tracking to the session.

func (*Session) WithCloudContext

func (s *Session) WithCloudContext(cloud *CloudContext) *Session

WithCloudContext adds cloud context to the session.

func (*Session) WithFilesystemContext

func (s *Session) WithFilesystemContext(fs *FilesystemContext) *Session

WithFilesystemContext adds filesystem context to the session.

func (*Session) WithGitContext

func (s *Session) WithGitContext(git *GitContext) *Session

WithGitContext adds Git context to the session.

func (*Session) WithTerminalContext

func (s *Session) WithTerminalContext(terminal *TerminalContext) *Session

WithTerminalContext adds terminal context to the session.

func (*Session) WithUIPreferences

func (s *Session) WithUIPreferences(ui *UIPreferences) *Session

WithUIPreferences adds UI preferences to the session.

type SessionArchiver added in v1.39.0

type SessionArchiver interface {
	// ArchiveSessionByUUID soft-archives the session, if found and not already
	// archived. No-op (not an error) if the session is not tracked.
	ArchiveSessionByUUID(ctx context.Context, sessionUUID string) error
	// KillTmuxPaneOnly closes the session's live tmux pane, if any, leaving its
	// worktree intact (worktree cleanup is handled separately — see
	// cleanupItemWorktreesExcept). No-op if the session isn't tracked live.
	// Without this, ArchiveSessionByUUID alone only hides a terminal item's work
	// session from the default list — the underlying tmux/claude process keeps
	// running indefinitely, accumulating memory across every completed backlog
	// item (root cause of the 2026-07-29 OOM: dozens of `done` items' work
	// sessions still live, each with its own MCP server subprocess fleet).
	KillTmuxPaneOnly(ctx context.Context, sessionUUID string) error
}

SessionArchiver soft-archives a session by UUID so it stops accumulating in the default session list, and can also kill its live tmux pane. Implemented by server/services.SessionService (it owns the live in-memory Instance registry both operations must go through — see ArchivedAt's doc comment on session.Instance); wired via SetSessionArchiver from server/dependencies.go, same pattern as SetNotifier/SetSessionCreator below. Used by the archive_terminal_sessions detector in ReconcileStuck as a periodic safety net for work sessions belonging to backlog items that reached done/archived without their sessions being archived/stopped by the (also newly added) transition hook — e.g. pre-existing terminal items from before this detector existed, or a race/crash mid-transition. Nil-safe: the detector no-ops when unset.

type SessionGoalData added in v1.35.0

type SessionGoalData struct {
	UUID        string     `json:"uuid"`
	SessionUUID string     `json:"session_uuid"`
	Goal        string     `json:"goal"`
	Status      string     `json:"status"`
	Tasks       []TaskNode `json:"tasks,omitempty"`
	SetBy       string     `json:"set_by,omitempty"`
	UpdatedAt   time.Time  `json:"updated_at"`
}

SessionGoalData holds the goal state for a session, including the task tree.

func (*SessionGoalData) TasksDone added in v1.35.0

func (g *SessionGoalData) TasksDone() int

TasksDone returns the count of all tasks with status "done" (including nested children).

func (*SessionGoalData) TasksTotal added in v1.35.0

func (g *SessionGoalData) TasksTotal() int

TasksTotal returns the total count of all tasks (including nested children) in the goal.

type SessionHealthChecker

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

SessionHealthChecker manages session health validation and recovery

func NewSessionHealthChecker

func NewSessionHealthChecker(storage *Storage) *SessionHealthChecker

NewSessionHealthChecker creates a new session health checker

func (*SessionHealthChecker) CheckAllSessions

func (h *SessionHealthChecker) CheckAllSessions() ([]HealthCheckResult, error)

CheckAllSessions performs a health check on all active sessions

func (*SessionHealthChecker) RecoverUnhealthySessions

func (h *SessionHealthChecker) RecoverUnhealthySessions() error

RecoverUnhealthySessions attempts to recover all unhealthy sessions

func (*SessionHealthChecker) ScheduledHealthCheck

func (h *SessionHealthChecker) ScheduledHealthCheck(interval time.Duration, stopChan <-chan struct{})

ScheduledHealthCheck runs health checks at regular intervals

type SessionSummaryGenerator added in v1.41.0

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

SessionSummaryGenerator is the domain-level orchestrator that owns the headless pool, the ent client, and the FR-7 in-process dedup map. GenerateAndPersist runs the full async pipeline (assemble deterministic snapshots, call the LLM for a narrative, render markdown, persist) and satisfies the summaryGenerator interface consumed by sessionSummaryListener structurally — no explicit "implements" declaration needed.

func NewSessionSummaryGenerator added in v1.41.0

func NewSessionSummaryGenerator(entClient *ent.Client, pool headless.PoolClient, notifLister NotificationDecisionLister, tokenStore tokens.TokenStoreReader, reviewLookup ReviewQueueLookup) *SessionSummaryGenerator

NewSessionSummaryGenerator creates a SessionSummaryGenerator. notifLister and reviewLookup may be nil in a degraded/partial deployment — BuildDecisionsSnapshot nil-checks both.

func (*SessionSummaryGenerator) FindRowBySessionID added in v1.41.0

func (g *SessionSummaryGenerator) FindRowBySessionID(ctx context.Context, sessionID string) (*ent.SessionSummary, error)

FindRowBySessionID queries the SessionSummary row for sessionID directly — never via the Session-keyed live-instance machinery (AC-3: a summary must remain retrievable after its Session row is gone). Wraps ent's not-found error as ErrNotFound so callers in server/services (which must not import session/ent's error-handling helpers directly — see .golangci.yml's no_ent_in_services/forbidigo rules) can check it with errors.Is instead.

func (*SessionSummaryGenerator) GenerateAndPersist added in v1.41.0

func (g *SessionSummaryGenerator) GenerateAndPersist(ctx context.Context, sessionUUID, sessionTitle string, createdAt time.Time, diff DiffSnapshot, diffContent string, sessionGoal *SessionGoalData, reason string)

GenerateAndPersist runs the full session-completion-summary pipeline: build deterministic snapshots, generate (or skip/fallback) a narrative, render markdown, and persist via a single final status-transitioning upsert. Always invoked as a detached goroutine (`go g.GenerateAndPersist(...)`) by sessionSummaryListener or (Phase 2) RegenerateSessionSummary — never call this synchronously.

diff/diffContent/sessionGoal are synchronous, in-memory-only reads captured by the caller at dispatch time (no I/O) — see sessionSummaryListener.OnLifecycleEvent. diff is the already-derived DiffSnapshot (callers that have a live *git.DiffStats build it via BuildDiffSnapshot; RegenerateSessionSummary's no-live-instance fallback builds it directly from the persisted row's diff_* columns, since BuildDiffSnapshot's FilesChanged derivation depends on diff Content, which isn't persisted). diffContent is the raw diff text forwarded into the LLM narrative prompt — empty when unavailable (e.g. the no-live-instance fallback).

func (*SessionSummaryGenerator) ReconcileStaleness added in v1.41.0

func (g *SessionSummaryGenerator) ReconcileStaleness(ctx context.Context, row *ent.SessionSummary) *ent.SessionSummary

ReconcileStaleness flips a row stuck in GENERATING for longer than staleGenerationTimeout to ERROR, unless this process's in-memory guard is still held for that session (a long-running call, not a genuinely stuck row). Called from the RPC read path (Phase 2's GetSessionSummary), not a background sweep — see plan.md's Pattern Decisions "FR-7 restart-survival dedup" row for the accepted v1 gap (a never-revisited session stays stuck in GENERATING forever). Exported so server/services.SessionSummaryService (a different package) can call it directly from the RPC handler.

func (*SessionSummaryGenerator) SetNotificationLister added in v1.41.0

func (g *SessionSummaryGenerator) SetNotificationLister(l NotificationDecisionLister)

SetNotificationLister wires notifLister after construction. Needed because server/dependencies.go constructs SessionSummaryGenerator early (alongside the headless pool, so it can be wired to every instance in the same loop that wires backlogLifecycleListener) but the NotificationHistoryStore it needs isn't built until later, in server.go's RunServer — the same "Set* called long after construction" ordering constraint documented on SessionService.SetHeadlessPool. Safe to call with nil; BuildDecisionsSnapshot nil-checks notifLister. Guarded by lateBindMu because GenerateAndPersist (always dispatched via a goroutine) reads notifLister concurrently with this call.

func (*SessionSummaryGenerator) SetTokenStore added in v1.41.0

func (g *SessionSummaryGenerator) SetTokenStore(t tokens.TokenStoreReader)

SetTokenStore wires tokenStore after construction, for the same reason and timing as SetNotificationLister — the token store is also constructed after SessionSummaryGenerator during server startup. Safe to call with nil; BuildCostSnapshot nil-checks tokenStore. Guarded by lateBindMu because GenerateAndPersist (always dispatched via a goroutine) reads tokenStore concurrently with this call.

type SessionSummaryStatus added in v1.41.0

type SessionSummaryStatus string

SessionSummaryStatus is the lifecycle status of a SessionSummary row.

const (
	// SessionSummaryStatusPending means a row exists but generation has not started.
	SessionSummaryStatusPending SessionSummaryStatus = "pending"
	// SessionSummaryStatusGenerating means the async pipeline is actively running.
	SessionSummaryStatusGenerating SessionSummaryStatus = "generating"
	// SessionSummaryStatusReady means generation completed successfully.
	SessionSummaryStatusReady SessionSummaryStatus = "ready"
	// SessionSummaryStatusError means generation failed at some stage.
	SessionSummaryStatusError SessionSummaryStatus = "error"
)

func (SessionSummaryStatus) IsValid added in v1.41.0

func (s SessionSummaryStatus) IsValid() bool

IsValid reports whether s is a recognized SessionSummaryStatus.

type SessionType

type SessionType = config.SessionType

SessionType is an alias for config.SessionType so callers can use either package.

type Shell added in v1.35.0

type Shell struct {
	// ID is the stable UUID for this shell. Also the fragment used in the tmux session name.
	ID string
	// Name is the user-visible label for the shell tab.
	Name string
	// Command is the command running in the shell (e.g. "bash", "python").
	Command string
	// WorkingDir is the working directory for the shell process.
	WorkingDir string
	// TmuxSessionName is the full computed tmux session name:
	// "{parentPrefix}_shell_{shellID}"
	TmuxSessionName string
	// Status is the current lifecycle status.
	Status ShellStatus
	// ExitCode is the process exit code (meaningful when Status != ShellStatusRunning).
	ExitCode int
	// OrderIndex controls tab display order.
	OrderIndex int
	// StartedAt is when the shell was spawned.
	StartedAt time.Time
	// contains filtered or unexported fields
}

Shell represents a custom shell attached to a session. It is the in-memory projection of the ent Shell entity; changes are written back through the repository.

type ShellData added in v1.35.0

type ShellData struct {
	// ID is the UUID for the shell. If empty, the caller must populate it.
	ID string
	// Name is the user-visible label.
	Name string
	// Command is the command to run in the shell.
	Command string
	// WorkingDir is the starting directory for the shell process.
	WorkingDir string
	// TmuxSessionName is the full sibling tmux session name.
	TmuxSessionName string
	// OrderIndex is the display order for the tab.
	OrderIndex int
}

ShellData carries the input fields for creating a new shell.

type ShellHandle added in v1.35.0

type ShellHandle interface {
	// GetPTY returns the PTY file for reading terminal output.
	// Returns ErrShellStopped if the shell has been closed.
	GetPTY() (*os.File, error)
	// Resize updates the PTY dimensions.
	Resize(cols, rows int) error
	// Close stops the shell process and releases resources.
	Close() error
}

ShellHandle is the interface for managing a single shell PTY. It is implemented by session/tmux.ShellTmuxHandle for real sessions and can be mocked in tests.

type ShellRegistry added in v1.35.0

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

ShellRegistry is a concurrent shell+handle store. Zero value is not usable; create with newShellRegistry(). The exported API intentionally has no Lock/Unlock methods — mutations go through named operations that hold the bucket lock only for fast in-memory work.

func (*ShellRegistry) Add added in v1.35.0

func (r *ShellRegistry) Add(sh *Shell, handle *tmux.ShellTmuxHandle)

Add stores a new shell+handle pair. Overwrites any existing entry for the same ID.

func (*ShellRegistry) AddStopped added in v1.35.0

func (r *ShellRegistry) AddStopped(sh *Shell)

AddStopped stores a shell that has no live handle (already stopped or error).

func (*ShellRegistry) Get added in v1.35.0

func (r *ShellRegistry) Get(shellID string) (*Shell, bool)

Get returns the Shell for shellID, or (nil, false) if absent.

func (*ShellRegistry) GetBoth added in v1.35.0

func (r *ShellRegistry) GetBoth(shellID string) (*Shell, *tmux.ShellTmuxHandle, bool)

GetBoth returns both shell and handle in one atomic load.

func (*ShellRegistry) GetHandle added in v1.35.0

func (r *ShellRegistry) GetHandle(shellID string) (*tmux.ShellTmuxHandle, bool)

GetHandle returns the ShellTmuxHandle for shellID, or (nil, false) if absent.

func (*ShellRegistry) Len added in v1.35.0

func (r *ShellRegistry) Len() int

Len returns the number of shells in the registry.

func (*ShellRegistry) List added in v1.35.0

func (r *ShellRegistry) List() []*Shell

List returns all shells sorted by OrderIndex.

func (*ShellRegistry) Remove added in v1.35.0

func (r *ShellRegistry) Remove(shellID string)

Remove deletes the entry for shellID. No-op if not present.

func (*ShellRegistry) SetHandle added in v1.35.0

func (r *ShellRegistry) SetHandle(shellID string, handle *tmux.ShellTmuxHandle)

SetHandle atomically replaces the handle for shellID without changing the shell.

func (*ShellRegistry) UpdateForRestart added in v1.35.0

func (r *ShellRegistry) UpdateForRestart(shellID string, newHandle *tmux.ShellTmuxHandle, newSessionName string, exitCh, watcherDone chan struct{})

UpdateForRestart atomically replaces a shell's mutable restart fields with new values (Status=Running, ExitCode=0, new TmuxSessionName, new exitCh/watcherDone). If the shellID is not found it stores a brand-new entry built from newShell.

func (*ShellRegistry) UpdateStatus added in v1.35.0

func (r *ShellRegistry) UpdateStatus(shellID string, status ShellStatus, exitCode *int) bool

UpdateStatus atomically updates Shell.Status and Shell.ExitCode for shellID. Returns true if the entry was found and updated.

type ShellRepository added in v1.35.0

type ShellRepository interface {
	// CreateShell persists a new shell record under the given session title.
	CreateShell(ctx context.Context, sessionTitle string, data ShellData) (*ent.Shell, error)
	// ListShells returns all shell records for the given session title, ordered by order_index.
	ListShells(ctx context.Context, sessionTitle string) ([]*ent.Shell, error)
	// UpdateShellStatus sets the status (and optionally exit code) for the shell with the given ID.
	UpdateShellStatus(ctx context.Context, shellID, status string, exitCode *int) error
	// DeleteShell removes the shell record with the given ID.
	DeleteShell(ctx context.Context, shellID string) error
}

ShellRepository is the minimal persistence interface for per-session shell management. It is implemented by EntRepository; pass nil to disable persistence (e.g., tests).

type ShellStatus added in v1.35.0

type ShellStatus string

ShellStatus represents the lifecycle status of a custom shell.

const (
	// ShellStatusRunning means the shell process is alive and the PTY is open.
	ShellStatusRunning ShellStatus = "running"
	// ShellStatusStopped means the shell exited cleanly (via exit command or StopShell).
	ShellStatusStopped ShellStatus = "stopped"
	// ShellStatusError means the shell exited with a non-zero status unexpectedly.
	ShellStatusError ShellStatus = "error"
)

type SourceSyncEventData added in v1.37.0

type SourceSyncEventData struct {
	ID           string
	ItemsCreated int
	ItemsUpdated int
	ItemsSkipped int
	ItemsErrored int
	ErrorMessage string
	CursorAfter  string
	StartedAt    time.Time
	FinishedAt   *time.Time
}

SourceSyncEventData is the domain DTO replacing *ent.SourceSyncEvent in Storage returns.

type SpawnShellRequest added in v1.35.0

type SpawnShellRequest struct {
	// Name is the optional user-visible label. Defaults to the command base name.
	Name string
	// Command is the command to run. Defaults to $SHELL or /bin/sh.
	Command string
	// WorkingDir is the starting directory. Defaults to the session's WorkingDir.
	WorkingDir string
}

SpawnShellRequest carries the parameters for Instance.SpawnShell.

type StaleWorkRemediator added in v1.39.0

type StaleWorkRemediator interface {
	// RemediateStaleWorkSession ends the item's current stale work session
	// (killing its tmux pane but keeping the worktree so uncommitted work
	// survives) and spawns a fresh one with a new turn budget. No-op (nil
	// error) if the item already moved off in_progress or its work session
	// already ended by the time this runs.
	RemediateStaleWorkSession(ctx context.Context, itemID string) error
}

StaleWorkRemediator can clean up and respawn an in_progress backlog item whose active work session has gone stale (StuckReasonStaleWork — no progress reported for over maxWorkSessionStaleness) but is NOT a zombie: the underlying tmux session and pane process are still alive (Instance.TmuxAlive/PaneProcessDead), so the generic tmux health check never flags it — the agent inside simply finished its own work and is idle at an interactive prompt instead of properly closing out. Before this existed, reconcileStaleWorkSessions was detection-only (MarkStuck + notify), so such an item sat "in_progress" forever once the agent went idle (docs/tasks/backlog-stuck-item-auto-remediation.md Phase B; live repro 2026-07-20, item 9264efe7-b4c2-455a-9e2a-ab0196a63ecd, rework suffix -r14). Implemented outside this package (BacklogService owns the live Instance registry needed to kill the stale tmux pane) and wired via SetStaleWorkRemediator, same pattern as AutoReopenSpawner/PRFixSpawner.

type StartupScanner added in v1.35.0

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

StartupScanner scans running sessions for pre-existing approval prompts and adds matches to the review queue immediately, before the first regular poll cycle.

func NewStartupScanner added in v1.35.0

func NewStartupScanner(statusManager StatusProvider, contentProvider ContentProvider) *StartupScanner

NewStartupScanner creates a StartupScanner using the provided status and content providers.

func (*StartupScanner) Scan added in v1.35.0

func (ss *StartupScanner) Scan(instances []*Instance, queue ReviewQueueWriter) int

Scan iterates over instances and adds any that need attention to the queue. Returns the number of sessions added to the queue.

type Status

type Status int
const (
	// Creating is the status when the instance is being initialized.
	Creating Status = 0
	// Active is the status when the instance has a live AI process (running or ready).
	Active Status = 1
	// Paused is if the instance is paused (worktree removed but branch preserved).
	Paused Status = 2
	// Stopped is a terminal state: the instance has been shut down and cannot transition further.
	Stopped Status = 3
	// Hibernated is the status when the instance has been checkpointed and the tmux session killed.
	Hibernated Status = 4
	// Restoring is the transient startup state when a hibernated session is being restored.
	// Never persisted to the database — transitions to Active or Creating on completion.
	Restoring Status = 5
	// Crashed is a terminal state distinct from Stopped: the wrapped program exited
	// abnormally (non-zero exit code or signal) and tmux's remain-on-exit left a dead
	// pane placeholder, detected by SessionHealthChecker's polling (see session/health.go).
	// Unlike Stopped, a Crashed session is not auto-recovered by the health checker —
	// it surfaces to the user/automation for an explicit resume (see ExitReason).
	Crashed Status = 6

	// Deprecated: use Active.
	Running = Active
	// Deprecated: use Active.
	Ready = Active
	// Deprecated: use Creating.
	Loading = Creating
)

func StatusFromDetected

func StatusFromDetected(detected detection.DetectedStatus) Status

StatusFromDetected maps a DetectedStatus to the corresponding lifecycle Status. All detected states map to Active because the instance process is still executing. NeedsApproval, InputRequired, Error, and TestsFailing are sub-status signals surfaced via GetEffectiveStatus() — they do not change the lifecycle state.

func (Status) String

func (s Status) String() string

String returns a human-readable name for the status.

type StatusChange

type StatusChange struct {
	Timestamp time.Time
	Status    detection.DetectedStatus
	Context   string
}

StatusChange represents a change in detected status during execution.

type StatusChangeListener added in v1.35.0

type StatusChangeListener func(newStatus detection.DetectedStatus, sessionName string)

StatusChangeListener is called when the controller detects a terminal status transition. Always invoked from the controller's own background goroutine, outside any lock.

type StatusDeterminer added in v1.35.0

type StatusDeterminer interface {
	Determine(
		inst *Instance,
		content string,
		statusInfo InstanceStatusInfo,
		detector detection.TerminalDetector,
	) DetectionResult
}

StatusDeterminer evaluates whether a session should be added to, removed from, or left unchanged in the review queue. It is a pure function — no queue operations.

type StatusProvider added in v1.35.0

type StatusProvider interface {
	GetStatus(inst *Instance) InstanceStatusInfo
	GetController(instanceTitle string) (*ClaudeController, bool)
}

StatusProvider is the interface ReviewQueuePoller uses to fetch session status. Defined at the consumption point (the poller), not the production point.

type Storage

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

Storage handles saving and loading instances via the repository backend.

func NewStorageWithRepository

func NewStorageWithRepository(repo Repository) (*Storage, error)

NewStorageWithRepository creates a Storage backed by a Repository.

func (*Storage) AddBacklogItemDependency added in v1.43.0

func (s *Storage) AddBacklogItemDependency(ctx context.Context, edge BacklogItemDependencyEdge) error

AddBacklogItemDependency records a blocker/blocked dependency edge.

func (*Storage) AddInstance

func (s *Storage) AddInstance(instance *Instance) error

AddInstance adds a new instance to storage. Unlike SaveInstances, this does not require instance.Started() to be true.

func (*Storage) AllRules added in v1.12.0

func (s *Storage) AllRules(ctx context.Context) ([]ApprovalRuleData, error)

AllRules returns all auto-approval rules from the repository.

func (*Storage) AppendProgressNote added in v1.38.0

func (s *Storage) AppendProgressNote(ctx context.Context, itemID string, criterionIndex int, note, status string) error

AppendProgressNote records a single report_progress call as an immutable history entry, in addition to the current-note-per-criterion updated by UpdateAcCriterionStatus.

func (*Storage) ArchiveBacklogItem added in v1.35.0

func (s *Storage) ArchiveBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)

ArchiveBacklogItem sets the archived_at timestamp.

func (*Storage) AssignSessionsToProject added in v1.23.0

func (s *Storage) AssignSessionsToProject(ctx context.Context, projectName string, sessionTitles []string) error

AssignSessionsToProject links sessions to a project in storage.

func (*Storage) BulkResetStuckRemediation added in v1.39.0

func (s *Storage) BulkResetStuckRemediation(ctx context.Context, reason *domain.StuckReason, onlyParked bool) (int, error)

BulkResetStuckRemediation is a thin passthrough to *EntRepository, same rationale as RecordRemediationAttempt above.

func (*Storage) Close

func (s *Storage) Close() error

Close performs graceful shutdown of storage.

func (*Storage) ComputeCurrentDiffHash added in v1.42.0

func (s *Storage) ComputeCurrentDiffHash(ctx context.Context, itemID string) string

ComputeCurrentDiffHash resolves itemID's most recent completed work session's base..head commit range (via GetRepoPathAndLatestCompletedWorkSessionCommits — two bounded, no-edge queries, not GetBacklogItem/ListItemSessions' unbounded eager-loaded fetch) and returns a content hash of that diff (git.DiffHashBetween), for stamping onto a review verdict's DiffHash at save time — see stuck_decisions.go's IsFlakyVerdictFlipFlop, which the hash feeds.

Best-effort: any resolution failure (item/session lookup, missing SHAs, a git error) returns "" rather than propagating an error, matching this codebase's "best-effort, never blocks the write it's attached to" convention (see e.g. SaveReviewVerdict's publish-hook comments) — a missing DiffHash just means IsFlakyVerdictFlipFlop treats that verdict as unknown, never as a false match.

func (*Storage) CreateBacklogItem added in v1.35.0

func (s *Storage) CreateBacklogItem(ctx context.Context, data BacklogItemData) (*BacklogItemData, error)

CreateBacklogItem inserts a new backlog item.

func (*Storage) CreateItemSession added in v1.35.0

func (s *Storage) CreateItemSession(ctx context.Context, data ItemSessionData) (ItemSessionSummary, error)

CreateItemSession creates a new ItemSession linked to a BacklogItem.

func (*Storage) CreateItemSessionWithVerdict added in v1.35.0

func (s *Storage) CreateItemSessionWithVerdict(ctx context.Context, isData ItemSessionData, verdict ReviewVerdictData) (ItemSessionSummary, error)

CreateItemSessionWithVerdict atomically creates an ItemSession and its initial ReviewVerdict in a single transaction. Falls back gracefully if the backend is not ent-based.

func (*Storage) CreateItemSource added in v1.35.0

func (s *Storage) CreateItemSource(ctx context.Context, data ItemSourceData) (*ItemSourceData, error)

CreateItemSource registers a new external item source.

func (*Storage) CreateProject added in v1.23.0

func (s *Storage) CreateProject(ctx context.Context, data ProjectData) (*ProjectData, error)

CreateProject inserts a new project into storage.

func (*Storage) CreateSourceSyncEvent added in v1.35.0

func (s *Storage) CreateSourceSyncEvent(ctx context.Context, sourceID, cursorAfter string, created, updated, skipped, errored int, errMsg string, startedAt, finishedAt time.Time) error

CreateSourceSyncEvent records a sync run for an item source. Direct EntRepository delegation, like ListSourceSyncEvents above.

func (*Storage) DeleteAllInstances

func (s *Storage) DeleteAllInstances() error

DeleteAllInstances removes all stored instances.

func (*Storage) DeleteBacklogItem added in v1.35.0

func (s *Storage) DeleteBacklogItem(ctx context.Context, id string) error

DeleteBacklogItem permanently removes an item and all its child records.

func (*Storage) DeleteInstance

func (s *Storage) DeleteInstance(title string) error

DeleteInstance removes an instance from storage.

func (*Storage) DeleteItemSource added in v1.35.0

func (s *Storage) DeleteItemSource(ctx context.Context, id string) error

DeleteItemSource removes an item source by UUID string.

func (*Storage) DeleteProject added in v1.23.0

func (s *Storage) DeleteProject(ctx context.Context, name string) error

DeleteProject removes a project from storage (sessions are unassigned).

func (*Storage) DeleteRule added in v1.12.0

func (s *Storage) DeleteRule(ctx context.Context, id string) error

DeleteRule removes an auto-approval rule from the repository.

func (*Storage) FindDoneItemsOlderThan added in v1.39.0

func (s *Storage) FindDoneItemsOlderThan(ctx context.Context, cutoff time.Time) ([]BacklogItemData, error)

FindDoneItemsOlderThan returns backlog items in "done" status whose most recent done-transition happened at/before cutoff. Thin passthrough to the ent-backed repository (same rationale as MarkStuck below) — returns nil, nil for backends that don't support it (e.g. an in-memory test double), never an error.

func (*Storage) FindInstanceDataByID added in v1.35.0

func (s *Storage) FindInstanceDataByID(id string) (*InstanceData, error)

FindInstanceDataByID finds the first InstanceData whose stable ID or title matches id. Returns ErrInstanceDataNotFound when no match exists.

func (*Storage) FindOpenStuckStates added in v1.38.0

func (s *Storage) FindOpenStuckStates(ctx context.Context) ([]OpenStuckStateData, error)

FindOpenStuckStates returns every open (unresolved, un-snoozed) BacklogStuckState row, joined with rendering-relevant item fields. Returns an empty slice (no error) when the backend does not support stuck-state queries (e.g. an in-memory test double).

func (*Storage) GetAllInstanceArtifacts added in v1.35.0

func (s *Storage) GetAllInstanceArtifacts() (map[string]string, error)

GetAllInstanceArtifacts returns a map of title → raw artifacts JSON for all sessions that have stored artifacts. Single bulk query (M-4 fix).

func (*Storage) GetAllItemSessionsWithBacklogInfo added in v1.37.0

func (s *Storage) GetAllItemSessionsWithBacklogInfo(ctx context.Context) ([]ItemSessionBacklogEntry, error)

GetAllItemSessionsWithBacklogInfo returns all item sessions joined with backlog item metadata. Delegates to EntRepository; returns an error for non-ent backends.

func (*Storage) GetBacklogItem added in v1.35.0

func (s *Storage) GetBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)

GetBacklogItem retrieves a backlog item by UUID string.

func (*Storage) GetBaseCommitSHAsForSessions added in v1.37.0

func (s *Storage) GetBaseCommitSHAsForSessions(ctx context.Context, uuids []string) (map[string]string, error)

GetBaseCommitSHAsForSessions returns a sessionUUID→base_commit_sha map for the given UUIDs.

func (*Storage) GetClaudeConversationUUIDBySessionUUID added in v1.37.0

func (s *Storage) GetClaudeConversationUUIDBySessionUUID(ctx context.Context, sessionUUID string) (string, error)

GetClaudeConversationUUIDBySessionUUID returns the Claude conversation UUID for the session whose title matches the given UUID. Returns "" when the session has no ClaudeSession, and ErrNotFound when no session matches.

func (*Storage) GetEntClient added in v1.35.0

func (s *Storage) GetEntClient() *ent.Client

GetEntClient returns the *ent.Client from the underlying EntRepository, or nil when the repository is not ent-backed (e.g. in-memory test doubles).

func (*Storage) GetInstanceArtifacts added in v1.35.0

func (s *Storage) GetInstanceArtifacts(title string) (string, error)

GetInstanceArtifacts loads the raw JSON-encoded artifact blob for a session. Returns ("", nil) if the session exists but has no artifacts yet.

func (*Storage) GetItemSession added in v1.35.0

func (s *Storage) GetItemSession(ctx context.Context, id string) (ItemSessionSummary, error)

GetItemSession looks up an ItemSession by entity UUID (loads BacklogItem edge).

func (*Storage) GetItemSessionBySessionAndItem added in v1.35.0

func (s *Storage) GetItemSessionBySessionAndItem(ctx context.Context, sessionUUID string, itemID string) (ItemSessionSummary, error)

GetItemSessionBySessionAndItem looks up an ItemSession by both sessionUUID and backlog item ID. Returns ErrNotFound if no matching record exists.

func (*Storage) GetItemSessionBySessionUUID added in v1.35.0

func (s *Storage) GetItemSessionBySessionUUID(ctx context.Context, sessionUUID string) (ItemSessionSummary, error)

GetItemSessionBySessionUUID looks up the ItemSession for a given session UUID (loads BacklogItem edge).

func (*Storage) GetItemSourceByID added in v1.41.0

func (s *Storage) GetItemSourceByID(ctx context.Context, id string) (*ItemSourceData, error)

GetItemSourceByID retrieves a single item source's domain data by UUID string. Used by the GitHub forward-sync EventBus subscriber (see server/services/backlog_github_forward_sync.go) to look up a backlog item's source (ForwardSyncEnabled, ForwardSyncCloseLabel, PluginID, Config) without needing an *EntRepository handle of its own.

func (*Storage) GetMostRecentReviewVerdictForItem added in v1.35.0

func (s *Storage) GetMostRecentReviewVerdictForItem(ctx context.Context, itemID string) (ReviewOutcome, error)

GetMostRecentReviewVerdictForItem returns the OverallOutcome of the most recent ReviewVerdict linked to any ItemSession for itemID. Returns "" when none exists.

func (*Storage) GetRecentReviewVerdictSummaries added in v1.39.0

func (s *Storage) GetRecentReviewVerdictSummaries(ctx context.Context, itemID string, limit int) ([]ReviewVerdictSummary, error)

GetRecentReviewVerdictSummaries returns up to limit ReviewVerdicts for itemID, most recent first. Returns nil (not an error) when the repo isn't ent-backed.

func (*Storage) GetSession

func (s *Storage) GetSession(ctx context.Context, title string, opts ContextOptions) (*Session, error)

GetSession retrieves a session by title using the Session domain model. Use ContextOptions presets (ContextMinimal, ContextUIView, etc.) to control what is loaded.

func (*Storage) GetSessionGoal added in v1.35.0

func (s *Storage) GetSessionGoal(ctx context.Context, sessionUUID string) (*SessionGoalData, error)

GetSessionGoal retrieves the goal for a session by session UUID. Returns ErrNotFound if no goal has been set for the session.

func (*Storage) GetSubcommandBreakdown added in v1.35.0

func (s *Storage) GetSubcommandBreakdown(ctx context.Context, program string, since time.Time) ([]SubcommandDecisionCount, error)

GetSubcommandBreakdown returns per-(subcommand, decision) counts for a program.

func (*Storage) GetSubcommandTrend added in v1.35.0

func (s *Storage) GetSubcommandTrend(ctx context.Context, program, subcommand string, since time.Time) ([]AnalyticsData, error)

GetSubcommandTrend returns raw analytics rows for (program, subcommand) since a time.

func (*Storage) GetWorktreeDataBySessionUUID added in v1.37.0

func (s *Storage) GetWorktreeDataBySessionUUID(ctx context.Context, sessionUUID string) (GitWorktreeData, error)

GetWorktreeDataBySessionUUID returns the git worktree data for the Session with the given UUID. Returns empty GitWorktreeData for directory-mode sessions or if the session is not found.

func (*Storage) ListAnalytics added in v1.12.0

func (s *Storage) ListAnalytics(ctx context.Context, limit int) ([]AnalyticsData, error)

ListAnalytics retrieves recent classification decisions from the repository.

func (*Storage) ListAnalyticsByProgramSince added in v1.35.0

func (s *Storage) ListAnalyticsByProgramSince(ctx context.Context, program string, since time.Time, limit int) ([]AnalyticsData, error)

ListAnalyticsByProgramSince retrieves entries for a specific program since a time.

func (*Storage) ListAnalyticsSince added in v1.35.0

func (s *Storage) ListAnalyticsSince(ctx context.Context, since time.Time, limit int) ([]AnalyticsData, error)

ListAnalyticsSince retrieves analytics entries with created_at >= since.

func (*Storage) ListBacklogItemSummaries added in v1.37.0

func (s *Storage) ListBacklogItemSummaries(ctx context.Context, filter BacklogItemFilter) ([]BacklogItemSummary, error)

ListBacklogItemSummaries returns lightweight summaries for list views.

func (*Storage) ListBacklogItems added in v1.35.0

func (s *Storage) ListBacklogItems(ctx context.Context, filter BacklogItemFilter) ([]BacklogItemData, error)

ListBacklogItems returns backlog items with optional filtering.

func (*Storage) ListInstanceData added in v1.18.0

func (s *Storage) ListInstanceData() ([]InstanceData, error)

ListInstanceData returns raw InstanceData from the repository without constructing Instance objects. This avoids the side effect of FromInstanceData() calling Start() (which spawns PTY processes). Use for read-only existence and title checks.

func (*Storage) ListInstanceDataWithWorktree added in v1.44.0

func (s *Storage) ListInstanceDataWithWorktree() ([]InstanceData, error)

ListInstanceDataWithWorktree returns raw InstanceData with the Worktree edge eager-loaded. Use this instead of ListInstanceData whenever a read-only pass needs Worktree.WorktreePath/ RepoPath/BranchName/BaseCommitSHA (e.g. to stat the worktree or check dirty status) — plain ListInstanceData uses LoadMinimal, which never populates Worktree, so any such field will silently read as its zero value under that call.

func (*Storage) ListInstanceIDs added in v1.35.0

func (s *Storage) ListInstanceIDs() ([]string, error)

ListInstanceIDs returns the stable ID (UUID if set, else Title) for every stored InstanceData. Used by Registry.AcquireAll to seed the initial live-handle set.

func (*Storage) ListItemSessions added in v1.35.0

func (s *Storage) ListItemSessions(ctx context.Context, itemID string) ([]ItemSessionSummary, error)

ListItemSessions returns all ItemSessions for a given BacklogItem UUID string.

func (*Storage) ListItemSources added in v1.35.0

func (s *Storage) ListItemSources(ctx context.Context) ([]ItemSourceData, error)

ListItemSources returns all registered item sources.

func (*Storage) ListProgressNotesForItem added in v1.38.0

func (s *Storage) ListProgressNotesForItem(ctx context.Context, itemID string) ([]ProgressNoteData, error)

ListProgressNotesForItem returns the full append-only history of report_progress calls for a backlog item, ordered by created_at ascending.

func (*Storage) ListProjects added in v1.23.0

func (s *Storage) ListProjects(ctx context.Context) ([]ProjectData, error)

ListProjects returns all projects from storage.

func (*Storage) ListRecentCommandsByProgram added in v1.35.0

func (s *Storage) ListRecentCommandsByProgram(ctx context.Context, program, subcommand string, since time.Time, n int) ([]string, error)

ListRecentCommandsByProgram returns the most recent n command_preview strings.

func (*Storage) ListSessionRecords added in v1.35.0

func (s *Storage) ListSessionRecords() []tokens.SessionRecord

ListSessionRecords returns a snapshot of all sessions as SessionRecords, for use by the tokens.Associator to match JSONL files to stapler-squad sessions.

func (*Storage) ListSessions

func (s *Storage) ListSessions(ctx context.Context, opts ContextOptions) ([]*Session, error)

ListSessions retrieves all sessions using the Session domain model. Use ContextOptions presets (ContextMinimal, ContextUIView, etc.) to control what is loaded.

func (*Storage) ListSourceSyncEvents added in v1.35.0

func (s *Storage) ListSourceSyncEvents(ctx context.Context, sourceID string) ([]SourceSyncEventData, bool, error)

ListSourceSyncEvents returns sync history events for an item source, most recent first. Direct EntRepository delegation, like GetItemSession below.

func (*Storage) ListWorkspacePeers added in v1.41.0

func (s *Storage) ListWorkspacePeers(ctx context.Context, workspaceKey string, excludeSessionUUID string) ([]WorkspacePeer, error)

ListWorkspacePeers returns other sessions sharing workspaceKey, excluding excludeSessionUUID. Returns an empty slice (not an error) when workspaceKey is empty or no peers exist. Goal-less peers are still returned (Goal is nil) since AC0 only requires "other active sessions", not "sessions with a goal set".

func (*Storage) LoadInstances

func (s *Storage) LoadInstances() ([]*Instance, error)

LoadInstances loads the list of instances from the repository.

func (*Storage) MarkStuck added in v1.38.0

func (s *Storage) MarkStuck(ctx context.Context, itemID string, reason domain.StuckReason, expectedStatus BacklogStatus, stuckContext string) (bool, error)

MarkStuck opens/refreshes/reopens a durable BacklogStuckState row for (itemID, reason). Thin passthrough so callers outside package session (e.g. server/services, which cannot reach the unexported repo field) can write stuck state. Returns false, nil when the backend does not support stuck-state writes — never an error for an unsupported backend.

func (*Storage) MarkStuckNotified added in v1.38.0

func (s *Storage) MarkStuckNotified(ctx context.Context, itemID string, reason domain.StuckReason) (bool, error)

MarkStuckNotified sets notified_at=now on an open, not-yet-notified stuck row for (itemID, reason). Thin passthrough, same rationale as MarkStuck above.

func (*Storage) RecordAnalytics added in v1.12.0

func (s *Storage) RecordAnalytics(ctx context.Context, data AnalyticsData) error

RecordAnalytics logs a classification decision to the repository.

func (*Storage) RecordManualRemediationAttempt added in v1.39.0

func (s *Storage) RecordManualRemediationAttempt(ctx context.Context, itemID string, reason domain.StuckReason) (justParked bool, err error)

RecordManualRemediationAttempt implements the operator-triggered "Retry now" path (TriggerRemediationNow RPC): it requires an already-open row for (itemID, reason) — mirroring SnoozeStuckItem's validation, there must be something currently stuck to retry — and rejects with ErrRemediationParked once the row already exhausted its attempt budget, rather than silently un-parking it (reset is a separate, explicit operator action). On success it records exactly the same accounting a normal dispatcher-triggered attempt would (this IS a real attempt, just operator- instead of timer-initiated), so it counts toward the same 5-attempt cap. Callers invoke the reason-specific remediation action themselves once this returns successfully — this method only owns the gate/accounting, not the action dispatch (which differs by caller: BacklogService methods directly for the RPC handler, vs the interfaces the periodic sweep uses).

func (*Storage) RecordRemediationAttempt added in v1.39.0

func (s *Storage) RecordRemediationAttempt(ctx context.Context, itemID string, reason domain.StuckReason, attempts int32, nextAt *time.Time) (bool, error)

RecordRemediationAttempt is a thin passthrough to *EntRepository, mirroring MarkStuck/ResolveStuck's rationale: callers outside package session cannot reach the unexported repo field. Returns false, nil (never an error) when the backend does not support stuck-state writes.

func (*Storage) RecordRemediationRestartGrace added in v1.39.0

func (s *Storage) RecordRemediationRestartGrace(ctx context.Context, itemID string, reason domain.StuckReason, bootTime time.Time) (bool, error)

RecordRemediationRestartGrace is a thin passthrough to *EntRepository, same rationale as RecordRemediationAttempt above.

func (*Storage) RecordSourceSyncFailure added in v1.41.0

func (s *Storage) RecordSourceSyncFailure(ctx context.Context, sourceID, message string) error

RecordSourceSyncFailure records a forward-sync failure (e.g. CloseIssue erroring) as a queryable sync-history row. Direct EntRepository delegation, like CreateSourceSyncEvent above.

func (*Storage) RemediationBlocked added in v1.41.0

func (s *Storage) RemediationBlocked(ctx context.Context, itemID string, reason domain.StuckReason) (blocked bool, err error)

RemediationBlocked is a read-only peek at whether reason's own remediation gate is currently closed (parked at the attempt cap, or mid-backoff) for itemID — unlike RemediationDue, it never mutates the row or consumes an attempt. Built for callers whose remediation action's entire value depends on a DIFFERENT reason's gate letting a downstream step through: producing a fresh review verdict is pointless if the reopen that verdict would trigger (autoReopenWithBackoffGate, gated on StuckReasonBouncing) is itself closed right now — the diff hasn't changed, so a respawn's outcome would be identical to the last one. Spending an attempt on a foregone conclusion wastes that budget for zero forward progress and, once it repeats enough times, silently parks the caller's OWN reason with a "use Reset to retry" notification that doesn't mention the real blocker (BUG-043) — the caller should check this FIRST and skip the attempt entirely when true, logging why, rather than spend it on a call that cannot help.

Returns false (not blocked) when no open row exists for (itemID, reason): nothing is gating yet, same "ungated until first detected" default RemediationDue documents.

func (*Storage) RemediationDue added in v1.39.0

func (s *Storage) RemediationDue(ctx context.Context, itemID string, reason domain.StuckReason) (due bool, justParked bool, err error)

RemediationDue is the shared backoff gate every automated remediation action — inside package session (BacklogLifecycleListener) or outside it (server/services' AutonomousOrchestrationService) — must call before invoking its reason-specific respawn action. It reports whether the caller should proceed, and atomically records the resulting accounting (a normal attempt, or a restart-grace pass) BEFORE returning true — so a caller that dispatches its actual action asynchronously (e.g. bounded by a semaphore, taking minutes) can never double-count across overlapping sweep ticks or concurrent event callbacks.

due=true, justParked=false: caller should invoke its action now. due=true, justParked=true: caller should invoke its action now AND send a one-time "auto-remediation exhausted, this was the last automated attempt" notification — this attempt is the one that pushed remediation_attempts to the cap. due=false: caller must not invoke its action this tick (parked or not yet due). Not an error.

Returns (true, false, nil) — ungated — when no open row exists for (itemID, reason) yet: the reason hasn't been detected as stuck at all, so there is nothing to gate against. This preserves today's behavior for the first review-failure/driver-turn-cap/etc. before the corresponding detector has had a chance to MarkStuck a row.

func (*Storage) ResetStuckRemediation added in v1.39.0

func (s *Storage) ResetStuckRemediation(ctx context.Context, itemID string, reason domain.StuckReason) (bool, error)

ResetStuckRemediation is a thin passthrough to *EntRepository, same rationale as RecordRemediationAttempt above.

func (*Storage) ResolveStuck added in v1.38.0

func (s *Storage) ResolveStuck(ctx context.Context, itemID string, reason domain.StuckReason) (bool, error)

ResolveStuck atomically, idempotently closes an open BacklogStuckState row for (itemID, reason). Thin passthrough, same rationale as MarkStuck above.

func (*Storage) SaveInstances

func (s *Storage) SaveInstances(instances []*Instance) error

SaveInstances upserts each started instance into the repository.

func (*Storage) SaveInstancesSync

func (s *Storage) SaveInstancesSync(instances []*Instance) error

SaveInstancesSync saves instances synchronously (same as SaveInstances for the repo backend).

func (*Storage) SaveReviewVerdict added in v1.35.0

func (s *Storage) SaveReviewVerdict(ctx context.Context, itemSessionID string, verdict ReviewVerdictData) error

SaveReviewVerdict upserts a ReviewVerdict for a given ItemSession UUID.

func (*Storage) SaveSession

func (s *Storage) SaveSession(ctx context.Context, session *Session) error

SaveSession upserts a session using the Session domain model. If the session exists it is updated; otherwise it is created. Deprecated InstanceData-based methods (SaveInstances, LoadInstances) remain for backward compatibility.

func (*Storage) SetBacklogItemPRAndTransition added in v1.41.0

func (s *Storage) SetBacklogItemPRAndTransition(ctx context.Context, observed *BacklogItemData, prURL string, prNumber int, summary string, guard *PRReassignmentGuard) error

SetBacklogItemPRAndTransition is the shared primary-write path for recording a PR that genuinely exists on GitHub against a backlog item and moving it review -> pr_pending, or — when observed is already pr_pending — correcting an already-recorded PR to a different one (a reassignment, e.g. the tracked branch was polluted and the real PR was opened from a clean one instead). Used by both the agent-initiated report_pr_created MCP tool (server/mcp/tools_backlog.go, Epic 3.1), the reconciliation backstop detector (BacklogLifecycleListener.reconcileOrphanedAgentPRs, Epic 3.2), and the manual-override RPC (server/services/backlog_service_lifecycle.go) — see "PR Metadata Capture Fix", project_plans/backlog-agent-communication/implementation/plan.md.

observed must be the caller's own, already-fetched snapshot of the item — this function never re-fetches it. That's load-bearing, not an optimization: the CAS precondition below pins to observed.Status and observed.UpdatedAt exactly as the caller read them. A prior version of this function did its own internal GetBacklogItem call and derived the precondition from THAT fresh read; under a real race, that internal read could land after a concurrent winner's write had already committed, so it would see the winner's (already valid) post-write state and re-derive a precondition that ALSO matched — letting a second, policy-unvalidated call silently succeed as an accidental "reassignment" the caller never actually decided to allow (its override_reason/merged-PR/author checks all ran against the caller's original, now-stale read). Pinning to the caller's own observed snapshot closes that: any state change since the caller's read — including a second call winning first — is guaranteed to fail this call's CAS, rather than being silently reinterpreted.

Only observed.Status == "review" or "pr_pending" is accepted; anything else is rejected outright (ErrPreconditionFailed) rather than attempted against an arbitrary starting status.

Unlike AppendProgressNote's best-effort discipline, a failure persisting the PR fields or performing the transition is returned to the caller, not merely logged — BUG-040's root cause #1 was exactly this class of silent failure (a write whose result was never checked against the invariant it protects), and this is the primitive that must not repeat it.

Idempotent: if observed is already pr_pending with this exact prNumber, this is a no-op success — a retried report_pr_created call (network blip) or the reconciliation backstop re-scanning an item it already fixed on a prior tick must not error.

guard is required whenever observed is already pr_pending (a reassignment): the caller must supply a PRReassignmentGuard attesting it already verified override_reason, the currently-tracked PR's merged state, and the new PR's author — nil (or a guard failing any of those checks) is rejected outright with ErrPRReassignmentNotAllowed. This function does not itself call GitHub — that verification stays in the caller (server/mcp/tools_backlog.go's reportPRCreated is the only caller today with that machinery) — but centralizing the *requirement* here means every caller of this shared primitive gets the same guarantee: a caller with no way to produce a valid guard (e.g. the manual-override RPC in server/services/backlog_service_lifecycle.go, which by design never calls GitHub — see its own doc comment) simply cannot reassign, rather than silently succeeding because the check only lived in one handler. guard is ignored (may be nil) when observed.Status is review — a first-time recording never needs one.

func (*Storage) SetCallbackDispatcher added in v1.43.0

func (s *Storage) SetCallbackDispatcher(d CallbackDispatcher)

SetCallbackDispatcher forwards to the concrete *EntRepository's SetCallbackDispatcher, mirroring SetItemChangePublisher above — same reasoning: server/dependencies.go only has a *Storage value in scope. When the repository is not ent-backed, the dispatcher is simply never wired (no panic).

func (*Storage) SetItemChangePublisher added in v1.41.0

func (s *Storage) SetItemChangePublisher(p ItemChangePublisher)

SetItemChangePublisher wires p into the underlying repository when it is ent-backed, following the same type-assertion-forwarding precedent as GetEntClient above. server/dependencies.go only has a *Storage value in scope (Storage.repo is a Repository interface field, not a concrete *EntRepository), so this forwarding method is the entry point it uses to reach ItemChangePublisher wiring. When the repository is not ent-backed (e.g. an in-memory test double), the type assertion fails gracefully and the publisher is simply never wired — no panic, matching GetEntClient's nil-on-mismatch behavior.

func (*Storage) SetItemSessionBaseCommit added in v1.41.0

func (s *Storage) SetItemSessionBaseCommit(ctx context.Context, id, sha string) error

SetItemSessionBaseCommit records the pre-work base commit SHA on an ItemSession. See the EntRepository method for why this is separate from git activity.

func (*Storage) SetSessionGoal added in v1.35.0

func (s *Storage) SetSessionGoal(ctx context.Context, sessionUUID string, goal string, status string, tasks []TaskNode, setBy string, workspaceKey string) (*SessionGoalData, error)

SetSessionGoal upserts the goal for a session (1:1 per session_uuid). If a goal already exists for the session, it is replaced. workspaceKey, when non-empty, is stamped in the same upsert as the goal write (rather than a separate follow-up UPDATE) so the two never diverge on a crash between writes.

func (*Storage) SetSessionGoalWorkspaceKey added in v1.41.0

func (s *Storage) SetSessionGoalWorkspaceKey(ctx context.Context, sessionUUID, workspaceKey string) error

SetSessionGoalWorkspaceKey stamps the workspace_key column on an existing goal row. Best-effort/no-op if no goal row exists yet for sessionUUID. Kept separate from SetSessionGoal so that method's signature (and existing tests/call sites) stay unchanged.

func (*Storage) SnoozeStuckState added in v1.38.0

func (s *Storage) SnoozeStuckState(ctx context.Context, itemID string, reason domain.StuckReason, until time.Time) (bool, error)

SnoozeStuckState sets snoozed_until on an open BacklogStuckState row for (itemID, reason). Returns false, nil when the backend does not support stuck-state writes or no matching open row exists — never an error for a missing row.

func (*Storage) TransitionBacklogItemStatus added in v1.35.0

func (s *Storage) TransitionBacklogItemStatus(ctx context.Context, id string, toStatus BacklogStatus, precondition *BacklogItemPrecondition, triggeredBy string) (*BacklogItemData, error)

TransitionBacklogItemStatus changes the status of a backlog item.

func (*Storage) UnarchiveBacklogItem added in v1.44.0

func (s *Storage) UnarchiveBacklogItem(ctx context.Context, id string) (*BacklogItemData, error)

UnarchiveBacklogItem clears archived_at and restores the item to "idea".

func (*Storage) UnresolvedBlockerIDs added in v1.43.0

func (s *Storage) UnresolvedBlockerIDs(ctx context.Context, itemID string) ([]string, error)

UnresolvedBlockerIDs returns the specific blocker item IDs still unresolved for a single item.

func (*Storage) UnresolvedBlockerItemIDs added in v1.43.0

func (s *Storage) UnresolvedBlockerItemIDs(ctx context.Context, itemIDs []string) (map[string]bool, error)

UnresolvedBlockerItemIDs returns the subset of itemIDs blocked by an unresolved dependency.

func (*Storage) UpdateAcCriterionStatus added in v1.35.0

func (s *Storage) UpdateAcCriterionStatus(ctx context.Context, itemID string, criterionIndex int, status string, note string) error

UpdateAcCriterionStatus updates a single acceptance criterion's status by index.

func (*Storage) UpdateBacklogItem added in v1.35.0

func (s *Storage) UpdateBacklogItem(ctx context.Context, id string, update BacklogItemUpdate, precondition *BacklogItemPrecondition) (*BacklogItemData, error)

UpdateBacklogItem modifies an existing backlog item.

func (*Storage) UpdateInstance

func (s *Storage) UpdateInstance(instance *Instance) error

UpdateInstance updates an existing instance in storage.

func (*Storage) UpdateInstanceAcknowledged added in v1.18.0

func (s *Storage) UpdateInstanceAcknowledged(title string) error

UpdateInstanceAcknowledged sets the LastAcknowledged timestamp to now for a specific instance. Used by AcknowledgeSession when the instance is not available in the live poller.

func (*Storage) UpdateInstanceArtifacts added in v1.35.0

func (s *Storage) UpdateInstanceArtifacts(title string, blob string) error

UpdateInstanceArtifacts persists the JSON-encoded artifact blob for a session. Only the session_artifacts column is touched; all other fields are unchanged.

func (*Storage) UpdateInstanceForkFlag added in v1.12.0

func (s *Storage) UpdateInstanceForkFlag(_ string, _ bool) error

UpdateInstanceForkFlag is intentionally a no-op: fork status is not persisted in the ent schema. Callers (e.g. PRStatusPoller) call this as a persistence hook, but no DB write occurs.

func (*Storage) UpdateInstanceLastAddedToQueue

func (s *Storage) UpdateInstanceLastAddedToQueue(title string, lastAddedToQueue time.Time) error

UpdateInstanceLastAddedToQueue updates ONLY the LastAddedToQueue field for a specific instance.

func (*Storage) UpdateInstanceLastUserResponse

func (s *Storage) UpdateInstanceLastUserResponse(title string, lastUserResponse time.Time) error

UpdateInstanceLastUserResponse persists the LastUserResponse timestamp for a session. Uses a direct UPDATE (no read round-trip) via UpdateReviewQueueState.

func (*Storage) UpdateInstanceMetadata added in v1.42.0

func (s *Storage) UpdateInstanceMetadata(currentTitle string, newTitle, category, note, workingDir *string) error

UpdateInstanceMetadata persists a narrow set of session metadata fields (title rename, category, note, working dir) via a single UPDATE, avoiding the full-row rewrite (and worktree/diffstats/tags/claude_session churn) that SaveInstances performs for every started session. currentTitle must be the title from before any rename already applied in-memory by the caller — see EntRepository.UpdateSessionMetadata for why. A nil field pointer leaves that field untouched.

func (*Storage) UpdateInstancePRNumber added in v1.12.0

func (s *Storage) UpdateInstancePRNumber(title string, prNumber int) error

UpdateInstancePRNumber persists the discovered PR number for a session so it survives restarts and avoids repeated branch-name lookups in PRStatusPoller.

func (*Storage) UpdateInstancePRStatus added in v1.12.0

func (s *Storage) UpdateInstancePRStatus(_, _, _, _ string, _, _ int, _, _ bool) error

UpdateInstancePRStatus updates the PR status fields for a specific instance. PR fields are not stored in the ent schema — they live in memory and are re-populated by PRStatusPoller on each poll cycle. No DB write is needed.

func (*Storage) UpdateInstanceProcessingGrace

func (s *Storage) UpdateInstanceProcessingGrace(title string, processingGraceUntil time.Time) error

UpdateInstanceProcessingGrace persists the ProcessingGraceUntil timestamp. Uses a direct UPDATE (no read round-trip) via UpdateReviewQueueState.

func (*Storage) UpdateInstanceTimestampsOnly

func (s *Storage) UpdateInstanceTimestampsOnly(title string, lastTerminalUpdate, lastMeaningfulOutput time.Time, lastOutputSignature string, lastViewed time.Time) error

UpdateInstanceTimestampsOnly updates ONLY the timestamp fields in storage without creating Instance objects. This preserves in-memory state like controllers. This is critical for WebSocket terminal streaming which updates timestamps frequently.

func (*Storage) UpdateItemSessionEnded added in v1.35.0

func (s *Storage) UpdateItemSessionEnded(ctx context.Context, id string, endedAt time.Time) error

UpdateItemSessionEnded records the end time for an ItemSession.

func (*Storage) UpdateItemSessionEndedWithReason added in v1.41.0

func (s *Storage) UpdateItemSessionEndedWithReason(ctx context.Context, id string, endedAt time.Time, reason string) error

UpdateItemSessionEndedWithReason records the end time for an ItemSession alongside classifyHeadlessCallError's bucket (or "" for a successful end).

func (*Storage) UpdateItemSessionFailureCapture added in v1.42.0

func (s *Storage) UpdateItemSessionFailureCapture(ctx context.Context, id string, path string) error

UpdateItemSessionFailureCapture records the absolute path to a durable raw-output capture file for a headless triage/review call that errored or produced unparseable output. See EntRepository.UpdateItemSessionFailureCapture.

func (*Storage) UpdateItemSessionGitActivity added in v1.37.0

func (s *Storage) UpdateItemSessionGitActivity(ctx context.Context, id string, sha, msg string, commitAt time.Time, commitCount int) error

UpdateItemSessionGitActivity records the session's current tip commit and related fields on an ItemSession. For the spawn-time baseline, use SetItemSessionBaseCommit.

func (*Storage) UpdateItemSessionSessionUUID added in v1.35.0

func (s *Storage) UpdateItemSessionSessionUUID(ctx context.Context, id string, sessionUUID string) error

UpdateItemSessionSessionUUID updates the session_uuid on an existing ItemSession record.

func (*Storage) UpdateItemSessionStarted added in v1.35.0

func (s *Storage) UpdateItemSessionStarted(ctx context.Context, id string, startedAt time.Time) error

UpdateItemSessionStarted records the start time for an ItemSession.

func (*Storage) UpdateItemSessionTriageResult added in v1.35.0

func (s *Storage) UpdateItemSessionTriageResult(ctx context.Context, id string, triageResult string) error

UpdateItemSessionTriageResult stores the triage result JSON payload on an ItemSession.

func (*Storage) UpdateItemSessionVerificationNotes added in v1.37.0

func (s *Storage) UpdateItemSessionVerificationNotes(ctx context.Context, id string, verificationNotes string) error

UpdateItemSessionVerificationNotes stores verification evidence (commands run, manual checks performed) reported via request_review on an ItemSession.

func (*Storage) UpdateItemSource added in v1.35.0

func (s *Storage) UpdateItemSource(ctx context.Context, id string, update ItemSourceUpdate) (*ItemSourceData, error)

UpdateItemSource modifies an existing item source.

func (*Storage) UpdateProject added in v1.23.0

func (s *Storage) UpdateProject(ctx context.Context, data ProjectData) (*ProjectData, error)

UpdateProject modifies an existing project in storage.

func (*Storage) UpdateSessionTaskStatus added in v1.35.0

func (s *Storage) UpdateSessionTaskStatus(ctx context.Context, sessionUUID string, taskID string, newStatus string) (*SessionGoalData, error)

UpdateSessionTaskStatus loads the goal for a session, finds the task by ID, updates its status, and saves the goal back. Returns ErrNotFound if no goal exists, or an error if task_id is not found in the tree. The read-modify-write is wrapped in a transaction to prevent concurrent update races.

func (*Storage) UpsertRule added in v1.12.0

func (s *Storage) UpsertRule(ctx context.Context, rule ApprovalRuleData) error

UpsertRule creates or updates an auto-approval rule in the repository.

func (*Storage) WireChainFirer added in v1.43.0

func (s *Storage) WireChainFirer(workflows WorkflowRepository, fireEvents TriggerFireEventRepository, firer TriggerFirer, cfg *config.Config) *ChainFirer

WireChainFirer constructs a ChainFirer bound to the underlying *EntRepository (so its ListItemSessions/UpdateBacklogItem calls share the exact same callbackDispatcher/itemChangePublisher wiring as every other backlog mutation) and wires it as that repository's own chain-fire dispatcher (EntRepository.SetChainFirer — the happy-path caller from TransitionBacklogItemStatus, webhook-triggers Phase 6). Returns nil when the repository is not ent-backed, mirroring GetEntClient's nil-on-mismatch behavior — callers should skip TriggerChainReconciler wiring in that case.

type SubcommandDecisionCount added in v1.35.0

type SubcommandDecisionCount struct {
	Subcommand string
	Decision   string
	Count      int
}

SubcommandDecisionCount holds a (subcommand, decision) aggregate count. Returned by GetSubcommandBreakdown.

type Subscriber

type Subscriber struct {
	ID string
	Ch chan ResponseChunk
	// contains filtered or unexported fields
}

Subscriber represents a client that is receiving response chunks.

type SuspendedProcessRecord added in v1.42.0

type SuspendedProcessRecord struct {
	PID          int32                    `json:"pid"`
	CreateTimeMs int64                    `json:"create_time_ms"`
	Candidate    ExternalSessionCandidate `json:"candidate"`
	InstanceID   string                   `json:"instance_id"`
}

SuspendedProcessRecord durably records an external process this server SIGSTOP'd during CommitImportExternalSession, so a server restart can reconcile it (see ReconcileSuspendedProcesses) instead of leaving it frozen forever.

type SuspendedProcessStore added in v1.42.0

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

SuspendedProcessStore persists SuspendedProcessRecord entries to suspended_processes.json using the same exclusive-flock + write-tmp-then-rename idiom as config/state.go.

func NewSuspendedProcessStore added in v1.42.0

func NewSuspendedProcessStore() (*SuspendedProcessStore, error)

NewSuspendedProcessStore creates a store rooted at the same config directory config.GetConfigDir() resolves (workspace-isolated in tests via STAPLER_SQUAD_TEST_DIR, matching config/state.go's behavior).

func (*SuspendedProcessStore) Add added in v1.42.0

Add persists a SuspendedProcessRecord under an exclusive lock, using upsert semantics: any existing record for the same InstanceID is replaced rather than duplicated. CommitImportExternalSession's compensating-delete path can retry Add for the same InstanceID (e.g. after a transient write failure), and without this dedup a retry would leave two records for one instance, which would make ReconcileSuspendedProcesses/Remove behavior ambiguous.

func (*SuspendedProcessStore) Get added in v1.42.0

Get returns the SuspendedProcessRecord for instanceID, if one exists.

func (*SuspendedProcessStore) List added in v1.42.0

List returns every currently-persisted SuspendedProcessRecord.

func (*SuspendedProcessStore) Remove added in v1.42.0

func (s *SuspendedProcessStore) Remove(instanceID string) error

Remove deletes the record for instanceID, if present. Removing a non-existent record is not an error -- callers call this on every resume/kill path, some of which race with reconciliation.

type SyncLoop added in v1.35.0

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

SyncLoop drives periodic sync of all enabled ItemSources.

func NewSyncLoop added in v1.35.0

func NewSyncLoop(storage *Storage, registry *PluginRegistry) *SyncLoop

NewSyncLoop creates a SyncLoop with the default interval and no key provider.

func NewSyncLoopWithKeyProvider added in v1.35.0

func NewSyncLoopWithKeyProvider(storage *Storage, registry *PluginRegistry, keyFunc func() ([]byte, error)) *SyncLoop

NewSyncLoopWithKeyProvider creates a SyncLoop with a key provider for decryption.

func (*SyncLoop) DecryptConfigToken added in v1.41.0

func (sl *SyncLoop) DecryptConfigToken(raw string) (string, error)

DecryptConfigToken decrypts an encrypted token in config JSON if needed. If the config has "encrypted":true, it decrypts the token field using the provided key function. If decryption is not available or not needed, returns the raw config unchanged. Exported so package server/services (holding a *SyncLoop handle) can call it cross-package for the forward-sync subscriber.

func (*SyncLoop) PreviewBackwardSyncImpact added in v1.41.0

func (sl *SyncLoop) PreviewBackwardSyncImpact(ctx context.Context, source *ent.ItemSource) (itemCount int, sampleTitles []string, possiblyIncomplete bool, err error)

PreviewBackwardSyncImpact reports how many already-imported items for source would immediately transition — per determineBackwardSyncTarget, ADR-002 — if backward sync were enabled for it right now. Used to gate the Settings UI's first-enable confirmation dialog (Epic 4.4, resolving Unresolved Question #3) so a user can see the blast radius of already-closed linked issues before opting in, rather than a silent bulk-archive on the same tick the toggle flips.

Read-only: reuses the same decrypted-token/plugin-Fetch path SyncOne uses, but does not advance source.SyncCursor, does not record a SourceSyncEvent, and does not itself gate on source.BackwardSyncEnabled — the entire point is to preview what WOULD happen if it were enabled. Deliberately fetches with an empty cursor rather than source.SyncCursor: the preview needs the full current state of already-imported items' issues (a source may have been forward-syncing for a while before backward sync is ever considered, advancing the cursor well past issues that are now closed but haven't changed since).

If the plugin implements PaginatedFetcher, the full result set is fetched across all pages (bounded by the plugin's own cap) rather than just the newest page — GitHub's Issues API sorts by `created` descending by default, so a single-page Fetch would silently miss older closed issues on repos with more than one page of history, undercounting the blast radius. possiblyIncomplete is true when the underlying fetch hit its page cap, meaning the count/titles returned are a lower bound, not exhaustive.

func (*SyncLoop) PreviewBackwardSyncImpactByID added in v1.41.0

func (sl *SyncLoop) PreviewBackwardSyncImpactByID(ctx context.Context, sourceID string) (itemCount int, sampleTitles []string, possiblyIncomplete bool, err error)

PreviewBackwardSyncImpactByID looks up an ItemSource by ID and previews the impact of enabling backward sync for it — see PreviewBackwardSyncImpact.

func (*SyncLoop) Start added in v1.35.0

func (sl *SyncLoop) Start(ctx context.Context)

Start runs the sync loop until ctx is cancelled or Stop is called.

func (*SyncLoop) Stop added in v1.35.0

func (sl *SyncLoop) Stop()

Stop gracefully shuts down the sync loop. Safe to call multiple times.

func (*SyncLoop) SyncByID added in v1.35.0

func (sl *SyncLoop) SyncByID(ctx context.Context, sourceID string) error

SyncByID looks up an ItemSource by ID and syncs it, regardless of its Enabled flag — unlike the periodic loop (runAllSources), which only syncs enabled sources, this is for an explicit manual/on-demand trigger where the caller already decided to sync this specific source.

func (*SyncLoop) SyncOne added in v1.35.0

func (sl *SyncLoop) SyncOne(ctx context.Context, source *ent.ItemSource) error

SyncOne fetches and upserts items for a single ItemSource. Concurrent calls for the same source (e.g. a manual TriggerSync racing the periodic tick) are serialized via a per-source lock — see syncSourceLocks.

type TagManager

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

TagManager provides CRUD operations for session tags. It is a pure data structure with no I/O or external dependencies. Thread safety is provided by Instance.mu -- callers must hold the lock when calling TagManager methods.

TagManager stores a pointer to the Instance.Tags slice so that mutations are automatically visible via inst.Tags (used by instance_adapter.go, review_queue_poller.go, and ToInstanceData for serialization).

func NewTagManager

func NewTagManager(tags *[]string) TagManager

NewTagManager creates a TagManager backed by the given slice pointer.

func (*TagManager) Add

func (tm *TagManager) Add(tag string) error

Add adds a tag if it does not already exist and does not exceed MaxTagLength. Returns ErrTagTooLong if the tag exceeds MaxTagLength. Returns ErrDuplicateTag if the tag already exists.

func (*TagManager) All

func (tm *TagManager) All() []string

All returns a copy of the tag slice.

func (*TagManager) Has

func (tm *TagManager) Has(tag string) bool

Has returns true if the tag exists.

func (*TagManager) Remove

func (tm *TagManager) Remove(tag string)

Remove removes a tag by value. No-op if the tag does not exist.

func (*TagManager) Set

func (tm *TagManager) Set(tags []string) error

Set replaces all tags with a new deduplicated set. Returns ErrTagTooLong on the first tag that exceeds MaxTagLength. Returns ErrTooManyTags if the deduplicated count exceeds MaxTagCount.

type TaskNode added in v1.35.0

type TaskNode struct {
	ID       string     `json:"id"`
	Title    string     `json:"title"`
	Status   string     `json:"status"`
	Children []TaskNode `json:"children,omitempty"`
}

TaskNode represents a single task in the goal's task tree.

func DecodeTasks added in v1.35.0

func DecodeTasks(s string) ([]TaskNode, error)

DecodeTasks deserializes a JSON string to a task tree.

type TerminalContext

type TerminalContext struct {
	// Height is the terminal height in rows
	Height int `json:"height,omitempty"`

	// Width is the terminal width in columns
	Width int `json:"width,omitempty"`

	// TmuxSessionName is the name of the tmux session
	TmuxSessionName string `json:"tmux_session_name,omitempty"`

	// TmuxPrefix is the prefix used for tmux session naming
	TmuxPrefix string `json:"tmux_prefix,omitempty"`

	// TmuxServerSocket is the path to the tmux server socket
	TmuxServerSocket string `json:"tmux_server_socket,omitempty"`

	// TerminalType indicates the terminal backend type
	// Possible values: "tmux", "mux", "pty", "web"
	TerminalType string `json:"terminal_type,omitempty"`
}

TerminalContext represents the terminal-related context for a session. This includes terminal dimensions, tmux configuration, and terminal type.

func (*TerminalContext) IsEmpty

func (t *TerminalContext) IsEmpty() bool

IsEmpty returns true if the TerminalContext has no meaningful data

type TimeRestriction

type TimeRestriction struct {
	DaysOfWeek []time.Weekday `json:"days_of_week"` // Empty = all days
	StartHour  int            `json:"start_hour"`   // 0-23
	EndHour    int            `json:"end_hour"`     // 0-23
}

TimeRestriction limits when a policy is active.

type TimelineSnapshot added in v1.41.0

type TimelineSnapshot struct {
	StartedAt time.Time
	StoppedAt time.Time
}

TimelineSnapshot captures the start/stop times of a session.

func BuildTimelineSnapshot added in v1.41.0

func BuildTimelineSnapshot(createdAt time.Time, stoppedAt time.Time) TimelineSnapshot

BuildTimelineSnapshot builds a TimelineSnapshot from a session's creation time and the time it was captured as stopped (both captured at dispatch time by the caller, not re-derived later).

func (TimelineSnapshot) Duration added in v1.41.0

func (t TimelineSnapshot) Duration() time.Duration

Duration returns the elapsed time between StartedAt and StoppedAt.

type TmuxBackend added in v1.35.0

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

TmuxBackend implements ProcessManager by delegating to TmuxManager. It is the default backend used when process_manager_backend = "tmux" (or empty).

func NewTmuxBackend added in v1.35.0

func NewTmuxBackend(mgr TmuxManager) *TmuxBackend

NewTmuxBackend creates a TmuxBackend wrapping the given TmuxManager.

func (*TmuxBackend) Attach added in v1.35.0

func (b *TmuxBackend) Attach() (chan struct{}, error)

func (*TmuxBackend) CapturePaneContent added in v1.35.0

func (b *TmuxBackend) CapturePaneContent() (string, error)

func (*TmuxBackend) CapturePaneContentRaw added in v1.35.0

func (b *TmuxBackend) CapturePaneContentRaw() (string, error)

func (*TmuxBackend) CapturePaneContentWithOptions added in v1.35.0

func (b *TmuxBackend) CapturePaneContentWithOptions(start, end string) (string, error)

func (*TmuxBackend) CaptureViewport added in v1.35.0

func (b *TmuxBackend) CaptureViewport(lines int) (string, error)

func (*TmuxBackend) Close added in v1.35.0

func (b *TmuxBackend) Close() error

func (*TmuxBackend) DetachSafely added in v1.35.0

func (b *TmuxBackend) DetachSafely() error

func (*TmuxBackend) FilterBanners added in v1.35.0

func (b *TmuxBackend) FilterBanners(content string) (string, int)

func (*TmuxBackend) GetCurrentWorkingDirectory added in v1.35.0

func (b *TmuxBackend) GetCurrentWorkingDirectory() (string, error)

GetCurrentWorkingDirectory returns the current working directory of the pane. Delegates to the underlying Session().GetPaneCurrentPath() via type assertion.

func (*TmuxBackend) GetCursorPosition added in v1.35.0

func (b *TmuxBackend) GetCursorPosition() (x, y int, err error)

func (*TmuxBackend) GetPTY added in v1.35.0

func (b *TmuxBackend) GetPTY() (*os.File, error)

func (*TmuxBackend) GetPaneDimensions added in v1.35.0

func (b *TmuxBackend) GetPaneDimensions() (width, height int, err error)

func (*TmuxBackend) GetPanePID added in v1.35.0

func (b *TmuxBackend) GetPanePID() (int32, error)

func (*TmuxBackend) GetSessionIdentifier added in v1.35.0

func (b *TmuxBackend) GetSessionIdentifier() string

GetSessionIdentifier implements ProcessManager by delegating to GetTmuxSessionName. This is the name-mapping method: backend-agnostic callers use GetSessionIdentifier, but the value is identical to what GetTmuxSessionName returns for the tmux backend.

func (*TmuxBackend) HasMeaningfulContent added in v1.35.0

func (b *TmuxBackend) HasMeaningfulContent(content string) bool

func (*TmuxBackend) HasSession added in v1.35.0

func (b *TmuxBackend) HasSession() bool

func (*TmuxBackend) HasUpdated added in v1.35.0

func (b *TmuxBackend) HasUpdated() (updated bool, hasPrompt bool, content string)

func (*TmuxBackend) IsAlive added in v1.35.0

func (b *TmuxBackend) IsAlive() bool

func (*TmuxBackend) RefreshClient added in v1.35.0

func (b *TmuxBackend) RefreshClient() error

func (*TmuxBackend) ResetExitOnce added in v1.35.0

func (b *TmuxBackend) ResetExitOnce()

func (*TmuxBackend) RestoreWithWorkDir added in v1.35.0

func (b *TmuxBackend) RestoreWithWorkDir(w string) error

func (*TmuxBackend) SendInputViaControlMode added in v1.35.0

func (b *TmuxBackend) SendInputViaControlMode(ctx context.Context, data []byte) error

func (*TmuxBackend) SendKeys added in v1.35.0

func (b *TmuxBackend) SendKeys(keys string) (int, error)

func (*TmuxBackend) SendPromptWithEnter added in v1.35.0

func (b *TmuxBackend) SendPromptWithEnter(p string) error

func (*TmuxBackend) SetDetachedSize added in v1.35.0

func (b *TmuxBackend) SetDetachedSize(w, h int, title string) error

func (*TmuxBackend) SetOnExitCallback added in v1.35.0

func (b *TmuxBackend) SetOnExitCallback(fn func(string))

func (*TmuxBackend) SetWindowSize added in v1.35.0

func (b *TmuxBackend) SetWindowSize(cols, rows int) error

func (*TmuxBackend) Start added in v1.35.0

func (b *TmuxBackend) Start(dir string) error

func (*TmuxBackend) StartControlMode added in v1.35.0

func (b *TmuxBackend) StartControlMode() error

func (*TmuxBackend) StopControlMode added in v1.35.0

func (b *TmuxBackend) StopControlMode() error

func (*TmuxBackend) SubscribeToControlModeUpdates added in v1.35.0

func (b *TmuxBackend) SubscribeToControlModeUpdates() (string, chan []byte)

func (*TmuxBackend) TapEnter added in v1.35.0

func (b *TmuxBackend) TapEnter() error

func (*TmuxBackend) TmuxManager added in v1.35.0

func (b *TmuxBackend) TmuxManager() TmuxManager

TmuxManager returns the underlying TmuxManager for type assertions in reconciliation paths that need tmux-specific operations (e.g. Session(), SetSession()).

func (*TmuxBackend) UnsubscribeFromControlModeUpdates added in v1.35.0

func (b *TmuxBackend) UnsubscribeFromControlModeUpdates(id string)

type TmuxManager added in v1.15.0

type TmuxManager interface {
	HasSession() bool
	Session() *tmux.TmuxSession
	SetSession(*tmux.TmuxSession)
	GetTmuxSessionName() string
	IsAlive() bool
	Close() error
	DetachSafely() error
	DoesSessionExist() bool
	SetDetachedSize(width, height int, instanceTitle string) error
	Attach() (chan struct{}, error)
	CapturePaneContent() (string, error)
	CapturePaneContentPriority() (string, error)
	CapturePaneContentRaw() (string, error)
	CapturePaneContentWithOptions(startLine, endLine string) (string, error)
	GetPaneDimensions() (width, height int, err error)
	GetCursorPosition() (x, y int, err error)
	GetPTY() (*os.File, error)
	SendKeys(keys string) (int, error)
	SetWindowSize(cols, rows int) error
	RefreshClient() error
	RefreshClientPriority() error
	TapEnter() error
	HasUpdated() (updated bool, hasPrompt bool, content string)
	RestoreWithWorkDir(workDir string) error
	Start(dir string) error
	FilterBanners(content string) (string, int)
	HasMeaningfulContent(content string) bool
	CaptureViewport(lines int) (string, error)
	SendPromptWithEnter(prompt string) error
	GetPanePID() (int32, error)
	SetOnExitCallback(fn func(string))
	ResetExitOnce()
	StartControlMode() error
	StopControlMode() error
	SubscribeToControlModeUpdates() (string, chan []byte)
	UnsubscribeFromControlModeUpdates(id string)
	SendInputViaControlMode(ctx context.Context, data []byte) error
	// PaneExitStatus reports whether the pane's wrapped program has already
	// exited even though the tmux session object itself is still alive.
	// remain-on-exit keeps a "Pane is dead" placeholder pane around after the
	// wrapped program is killed (e.g. OOM SIGKILL) instead of tearing the
	// session down, so IsAlive()/HasSession() alone cannot detect this state.
	PaneExitStatus() (code int, signal string, dead bool)
}

TmuxManager is the interface satisfied by *TmuxProcessManager. It covers all tmux session operations used by Instance and can be implemented by test doubles to avoid requiring a real tmux server.

type TmuxProcessManager

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

TmuxProcessManager owns the tmux session and preview-size tracking state that were previously scattered as bare fields on Instance.

Instance keeps thin wrapper methods (with started/paused guards) that delegate here. TmuxProcessManager itself has no knowledge of Instance lifecycle; it only manages the tmux session and the preview-resize bookkeeping.

func (*TmuxProcessManager) Attach

func (tm *TmuxProcessManager) Attach() (chan struct{}, error)

Attach returns a channel that closes when the user detaches from the session.

func (*TmuxProcessManager) CapturePaneContent

func (tm *TmuxProcessManager) CapturePaneContent() (string, error)

CapturePaneContent returns the current visible pane content. Results are cached for capturePaneCacheTTL to reduce subprocess/forkLock contention when called per-session on every poll tick.

func (*TmuxProcessManager) CapturePaneContentPriority added in v1.44.0

func (tm *TmuxProcessManager) CapturePaneContentPriority() (string, error)

CapturePaneContentPriority mirrors CapturePaneContent but routes the subprocess call through the resync exec-gate fast lane instead of the default pool (Epic 4.2, terminal:resync-exec-gate-fast-lane), and always captures fresh — it deliberately bypasses the capturePaneCacheTTL cache since resync's whole point is an up-to-date snapshot, not the cached value a concurrent poll tick may have populated.

func (*TmuxProcessManager) CapturePaneContentRaw

func (tm *TmuxProcessManager) CapturePaneContentRaw() (string, error)

CapturePaneContentRaw returns pane content with ANSI escape codes preserved.

func (*TmuxProcessManager) CapturePaneContentWithOptions

func (tm *TmuxProcessManager) CapturePaneContentWithOptions(startLine, endLine string) (string, error)

CapturePaneContentWithOptions captures pane content between startLine and endLine.

func (*TmuxProcessManager) CaptureViewport

func (tm *TmuxProcessManager) CaptureViewport(lines int) (string, error)

CaptureViewport captures the last N lines of the pane. If lines <= 0, captures the current viewport height.

func (*TmuxProcessManager) Close

func (tm *TmuxProcessManager) Close() error

Close terminates the tmux session.

func (*TmuxProcessManager) DetachSafely

func (tm *TmuxProcessManager) DetachSafely() error

DetachSafely detaches the current tmux client from the session without closing it.

func (*TmuxProcessManager) DoesSessionExist

func (tm *TmuxProcessManager) DoesSessionExist() bool

DoesSessionExist returns true if the tmux session name is registered with the server.

func (*TmuxProcessManager) FilterBanners

func (tm *TmuxProcessManager) FilterBanners(content string) (string, int)

FilterBanners strips banner/header content from terminal output.

func (*TmuxProcessManager) GetCursorPosition

func (tm *TmuxProcessManager) GetCursorPosition() (x, y int, err error)

GetCursorPosition returns the current cursor column and row (0-based).

func (*TmuxProcessManager) GetPTY

func (tm *TmuxProcessManager) GetPTY() (*os.File, error)

GetPTY returns the PTY master file for reading terminal output.

func (*TmuxProcessManager) GetPaneDimensions

func (tm *TmuxProcessManager) GetPaneDimensions() (width, height int, err error)

GetPaneDimensions returns the current pane width and height.

func (*TmuxProcessManager) GetPanePID

func (tm *TmuxProcessManager) GetPanePID() (int32, error)

GetPanePID returns the PID of the foreground process in the pane. The pane PID is stable for the lifetime of a tmux pane, so the result is cached after the first successful lookup to avoid repeated subprocess calls.

func (*TmuxProcessManager) GetTmuxSessionName added in v1.15.0

func (tm *TmuxProcessManager) GetTmuxSessionName() string

GetTmuxSessionName returns the sanitized tmux session name for reconciliation. Returns empty string when no session has been initialized.

func (*TmuxProcessManager) HasMeaningfulContent

func (tm *TmuxProcessManager) HasMeaningfulContent(content string) bool

HasMeaningfulContent reports whether the terminal output contains substantive content.

func (*TmuxProcessManager) HasSession

func (tm *TmuxProcessManager) HasSession() bool

HasSession reports whether a tmux session has been initialized.

func (*TmuxProcessManager) HasUpdated

func (tm *TmuxProcessManager) HasUpdated() (updated bool, hasPrompt bool, content string)

HasUpdated reports whether the pane content has changed since the last check.

func (*TmuxProcessManager) IsAlive

func (tm *TmuxProcessManager) IsAlive() bool

IsAlive reports whether the tmux session process is still running.

func (*TmuxProcessManager) PaneExitStatus added in v1.37.0

func (tm *TmuxProcessManager) PaneExitStatus() (code int, signal string, dead bool)

PaneExitStatus reports the wrapped program's exit code/signal for a dead pane whose tmux session is otherwise still alive (remain-on-exit keeps the placeholder pane around after the wrapped program exits/is killed). Returns dead=false if there is no session, or the pane is still running.

func (*TmuxProcessManager) RefreshClient

func (tm *TmuxProcessManager) RefreshClient() error

RefreshClient forces the tmux client to redraw.

func (*TmuxProcessManager) RefreshClientPriority added in v1.44.0

func (tm *TmuxProcessManager) RefreshClientPriority() error

RefreshClientPriority mirrors RefreshClient but routes the subprocess call through the resync exec-gate fast lane instead of the default pool (Epic 4.2, terminal:resync-exec-gate-fast-lane).

func (*TmuxProcessManager) ResetExitOnce added in v1.15.0

func (tm *TmuxProcessManager) ResetExitOnce()

ResetExitOnce resets the sync.Once guard so that the exit callback can fire again on the next start cycle (e.g., after a restart). No-op if no session.

func (*TmuxProcessManager) RestoreWithWorkDir

func (tm *TmuxProcessManager) RestoreWithWorkDir(workDir string) error

RestoreWithWorkDir re-attaches to an existing session in the given directory.

func (*TmuxProcessManager) SendInputViaControlMode added in v1.35.0

func (tm *TmuxProcessManager) SendInputViaControlMode(ctx context.Context, data []byte) error

SendInputViaControlMode sends raw bytes through the existing control mode connection.

func (*TmuxProcessManager) SendKeys

func (tm *TmuxProcessManager) SendKeys(keys string) (int, error)

SendKeys sends a string of keys to the tmux session and returns the number of bytes written.

func (*TmuxProcessManager) SendPromptWithEnter

func (tm *TmuxProcessManager) SendPromptWithEnter(prompt string) error

SendPromptWithEnter sends text to the session followed by Enter key. Includes a brief pause between text and Enter to prevent interpretation issues.

func (*TmuxProcessManager) Session

func (tm *TmuxProcessManager) Session() *tmux.TmuxSession

Session returns the underlying tmux session (may be nil before Start).

func (*TmuxProcessManager) SetDetachedSize

func (tm *TmuxProcessManager) SetDetachedSize(width, height int, instanceTitle string) error

SetDetachedSize updates the tmux window dimensions without attaching. Rate-limits PTY-not-initialized warnings to avoid log spam.

func (*TmuxProcessManager) SetOnExitCallback added in v1.15.0

func (tm *TmuxProcessManager) SetOnExitCallback(fn func(string))

SetOnExitCallback registers a callback that fires when the tmux session exits unexpectedly. No-op if no session is initialized.

func (*TmuxProcessManager) SetSession

func (tm *TmuxProcessManager) SetSession(s *tmux.TmuxSession)

SetSession replaces the underlying tmux session. Used by tests and by Instance.start() when reusing a pre-created session.

func (*TmuxProcessManager) SetWindowSize

func (tm *TmuxProcessManager) SetWindowSize(cols, rows int) error

SetWindowSize resizes the tmux window to the given columns and rows.

func (*TmuxProcessManager) Start

func (tm *TmuxProcessManager) Start(dir string) error

Start creates and starts the tmux session in the given directory.

func (*TmuxProcessManager) StartControlMode added in v1.15.0

func (tm *TmuxProcessManager) StartControlMode() error

StartControlMode starts the tmux control mode stream. Returns nil if no session is initialized.

func (*TmuxProcessManager) StopControlMode added in v1.15.0

func (tm *TmuxProcessManager) StopControlMode() error

StopControlMode stops the tmux control mode stream. Returns nil if no session is initialized.

func (*TmuxProcessManager) SubscribeToControlModeUpdates added in v1.15.0

func (tm *TmuxProcessManager) SubscribeToControlModeUpdates() (string, chan []byte)

SubscribeToControlModeUpdates registers a new subscriber for real-time terminal output. Returns a pre-closed channel if no session is initialized.

func (*TmuxProcessManager) TapEnter

func (tm *TmuxProcessManager) TapEnter() error

TapEnter sends an Enter key to the session.

func (*TmuxProcessManager) UnsubscribeFromControlModeUpdates added in v1.15.0

func (tm *TmuxProcessManager) UnsubscribeFromControlModeUpdates(id string)

UnsubscribeFromControlModeUpdates removes a subscriber by ID. No-op if no session is initialized.

type TmuxSocketQuerier added in v1.37.0

type TmuxSocketQuerier interface {
	// ListSessions returns the set of live tmux session names on serverSocket.
	ListSessions(serverSocket string) (map[string]bool, error)
	// IsServerDown reports whether the tmux server on serverSocket is unreachable.
	IsServerDown(serverSocket string) bool
}

TmuxSocketQuerier abstracts read-only tmux server-socket queries so that callers needing to know "which sessions are alive on this socket" or "is this socket's server down" can be exercised in tests without a real tmux server. Production code backs this with the real tmux package (via realTmuxSocketQuerier); tests substitute a fake.

Both ReviewQueuePoller.reconcileSessions and SessionHealthChecker.CheckAllSessions need this: instances can be spread across multiple tmux server sockets (the default socket for ordinary sessions, isolated sockets for some worktree/test scenarios), so every query must be scoped to a specific socket rather than assumed to be shared across all instances.

func NewRealTmuxSocketQuerier added in v1.42.0

func NewRealTmuxSocketQuerier() TmuxSocketQuerier

NewRealTmuxSocketQuerier returns the production TmuxSocketQuerier backed by the real tmux package, for callers outside this package (e.g. server/services) that need to query live tmux state without depending on the unexported realTmuxSocketQuerier type directly.

type TransitionDef added in v1.35.0

type TransitionDef struct {
	From Status
	To   Status
	// Guard is called before the status is updated. Return non-nil to abort.
	// nil means unconditionally allowed.
	Guard func(ctx context.Context, i *Instance) error
	// After is called once the status has been updated (side-effects: process
	// management, worktree ops, scrollback restore, etc.).
	// nil means no post-transition side-effect.
	After func(ctx context.Context, i *Instance)
}

TransitionDef describes a single valid state machine transition with optional guard (pre-condition) and after (post-transition side-effect) hooks.

type TriageRespawner added in v1.41.0

type TriageRespawner interface {
	// AutoRespawnTriage re-triggers triage for itemID. No-op (nil error) if
	// the item already moved off "idea" by the time this runs (e.g. a human
	// already re-triggered triage manually, or the item was otherwise
	// resolved) — mirrors AutoRespawnReview's identical staleness guard.
	AutoRespawnTriage(ctx context.Context, itemID string) error

	// IsTriageLive reports whether the implementer (BacklogService) itself still
	// has a headless triage call genuinely in flight for itemID. Added for
	// reconcileOrphanedTriageItems' shape-1 staleness gate (BUG-055): a headless
	// triage session has no live tmux instance to query, so before this existed
	// that gate had no way to tell a session still open past
	// maxHeadlessTriageSessionStaleness because it's genuinely still running
	// apart from one that's actually dead — it assumed the latter unconditionally.
	IsTriageLive(itemID string) bool
}

TriageRespawner can automatically re-trigger triage for a backlog item whose most recent triage-role ItemSession orphaned (StuckReasonOrphanedTriage — reconcileOrphanedTriageItems found the session still open long after it should have finished, tombstoned it, and marked the item stuck). Before this existed, orphaned_triage was detection-and-notify only, exactly like StuckReasonAbandonedReview before ReviewRespawner: a human had to notice the one-time notification and manually re-trigger triage (confirmed live 2026-07-27, docs/tasks/backlog-feature-improvement.md — items 4f03de7b and 505fb733 sat in "idea" for 2 days this way). Implemented outside this package (BacklogService owns TriggerTriage, the RPC-shaped entry point that already knows how to tombstone/re-trigger triage safely) and wired via SetTriageRespawner, mirroring StaleWorkRemediator/ReviewRespawner exactly.

type TriageSuggestion added in v1.35.0

type TriageSuggestion struct {
	Text      string `json:"text"`
	Rationale string `json:"rationale"`
}

TriageSuggestion is a canonical suggestion entry shared by the headless triage path and the submit_triage_result MCP tool.

type TriageTask added in v1.35.0

type TriageTask struct {
	Text     string `json:"text"`
	Estimate string `json:"estimate"`
	Category string `json:"category"`
}

TriageTask is a canonical implementation task shared by the headless triage path and the submit_triage_result MCP tool.

type TriggerChainReconciler added in v1.43.0

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

TriggerChainReconciler completes pipeline chain-fires interrupted by a crash between the "done" transition committing (EntRepository.dispatchChainFire) and the chained session actually being created — AC5's restart-recovery scenario. ReconcileChains is invoked from BacklogLifecycleListener's existing 60s reconcile tick (ReconcileStuck), mirroring reconcileStaleWorkSessions' idiom exactly: no dedicated goroutine/ticker of its own (plan.md's explicit instruction — reuse the existing ticker infra, don't create a second one).

func NewTriggerChainReconciler added in v1.43.0

func NewTriggerChainReconciler(firer *ChainFirer) *TriggerChainReconciler

NewTriggerChainReconciler constructs a TriggerChainReconciler bound to firer — the same ChainFirer instance wired as the happy-path dispatcher (EntRepository.SetChainFirer), so both paths share one semaphore and one "fire exactly once" implementation (ChainFirer.Fire).

func (*TriggerChainReconciler) ReconcileChains added in v1.43.0

func (r *TriggerChainReconciler) ReconcileChains(ctx context.Context, er *EntRepository)

ReconcileChains scans done items for a pending, unfired chain (NextWorkflowID != nil && !ChainFired) and re-dispatches each through ChainFirer.Dispatch — the same async, semaphore-bounded path the happy-path caller uses, so a tick with many pending chains can't block the reconcile sweep itself on a burst of expensive CreateSession calls.

type TriggerFireEventInput added in v1.43.0

type TriggerFireEventInput struct {
	// WorkflowID is nil when the request was rejected before a Workflow could be
	// resolved (e.g. an unknown webhook slug).
	WorkflowID   *uuid.UUID
	Outcome      string // "fired_success" | "fired_failed" | "no_match" | "rejected"
	DeliveryID   string // "" when not applicable (e.g. cron fires) — left unset, not stored as ""
	SessionID    string
	ErrorMessage string
}

TriggerFireEventInput holds the fields for recording a single trigger evaluation attempt (fired/no-match/rejected).

type TriggerFireEventRepository added in v1.43.0

type TriggerFireEventRepository interface {
	// Create inserts a new TriggerFireEvent row. Returns ErrDuplicateDelivery when the
	// (workflow_id, delivery_id) unique constraint is violated by a concurrent duplicate
	// delivery.
	Create(ctx context.Context, input TriggerFireEventInput) error
	// ListByWorkflow returns the most recent TriggerFireEvent rows for workflowID,
	// newest first, capped at limit.
	ListByWorkflow(ctx context.Context, workflowID uuid.UUID, limit int) ([]*ent.TriggerFireEvent, error)
	// UpdateOutcome transitions an existing row — identified by its (workflow_id,
	// delivery_id) composite key, the same key Create claims atomically — to a final
	// outcome, optionally setting sessionID/errMsg (empty strings leave those fields
	// untouched). Used by the webhook handlers (Epic 2.2/2.3) to move a freshly-claimed
	// "pending" row to "fired_success"/"fired_failed" after the fire attempt completes.
	// Returns an error if no row matches the key.
	UpdateOutcome(ctx context.Context, workflowID uuid.UUID, deliveryID, outcome, sessionID, errMsg string) error
}

TriggerFireEventRepository defines persistence operations for the trigger-fire audit trail.

type TriggerFirer added in v1.43.0

type TriggerFirer interface {
	// FireTriggerChained fires wf as the next hop in a pipeline chain:
	// priorItemSummary (built via BuildSessionInitialPrompt over the
	// just-completed item) is interpolated into wf's own prompt template, and
	// chainDepth is threaded onto the created session's
	// TriggeredByChainDepth attribution field. Returns the created session ID.
	FireTriggerChained(ctx context.Context, wf *ent.Workflow, priorItemSummary string, chainDepth int32) (sessionID string, err error)
}

TriggerFirer is the narrow interface ChainFirer needs to fire the next workflow in a pipeline chain. Defined here (consumer-defined), not in server/workflows, to avoid a session -> server/workflows import cycle (server/workflows already imports session for WorkflowRepository, TriggerFireEventRepository, etc. — see server/workflows/scheduler.go). Satisfied by *workflows.Scheduler's FireTriggerChained method — per .claude/rules/interface-pollution-checklist.md, this is a genuine cross-package boundary (unlike ChainFirer's other collaborators below, which live in this same package and are referenced by concrete type).

type TurnCallback added in v1.35.0

type TurnCallback func(turn, maxTurns int, prompt string)

TurnCallback is called after each successful turn injection.

type UIPreferences

type UIPreferences struct {
	// Category is the organizational category for the session
	Category string `json:"category,omitempty"`

	// IsExpanded indicates if the session is expanded in grouped views
	IsExpanded bool `json:"is_expanded,omitempty"`

	// Tags are the user-defined tags for multi-dimensional organization
	Tags []string `json:"tags,omitempty"`

	// GroupingStrategy is the current grouping mode (e.g., "category", "tag", "branch")
	GroupingStrategy string `json:"grouping_strategy,omitempty"`

	// SortOrder is the preferred sort order (e.g., "name", "date", "status")
	SortOrder string `json:"sort_order,omitempty"`
}

UIPreferences represents the UI-related preferences for a session. This includes categorization, tags, and display preferences.

func (*UIPreferences) HasTag

func (u *UIPreferences) HasTag(tag string) bool

HasTag returns true if the UIPreferences contains the specified tag

func (*UIPreferences) IsEmpty

func (u *UIPreferences) IsEmpty() bool

IsEmpty returns true if the UIPreferences has no meaningful data

type UsageLimit

type UsageLimit struct {
	MaxUses     int           `json:"max_uses"`     // 0 = unlimited
	TimeWindow  time.Duration `json:"time_window"`  // 0 = no time window
	PerApproval bool          `json:"per_approval"` // Track per approval type vs globally
}

UsageLimit restricts how many times a policy can be used.

type VCSInfo

type VCSInfo struct {
	// VCSType is "jj" or "git"
	VCSType string
	// HasJJ indicates if JJ is available
	HasJJ bool
	// HasGit indicates if Git is available
	HasGit bool
	// IsColocated indicates if this is a JJ+Git colocated repo
	IsColocated bool
	// RepoPath is the repository root path
	RepoPath string
	// CurrentBookmark is the current branch/bookmark name
	CurrentBookmark string
	// CurrentRevision is the current revision (short ID)
	CurrentRevision string
	// HasUncommittedChanges indicates if there are uncommitted changes
	HasUncommittedChanges bool
	// ModifiedFileCount is the count of modified/added/deleted files
	ModifiedFileCount int
}

VCSInfo contains version control information for a session

type VNCProcessManager added in v1.35.0

type VNCProcessManager = vnc.VNCProcessManager

VNCProcessManager is a local alias for the vnc package interface so that files within the session package can reference it without importing vnc directly.

type WorkflowCreateInput added in v1.35.0

type WorkflowCreateInput struct {
	Slug            string
	Name            string
	Description     string
	Command         string
	TargetDirectory string
	InputTemplate   string
	SessionType     string
	Model           string
	AgentType       string
	CronExpression  string
	CronEnabled     bool
	// Enabled is the generic per-trigger enable gate. Pointer, not a bare bool: the
	// schema default (true) only applies when the column is never explicitly set, so
	// a bare bool would silently create a disabled workflow for any caller that
	// forgets to set it (the zero value is false) — nil here means "use the schema
	// default," matching every other optional-with-a-meaningful-default field's shape.
	Enabled           *bool
	KeepSessions      *int // nil = use default (0, disabled); 0 = keep all
	ArchiveAfterHours *int // nil = use default (0, disabled); 0 = disabled

	// Trigger fields (webhook-triggers Epic 1.1).
	TriggerType            string // "cron" | "github_push" | "webhook" | "manual"; "" -> ent default "manual"
	GitHubRepo             string
	GitHubBranch           string
	WebhookSlug            string
	WebhookSecretEncrypted string
	EventFilter            string
	LabelFilter            string
	PromptTemplate         string
}

WorkflowCreateInput holds the fields for creating a new workflow.

type WorkflowEngine added in v1.35.0

type WorkflowEngine interface {
	// CanTransition returns true if transitioning from → to is structurally allowed.
	CanTransition(from, to BacklogStatus) bool
	// ValidateGates runs guard rules for the transition. Returns nil if gates pass.
	ValidateGates(item BacklogItemTransitionInput, to BacklogStatus) error
	// AllowedTransitions returns the set of statuses reachable from from.
	AllowedTransitions(from BacklogStatus) []BacklogStatus
}

WorkflowEngine is the policy object that governs which backlog status transitions are permitted and what guards must pass.

type WorkflowRepository added in v1.35.0

type WorkflowRepository interface {
	Create(ctx context.Context, w WorkflowCreateInput) (*ent.Workflow, error)
	Update(ctx context.Context, id uuid.UUID, w WorkflowUpdateInput) (*ent.Workflow, error)
	// UpdateConditional applies a partial update only if the row's current updated_at
	// exactly matches expectedUpdatedAt (optimistic-concurrency CAS, mirrors
	// EntRepository.TransitionBacklogItemStatus's precondition pattern). Returns
	// ErrPreconditionFailed if the row has been modified since expectedUpdatedAt was
	// read, or ErrNotFound if the row doesn't exist.
	UpdateConditional(ctx context.Context, id uuid.UUID, w WorkflowUpdateInput, expectedUpdatedAt time.Time) (*ent.Workflow, error)
	Delete(ctx context.Context, id uuid.UUID) error
	GetByID(ctx context.Context, id uuid.UUID) (*ent.Workflow, error)
	GetBySlug(ctx context.Context, slug string) (*ent.Workflow, error)
	// GetByWebhookSlug retrieves a workflow by its webhook_slug (the routing key for
	// POST /webhooks/{slug}). Only meaningful when TriggerType == "webhook".
	GetByWebhookSlug(ctx context.Context, slug string) (*ent.Workflow, error)
	ListAll(ctx context.Context) ([]*ent.Workflow, error)
	ListEnabled(ctx context.Context) ([]*ent.Workflow, error) // cron_enabled=true
	// ListByTriggerType returns all workflows with the given trigger_type, regardless
	// of cron_enabled — callers (e.g. GitHubWebhookHandler) filter further by
	// repo/branch/enabled in application code. Narrowing by trigger_type at the DB
	// layer first avoids a linear decrypt-and-check over every workflow in the system
	// (webhook-triggers Task 2.2.1b).
	ListByTriggerType(ctx context.Context, triggerType string) ([]*ent.Workflow, error)
}

WorkflowRepository defines persistence operations for workflow definitions.

type WorkflowUpdateInput added in v1.35.0

type WorkflowUpdateInput struct {
	Name              *string
	Description       *string
	Command           *string
	TargetDirectory   *string
	InputTemplate     *string
	SessionType       *string
	Model             *string
	AgentType         *string
	CronExpression    *string
	CronEnabled       *bool
	Enabled           *bool // generic per-trigger enable gate
	KeepSessions      *int  // nil = do not update; 0 = keep all (disabled)
	ArchiveAfterHours *int  // nil = do not update; 0 = disabled

	// Trigger fields (webhook-triggers Epic 1.1).
	TriggerType            *string
	GitHubRepo             *string
	GitHubBranch           *string
	WebhookSlug            *string
	WebhookSecretEncrypted *string
	EventFilter            *string
	LabelFilter            *string
	PromptTemplate         *string
	LastFiredAt            *time.Time
}

WorkflowUpdateInput holds optional fields for updating an existing workflow. Pointer fields are only applied when non-nil (partial update).

type Workspace added in v1.12.0

type Workspace struct {
	// EffectivePath is the directory where the session process runs.
	// For worktree sessions: the worktree directory.
	// For directory sessions: the session's Path field.
	EffectivePath string

	// RepoRoot is the git repository root (the main checkout, not the worktree).
	// For directory sessions, this is the same as EffectivePath.
	RepoRoot string
}

Workspace describes where a session is operating. Use Instance.Workspace() to obtain this value; do not construct directly.

type WorkspacePath added in v1.35.0

type WorkspacePath string

WorkspacePath represents a cleaned, resolved workspace root path.

func NewWorkspacePath added in v1.35.0

func NewWorkspacePath(s string) (WorkspacePath, error)

NewWorkspacePath resolves symlinks and cleans a path to guarantee a single canonical representation.

type WorkspacePeer added in v1.41.0

type WorkspacePeer struct {
	SessionUUID string
	Title       string
	Branch      string
	Path        string
	Status      Status
	Goal        *SessionGoalData // nil when the peer has never set a goal

	// InstanceLive is a best-effort liveness signal: false when the session's Status is
	// Stopped. Callers wanting an authoritative "process confirmed dead" signal should
	// cross-reference LiveTmuxSessionUUIDs and override this field.
	InstanceLive bool
	// StaleGoal is true when the peer is live but its goal hasn't been updated within
	// goalStaleThreshold — independent of InstanceLive, per the two-signal requirement.
	StaleGoal bool
}

WorkspacePeer is another session sharing the caller's workspace (repo), regardless of which worktree/branch it's on.

func (WorkspacePeer) Lifecycle added in v1.41.0

func (p WorkspacePeer) Lifecycle() string

Lifecycle returns a coarse peer status label derived from the two independent signals: "gone" (process confirmed dead), "stuck" (alive but goal stale), or "active".

type WorkspaceSwitchRequest

type WorkspaceSwitchRequest struct {
	// Type is the type of switch operation
	Type WorkspaceSwitchType
	// Target is the destination (directory path, revision/branch, or worktree path)
	Target string
	// ChangeStrategy determines how to handle uncommitted changes
	ChangeStrategy vcs.ChangeStrategy
	// CreateIfMissing creates the bookmark/branch/worktree if it doesn't exist
	CreateIfMissing bool
	// BaseRevision is the base for new bookmark creation (empty = current)
	BaseRevision string
	// VCSPreference overrides the default VCS preference for this operation
	VCSPreference vcs.VCSPreference
}

WorkspaceSwitchRequest represents a request to switch the workspace

type WorkspaceSwitchResult

type WorkspaceSwitchResult struct {
	// Success indicates if the switch was successful
	Success bool
	// Error contains any error that occurred
	Error error
	// PreviousRevision is the revision before the switch
	PreviousRevision string
	// CurrentRevision is the revision after the switch
	CurrentRevision string
	// VCSType is the VCS that was used
	VCSType vcs.VCSType
	// ChangesHandled describes how uncommitted changes were handled
	ChangesHandled string
}

WorkspaceSwitchResult contains the result of a workspace switch operation

type WorkspaceSwitchType

type WorkspaceSwitchType int

WorkspaceSwitchType defines the type of workspace switch operation

const (
	// SwitchTypeDirectory is a simple directory change (no VCS, no restart)
	SwitchTypeDirectory WorkspaceSwitchType = iota
	// SwitchTypeRevision switches to a different revision/branch
	SwitchTypeRevision
	// SwitchTypeWorktree switches to or creates a different worktree
	SwitchTypeWorktree
)

func (WorkspaceSwitchType) String

func (t WorkspaceSwitchType) String() string

type WorktreeInfo

type WorktreeInfo struct {
	// IsWorktree is true if the path is a git worktree (not the main repo)
	IsWorktree bool
	// MainRepoPath is the path to the main repository's .git directory
	// For a worktree at ~/.stapler-squad/worktrees/foo, this might be /path/to/main/repo/.git
	MainRepoPath string
	// MainRepoRoot is the working directory root of the main repository
	MainRepoRoot string
	// RemoteURL is the git remote origin URL (e.g., https://github.com/owner/repo.git)
	RemoteURL string
	// GitHubOwner is the owner extracted from a GitHub remote URL
	GitHubOwner string
	// GitHubRepo is the repo name extracted from a GitHub remote URL
	GitHubRepo string
}

WorktreeInfo contains information about a git worktree

func DetectWorktree

func DetectWorktree(path string) (*WorktreeInfo, error)

DetectWorktree checks if the given path is a git worktree and extracts relevant info. Results are cached per-path for 5 minutes to avoid repeated git subprocess calls on every LoadInstances invocation for sessions whose GitHubOwner was never resolved. Returns WorktreeInfo with IsWorktree=false if it's not a worktree or not a git repo.

type WorktreePRPoller added in v1.35.0

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

WorktreePRPoller polls GitHub PR status for worktrees that have no active session. It is the counterpart to PRStatusPoller (which covers session-backed worktrees). The two pollers divide the worktree space: PRStatusPoller owns worktrees with a running session; WorktreePRPoller owns the rest.

Concurrency design:

  • data cache is a sync.Map — lock-free reads in the steady state
  • auth state is an atomic.Value (pollerAuthResult) — same pattern as PRStatusPoller
  • onUpdated callback is an atomic.Value — writers Store, readers Load, no lock
  • rateLimitedUntil and noPRPollAfter are guarded by mu

func NewWorktreePRPoller added in v1.35.0

func NewWorktreePRPoller(etagCache *github.ETagCache, prPoller *PRStatusPoller) *WorktreePRPoller

NewWorktreePRPoller creates a WorktreePRPoller with default configuration. source may be nil at construction time; call SetSource before Start.

func NewWorktreePRPollerWithConfig added in v1.35.0

func NewWorktreePRPollerWithConfig(etagCache *github.ETagCache, prPoller *PRStatusPoller, cfg WorktreePRPollerConfig) *WorktreePRPoller

NewWorktreePRPollerWithConfig creates a WorktreePRPoller with custom configuration.

func (*WorktreePRPoller) GetPRData added in v1.35.0

func (p *WorktreePRPoller) GetPRData(repoPath, branch string) *github.PRInfo

GetPRData returns cached PR info for a worktree, or nil if not yet known.

func (*WorktreePRPoller) SetOnUpdated added in v1.35.0

func (p *WorktreePRPoller) SetOnUpdated(fn func(repoPath, branch string, info *github.PRInfo))

SetOnUpdated registers a callback invoked whenever cached PR data changes. Safe to call before or after Start; the callback is replaced atomically.

func (*WorktreePRPoller) SetSource added in v1.35.0

func (p *WorktreePRPoller) SetSource(src WorktreeSource)

SetSource sets the worktree data source. Safe to call before Start.

func (*WorktreePRPoller) Start added in v1.35.0

func (p *WorktreePRPoller) Start(ctx context.Context)

Start begins the polling loop. It is a no-op if already started.

func (*WorktreePRPoller) Stop added in v1.35.0

func (p *WorktreePRPoller) Stop()

Stop gracefully shuts down the poller and waits for in-flight requests.

type WorktreePRPollerConfig added in v1.35.0

type WorktreePRPollerConfig struct {
	PollInterval      time.Duration
	CallTimeout       time.Duration
	AuthCacheDuration time.Duration
	// NoPRBackoff is how long to skip a branch after its PR list returns empty.
	// Zero disables the backoff.
	NoPRBackoff time.Duration
}

WorktreePRPollerConfig controls polling cadence and auth caching.

func DefaultWorktreePRPollerConfig added in v1.35.0

func DefaultWorktreePRPollerConfig() WorktreePRPollerConfig

DefaultWorktreePRPollerConfig returns sensible defaults matching PRStatusPoller.

type WorktreeScanItem added in v1.35.0

type WorktreeScanItem struct {
	RepoPath     string
	Branch       string
	WorktreePath string
}

WorktreeScanItem is the minimal worktree info the poller needs from the unfinished-work scanner. Using a local struct avoids an import cycle:

session → session/unfinished → pkg/events → session

The server layer bridges the two packages via WorktreeSource (adapter pattern).

type WorktreeSource added in v1.35.0

type WorktreeSource interface {
	// ScanDone returns a channel that receives the scan time after every scan pass.
	ScanDone() <-chan time.Time
	// GetWorktrees returns a snapshot of all currently-known worktrees.
	GetWorktrees() []WorktreeScanItem
}

WorktreeSource provides the set of currently-known worktrees. Implement this interface by wrapping an *unfinished.Scanner in the server layer.

type WorktreeTarget

type WorktreeTarget struct {
	Name       string
	Path       string
	Bookmark   string
	RevisionID string
	IsCurrent  bool
}

WorktreeTarget represents a worktree as a switch target

Source Files

Directories

Path Synopsis
Package cdp provides per-session Chrome DevTools Protocol (CDP) browser streaming.
Package cdp provides per-session Chrome DevTools Protocol (CDP) browser streaming.
Package detection: this file implements the TOML→DTO parsing layer for detector plugins (schema v1, see project_plans/detector-plugins/decisions/ADR-003-plugin-toml-schema-v1.md).
Package detection: this file implements the TOML→DTO parsing layer for detector plugins (schema v1, see project_plans/detector-plugins/decisions/ADR-003-plugin-toml-schema-v1.md).
binaries
Package binaries provides per-binary BinaryDetector implementations.
Package binaries provides per-binary BinaryDetector implementations.
dtypes
Package dtypes contains shared types for the detection package and its sub-packages.
Package dtypes contains shared types for the detection package and its sub-packages.
Package domain contains pure domain types for the backlog subsystem.
Package domain contains pure domain types for the backlog subsystem.
ent
tag
Package headless provides a subprocess-based interface for running claude -p headlessly.
Package headless provides a subprocess-based interface for running claude -p headlessly.
Package hibernation provides checkpoint writing and cleanup for hibernated sessions.
Package hibernation provides checkpoint writing and cleanup for hibernated sessions.
Package memory provides session memory measurement for the hibernation sweeper.
Package memory provides session memory measurement for the hibernation sweeper.
memorytest
Package memorytest provides test doubles for the memory package.
Package memorytest provides test doubles for the memory package.
Package mux provides PTY multiplexing functionality for external Claude sessions.
Package mux provides PTY multiplexing functionality for external Claude sessions.
Package scanbuf provides a pooled buffer for bufio.Scanner instances that need to handle large JSONL lines (base64-encoded tool output, etc.).
Package scanbuf provides a pooled buffer for bufio.Scanner instances that need to handle large JSONL lines (base64-encoded tool output, etc.).
Package tokens provides JSONL-based token usage parsing and aggregation for Claude Code sessions.
Package tokens provides JSONL-based token usage parsing and aggregation for Claude Code sessions.
Package unfinished provides background scanning for git worktrees that have uncommitted changes, commits ahead of the default branch, or commits behind.
Package unfinished provides background scanning for git worktrees that have uncommitted changes, commits ahead of the default branch, or commits behind.
gogitstore
mmapindex.go implements the mmap-backed .idx loader described in session/unfinished/design/pluggable-gitstore.md §5 ("mmap for the index — designed, not built").
mmapindex.go implements the mmap-backed .idx loader described in session/unfinished/design/pluggable-gitstore.md §5 ("mmap for the index — designed, not built").
Package vcs provides an abstraction layer over version control systems.
Package vcs provides an abstraction layer over version control systems.
Package vnc provides per-session virtual display and VNC server lifecycle management.
Package vnc provides per-session virtual display and VNC server lifecycle management.
Package workspace provides workspace tracking and status management for stapler-squad sessions.
Package workspace provides workspace tracking and status management for stapler-squad sessions.

Jump to

Keyboard shortcuts

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