itemstate

package
v0.0.77 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package itemstate provides the canonical single-owner state store for per-issue tracking in the Fabrik engine.

This package is the foundation of the reactive cache architecture described in docs/cache-refactor/02-design.md. It replaces the 25+ fragmented in-memory state structures spread across engine/ and boardcache/ with a single Store that holds one ItemState per (repo, issueNumber) pair.

The core contract:

  • All state mutations flow through Store.Apply — no bypassing writes.
  • All reads flow through Store.Get, which returns an immutable Snapshot.
  • Downstream components react to changes via Observer subscriptions.

Design rationale is in adrs/036-reactive-cache-single-owner.md. Phase-by-phase migration strategy is in docs/cache-refactor/02-design.md §5.

This package (Phase 3-A) is a pure addition. It is not yet wired into the engine or boardcache; that happens in Phase 3-B onward.

Index

Constants

This section is empty.

Variables

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

ErrNotFound is returned by Store.Get when an item does not exist in the cache and the FallbackFetcher also cannot locate it.

Functions

This section is empty.

Types

type BaseBranchWarnRecorded

type BaseBranchWarnRecorded struct {
	Repo   string
	Number int
	Branch string
}

BaseBranchWarnRecorded marks a base-branch override as having been warned about.

type BoardReconciled

type BoardReconciled struct {
	Items []gh.ProjectItem
}

BoardReconciled is submitted by the periodic poll loop after a full board fetch. The Store computes per-item deltas and applies them as focused mutations. itemKey returns "" because this mutation affects all items, not a single one.

type CIFixCycleIncremented

type CIFixCycleIncremented struct {
	Repo      string
	Number    int
	StageName string
}

CIFixCycleIncremented increments the CI-fix cycle counter for a stage.

type CIFixNoOpRecorded added in v0.0.71

type CIFixNoOpRecorded struct {
	Repo   string
	Number int
	SHA    string
}

CIFixNoOpRecorded sets LinkedPRState.LastCIFixNoOpSHA to the head SHA at which a CI-fix reinvoke (engine.dispatchCIFixReinvoke) completed without pushing a new commit (#958 leg 2). While the head SHA stays at this value, handleMergeAndCIGates skips further CI-fix dispatch/cycle-increment for it — CIWaitTimeout remains the backstop if CI never resolves on this SHA.

type CIMergePendingCleared

type CIMergePendingCleared struct {
	Repo   string
	Number int
}

CIMergePendingCleared records that CI-gate merge polling has ended for the item's linked PR. Zeros LinkedPRState.CIMergePendingSince. Replaces delete(engine.ciMergePendingSince, iKey).

type CIMergePendingStarted

type CIMergePendingStarted struct {
	Repo   string
	Number int
	At     time.Time
}

CIMergePendingStarted records that CI-gate merge polling has begun for the item's linked PR. Sets LinkedPRState.CIMergePendingSince to At. Replaces engine.ciMergePendingSince[iKey] = time.Now().

type Change

type Change struct {
	// Repo is "owner/repo" identifying the item.
	Repo string
	// Number is the issue number.
	Number int
	// Fields is a bitmask of ChangeFlags indicating which field groups changed.
	Fields ChangeFlags
}

Change describes what fields a mutation altered. Delivered to every Observer after a successful Store.Apply.

type ChangeFlags

type ChangeFlags uint32

ChangeFlags is a bitmask describing which logical field groups of an ItemState were altered by a mutation. Observers use this to cheaply filter whether a Change is relevant to them without inspecting the full Snapshot.

const (
	// StatusChanged indicates the project-board Status column changed.
	StatusChanged ChangeFlags = 1 << iota
	// LabelsChanged indicates the Labels slice changed.
	LabelsChanged
	// LockChanged indicates Lock state was acquired, released, or modified.
	LockChanged
	// StageStateChanged indicates StageState (attempts, cycles, pauses) changed.
	StageStateChanged
	// WorkerChanged indicates any Worker-handle field changed (set, cleared, heartbeat, PID).
	WorkerChanged
	// WorkerLifecycleChanged is a sub-flag emitted only by WorkerEntered and WorkerExited.
	// It is the flag that drives wakeChFlags / mayNeedWork so heartbeats and PID-sets
	// do not cause spurious deep-fetch cycles for in-flight items.
	WorkerLifecycleChanged
	// CooldownChanged indicates CooldownAt map entries were added or removed.
	CooldownChanged
	// LinkedPRChanged indicates LinkedPR state (including check runs) changed.
	LinkedPRChanged
	// CommentsChanged indicates Comments or PR thread comments changed.
	CommentsChanged
	// AssigneesChanged indicates the Assignees slice changed.
	AssigneesChanged
	// TitleBodyChanged indicates Title, Body, URL, or Author changed.
	TitleBodyChanged
	// StateChanged indicates the open/closed State or IsClosed changed.
	StateChanged
	// BlockedByChanged indicates the BlockedBy dependency list changed.
	BlockedByChanged
	// DeepFetchChanged indicates LastDeepFetchAt or LastDeepFetchFailureAt changed.
	DeepFetchChanged
	// InvocationChanged indicates LastInvocationCompleted, LastInvocationBlocked,
	// or LastTokenUsage changed.
	InvocationChanged
	// BaseBranchChanged indicates BaseBranchWarned map changed.
	BaseBranchChanged
	// ItemRemoved indicates the item was removed from the board during a Reset.
	// This flag is distinct from StateChanged (issue open/closed) and is emitted
	// only by Store.Reset for items present in the old map but absent from the
	// new items slice.
	ItemRemoved
	// CheckRunChanged indicates a check run was written to the pre-linkage
	// pendingCheckRuns buffer (SHA not yet linked to any item). Not in wakeChFlags:
	// the CI gate is catch-up-driven, not wake-driven. The flag exists for
	// observability (e.g. future TUI consumers) without forcing a poll wake.
	CheckRunChanged
	// TerminalChanged indicates the Terminal flag was explicitly set or cleared via
	// TerminalFlagSet. Store-internal clears (when status changes) piggyback on
	// StatusChanged and do not emit TerminalChanged.
	TerminalChanged
	// CommentBreakerChanged indicates ItemState.CommentBreaker (invocation
	// timestamps or last author) changed. Informational only — not in
	// wakeChFlags/cycleSetFlags, same treatment as InvocationChanged.
	CommentBreakerChanged
	// PRStateChanged is a narrower sub-flag of LinkedPRChanged, set only by
	// PRDetailsUpdated (LinkedPR.Title/State/Merged/Draft — a genuine PR-level
	// state transition such as merged, closed, or draft<->ready). Distinct from
	// LinkedPRChanged so consumers that only care about the PR's own state (not
	// review activity, check runs, or thread comments, which also set
	// LinkedPRChanged) can filter precisely.
	PRStateChanged
	// SelfWriteBaselineChanged indicates LastSeenSourceUpdatedAt was advanced by
	// a SelfWriteObserved mutation (#1090). Pure bookkeeping for the probe
	// staleness check — intentionally excluded from wakeChFlags/cycleSetFlags
	// (engine/observers.go) so it never wakes the poll loop or bypasses the
	// dispatch cooldown.
	SelfWriteBaselineChanged
	// LabelAppliedAtChanged indicates ItemState.LabelAppliedAt was written by a
	// LabelAppliedAtRecorded mutation (#1314). Informational only — like
	// SelfWriteBaselineChanged, intentionally excluded from wakeChFlags/
	// cycleSetFlags so it never wakes the poll loop or bypasses the dispatch
	// cooldown.
	LabelAppliedAtChanged
)

type CheckRunCompleted

type CheckRunCompleted struct {
	Repo string
	SHA  string
	Run  gh.CheckRun
}

CheckRunCompleted is emitted when a CI check run reaches a terminal state. The SHA field is used to route the run to the correct LinkedPRState.

type CommentBreakerInvocationRecorded added in v0.0.76

type CommentBreakerInvocationRecorded struct {
	Repo   string
	Number int
	At     time.Time
	Author string
	Cutoff time.Time
}

CommentBreakerInvocationRecorded appends a timestamp (and records the triggering comment's author) to ItemState.CommentBreaker for the runaway-loop circuit breaker (#1089). Applied once per comment-processing invocation that actually reaches Claude. Cutoff, when non-zero, additionally prunes any InvocationsAt entries older than Cutoff before appending — the caller (engine) computes it from its own window setting so an issue that receives invocations sparser than the window doesn't grow InvocationsAt without bound; a zero Cutoff performs no pruning (mirrors zero-means-default elsewhere, but here zero means "caller opted out of pruning").

type CommentBreakerReset added in v0.0.76

type CommentBreakerReset struct {
	Repo   string
	Number int
}

CommentBreakerReset clears ItemState.CommentBreaker.InvocationsAt (and LastAuthor) after forward progress is observed: a stage:*:complete transition, a new commit on the branch, a PR state change, an issue-body update, or a manual human unpause (#1089).

type CommentBreakerState added in v0.0.76

type CommentBreakerState struct {
	// InvocationsAt holds the timestamp of each recorded comment-processing
	// invocation since the last reset.
	InvocationsAt []time.Time
	// LastAuthor is the author of the comment that triggered the most recent
	// recorded invocation. Surfaced in the trip comment so the operator knows
	// who/what to look at.
	LastAuthor string
}

CommentBreakerState tracks comment-processing invocation timestamps used by the engine's circuit breaker to detect a non-advancing comment-processing loop. The engine (not this package) owns the threshold/window business logic and prunes-and-counts InvocationsAt on every read — mirroring the existing mergeTrainTrials runaway-guard precedent (ADR-059 D8).

type CommentProcessed

type CommentProcessed struct {
	Repo      string
	Number    int
	CommentID string
	At        time.Time
}

CommentProcessed records that a comment has been processed by the engine. Prevents reprocessing the same comment on subsequent polls or restarts (backed by rocket reactions on GitHub for cross-restart durability).

type CooldownRecorded

type CooldownRecorded struct {
	Repo   string
	Number int
	Reason string
	Until  time.Time
}

CooldownRecorded sets a cooldown expiry for a given reason key.

type DeepFetchFailed

type DeepFetchFailed struct {
	Repo   string
	Number int
	At     time.Time
}

DeepFetchFailed records that a deep-fetch attempt for this item failed.

type DeepFetchInvalidated

type DeepFetchInvalidated struct {
	Repo   string
	Number int
}

DeepFetchInvalidated clears LastDeepFetchAt so the next FetchItemDetails call re-fetches deep fields from GitHub. Used by delta handlers that detect stale deep state (e.g., new PR linkage discovered, new review thread comment added).

type EngineCyclesCleared

type EngineCyclesCleared struct {
	Repo      string
	Number    int
	StageName string
}

EngineCyclesCleared zeroes ReviewCycles, CIFixCycles, RebaseCycles, and EnqueueCycles for a stage. Called by clearFailedStage on unpause/success to prevent stale counters from triggering premature max-cycle pauses on the next run.

type EnginePaused

type EnginePaused struct {
	Repo      string
	Number    int
	StageName string
}

EnginePaused records that the engine has paused work on a stage due to repeated failures. (The design doc listed this as "EngineEnginePaused" — typo.)

type EngineUnpaused

type EngineUnpaused struct {
	Repo      string
	Number    int
	StageName string
}

EngineUnpaused clears PausedByEngine for a stage, used when a user comment triggers an unpause or when clearFailedStage resets engine-managed pause state.

type EnqueueCycleIncremented added in v0.0.71

type EnqueueCycleIncremented struct {
	Repo      string
	Number    int
	StageName string
}

EnqueueCycleIncremented increments the merge-queue re-enqueue cycle counter for a stage (ADR-058 D4 FR-3). Applied on each fresh enqueue trip in the convergence monitor; bounds a queue-thrash loop independently of the rebase/CI-fix caps.

type FallbackFetcher

type FallbackFetcher interface {
	FetchItem(repo string, number int) (gh.ProjectItem, error)
}

FallbackFetcher is called by Store.Get on cache miss to populate the item from GitHub. The actual implementation (wrapping gh.Client) is wired in Phase 3-B.

type InvocationRecorded

type InvocationRecorded struct {
	Repo      string
	Number    int
	Completed bool
	Blocked   bool
	// Errored is true when the Claude process exited non-zero AND the exit was not
	// classified as a turn-limit hit (see TurnLimited). Recorded independently of
	// Completed — the completion marker is authoritative for whether the stage
	// completed; Errored only records that the process didn't exit cleanly for a
	// genuine fault.
	Errored bool
	// TurnLimited is true when the Claude invocation exited because it exhausted
	// its configured turn budget (CLI subtype error_max_turns), as opposed to a
	// genuine failure. A turn-limited run is incomplete but resumable, not an
	// error — see claudeTurnLimitError in engine/claude.go and ADR-1178.
	TurnLimited bool
	Usage       TokenUsage
	// IsComment is true when the invocation processed a user comment rather than
	// running a stage. Stored in ItemState.LastInvocationIsComment so that the
	// InvocationObserver can forward the correct flag to the TUI.
	IsComment bool
	// Duration is the wall-clock time from invocation start to completion.
	// Zero when not set (e.g., comment-processing paths that don't track start time).
	Duration time.Duration
}

InvocationRecorded captures the outcome of a completed Claude invocation for TUI display.

type IssueAssigneesUpdated

type IssueAssigneesUpdated struct {
	Repo      string
	Number    int
	Assignees []string
}

IssueAssigneesUpdated is emitted when the assignee list changes (assigned or unassigned). Assignees is the full post-mutation list from the webhook payload.

type IssueClosed

type IssueClosed struct {
	Repo   string
	Number int
}

IssueClosed is emitted when an issue is closed.

type IssueCommentCreated

type IssueCommentCreated struct {
	Repo    string
	Number  int
	Comment gh.Comment
}

IssueCommentCreated is emitted when a new comment is added to an issue.

type IssueEdited

type IssueEdited struct {
	Repo   string
	Number int
	Title  string
	Body   string
}

IssueEdited is emitted when an issue's title or body is changed.

type IssueLabeled

type IssueLabeled struct {
	Repo   string
	Number int
	Label  string
}

IssueLabeled is emitted when a label is added to an issue.

type IssueOpened

type IssueOpened struct {
	Item gh.ProjectItem
}

IssueOpened is emitted when a new issue is created or first observed. Item is a gh.ProjectItem because there is no separate gh.Issue type; ProjectItem is the full per-item representation used throughout Fabrik.

type IssueReopened

type IssueReopened struct {
	Repo   string
	Number int
}

IssueReopened is emitted when a previously closed issue is reopened.

type IssueUnlabeled

type IssueUnlabeled struct {
	Repo   string
	Number int
	Label  string
}

IssueUnlabeled is emitted when a label is removed from an issue.

type ItemDeepFetched

type ItemDeepFetched struct {
	Repo       string
	Number     int
	FreshState gh.ProjectItem
}

ItemDeepFetched is applied after a single-item deep fetch from the GitHub API.

type ItemIDRegistered

type ItemIDRegistered struct {
	Repo   string
	Number int
	ItemID string
}

ItemIDRegistered sets the project-item node ID for an existing Store entry that was added without one (e.g., via issues.opened before projects_v2_item.created). This is triggered by the Layer 1 fallback GraphQL lookup, not by a Fabrik-initiated GitHub mutation. Returns ChangeFlags(0) — the itemIDToKey reverse index is updated via reflect.DeepEqual detection in applySingleItem regardless of the returned flags. Dispatch is triggered only by the subsequent UpdateItemStatus call, not by this.

type ItemState

type ItemState struct {

	// Repo is "owner/repo".
	Repo string
	// Number is the issue number.
	Number int
	// ID is the GitHub content node ID (e.g. "I_kwDO..." for issues, "PR_kwDO..." for PRs).
	// Used by github.Client.FetchItemDetails for its GraphQL node lookup.
	ID string
	// ItemID is the GitHub Project item node ID (empty if not on the board).
	ItemID string

	Title     string
	Body      string
	URL       string
	Author    string
	Assignees []string
	// State is "open" or "closed".
	State    string
	IsClosed bool
	IsPR     bool
	Labels   []string
	// Status is the project board column ("Specify", "Implement", etc.).
	Status string
	// UpdatedAt is max(issue.updatedAt, projectItem.updatedAt, linkedPR.updatedAt).
	UpdatedAt time.Time

	// BlockedBy holds issues that must be closed before this one can advance.
	BlockedBy []gh.Dependency

	// Comments holds all comments on this issue.
	Comments []gh.Comment

	// LinkedPR is the state of the closing PR; nil if none.
	LinkedPR *LinkedPRState

	// Lock is nil when unlocked; non-nil when this instance or another holds a lock.
	Lock *LockState
	// StageState holds per-stage attempt and cycle counters.
	StageState StageState
	// CooldownAt maps reason → expiry time (e.g. "retry", "review-blocked", "ci-await").
	CooldownAt map[string]time.Time
	// LabelAppliedAt maps label name → the time the engine itself most recently
	// applied that label to this issue (record-at-write, #1314). Deliberately a
	// separate map from CooldownAt, not a repurposing of it: CooldownAt's
	// HasExpiredCooldown treats any non-zero, past timestamp as "wake this item"
	// (engine/poll.go), but an applied-at timestamp is always in the past the
	// instant it's recorded — aliasing the two would make every recorded label
	// application look like a permanently expired cooldown. Populated only for
	// labels the engine writes exclusively through applyLabelAdd or an explicit
	// recordLabelAppliedAtNow call (engine/mutate.go); a label this map has no
	// entry for simply falls back to the live FetchLabelAppliedAt REST fetch.
	LabelAppliedAt map[string]time.Time
	// Worker is present when a worker is in-flight for this item.
	Worker *WorkerHandle

	// Terminal is set by the engine after a deep-fetch confirms the item satisfies
	// the terminal predicate (cleanup-stage status + stage:<Name>:complete label + no
	// transient lifecycle labels). While set, the poll loop skips deep-fetch entirely
	// for this item. Cleared automatically when the item's status changes.
	Terminal bool

	// LastDeepFetchAt is the time of the last successful deep fetch for this item.
	// A zero value means no deep fetch has occurred. Replaces boardcache.deepFetched[key].
	LastDeepFetchAt time.Time

	// LastSeenSourceUpdatedAt is the value of pi.UpdatedAt observed at the moment
	// of the last successful deep fetch. The cache compares this against the fresh
	// pi.UpdatedAt from each board read to decide whether the cached deep fields
	// are still authoritative; a later board updatedAt forces a deep re-fetch.
	// This is the "GraphQL fetching is primary; updatedAt makes the staleness check
	// cheap" contract — webhooks remain an optimization that keeps the cache fresh
	// between polls but never replace the polling refresh path.
	//
	// Has a second writer besides ItemDeepFetched: SelfWriteObserved (#1090)
	// advances it to time.Now() at each of Fabrik's own self-write call sites
	// (label add/remove, comment post, issue body edit, board status move) so a
	// self-caused GitHub updatedAt bump isn't mistaken for external staleness on
	// the next probe cycle. Both writers share the same monotonic guard in
	// applyToItem, and DeepFetchInvalidated's zero-reset always wins against a
	// stale SelfWriteObserved racing behind it.
	LastSeenSourceUpdatedAt time.Time

	// LastDeepFetchFailureAt is the time the most recent deep fetch attempt failed.
	// Replaces engine.deepFetchFailureTime[iKey].
	LastDeepFetchFailureAt time.Time

	// LastInvocationCompleted records whether the most recent Claude invocation
	// emitted FABRIK_STAGE_COMPLETE. Replaces engine.lastCompleted[iKey].
	LastInvocationCompleted bool

	// LastInvocationBlocked records whether the most recent Claude invocation
	// emitted FABRIK_BLOCKED_ON_INPUT. Replaces engine.lastBlocked[iKey].
	LastInvocationBlocked bool

	// LastInvocationIsComment is true when the most recent invocation processed a
	// user comment rather than running a full stage. Mirrors InvocationRecorded.IsComment.
	LastInvocationIsComment bool

	// LastInvocationDuration is the wall-clock time of the most recent Claude invocation.
	// Zero when not recorded (comment-processing paths that don't track start time).
	LastInvocationDuration time.Duration

	// LastInvocationErrored records whether the most recent Claude invocation exited
	// with a non-zero status (process error, timeout kill, etc.) AND was not classified
	// as a turn-limit exit (see LastInvocationTurnLimited) — i.e. a genuine fault. This
	// is recorded independently of LastInvocationCompleted: a stage can complete
	// (FABRIK_STAGE_COMPLETE emitted) even when the process exits non-zero — e.g. a
	// timeout kill after the stage finished. The error is surfaced as
	// JobCompletedEvent.Success=false in history.
	LastInvocationErrored bool

	// LastInvocationTurnLimited records whether the most recent Claude invocation
	// exited because it exhausted its configured turn budget (CLI subtype
	// error_max_turns), as opposed to a genuine failure. A turn-limited invocation
	// is incomplete but resumable — surfaced as JobCompletedEvent.TurnLimited in
	// history, rendered distinctly from a genuine error. See ADR-1178.
	LastInvocationTurnLimited bool

	// LastTokenUsage holds token consumption from the most recent Claude invocation.
	// Replaces engine.lastUsage[iKey].
	LastTokenUsage TokenUsage

	// BaseBranchWarned tracks which base-branch overrides have already produced a
	// "branch not found" warning comment. Replaces engine.baseBranchWarnedSet.
	BaseBranchWarned map[string]bool

	// CommentBreaker tracks comment-processing invocations for the runaway-loop
	// circuit breaker (#1089). Scoped to the item as a whole, not per-stage — the
	// breaker must survive a legitimate stage transition mid-window.
	CommentBreaker CommentBreakerState
}

ItemState is the canonical per-item state. All mutations flow through Store.Apply; all reads flow through Store.Get or change subscriptions.

Field grouping by lifecycle:

  • Identity: never changes after first Apply
  • GitHub state: mirrors GitHub's view of issue, project, and linked PR
  • Engine state: fabrik's local control state (locks, retries, cycle counts, cooldowns)
  • TUI state: mirrors last-invocation outcomes for display

type LabelAppliedAtRecorded added in v0.0.77

type LabelAppliedAtRecorded struct {
	Repo   string
	Number int
	Label  string
	At     time.Time
}

LabelAppliedAtRecorded records the time the engine itself applied Label to this issue (record-at-write, #1314). Always overwrites any prior entry for the same label — a genuine re-application (applied → removed → re-applied) must yield the latest timestamp, not the first. Callers are expected to only record here on a genuine new application (guarded by a "not already present" check at the write site), never as a defensive idempotent no-op.

type LinkageHealAttempted added in v0.0.69

type LinkageHealAttempted struct {
	Repo      string
	Number    int
	StageName string
	PRSHA     string
}

LinkageHealAttempted records that the engine attempted to auto-heal missing PR↔issue linkage for the given stage. Keyed by stage name → PR head SHA. In-memory only — does not survive restart. A force-push (new SHA) clears the guard naturally by virtue of the SHA not matching the stored value.

type LinkedPRState

type LinkedPRState struct {
	Number int
	// Title, State ("open"/"closed"), Merged, and Draft mirror gh.PRDetails fields
	// that were previously stored only in CacheImpl.linkedPRs. Populated by PRDetailsUpdated.
	Title  string
	State  string
	Merged bool
	Draft  bool
	// Mergeable is nil when unknown; true/false once GitHub resolves mergeability.
	Mergeable      *bool
	MergeableState string // "clean", "unstable", "blocked", etc.
	HeadSHA        string
	Reviews        []gh.PRReview
	ReviewRequests []gh.ReviewRequest
	// ThreadComments holds unresolved review-thread comments.
	ThreadComments      []gh.Comment
	ResolvedThreadCount int
	CheckRuns           []gh.CheckRun

	// IsMergeQueueEnabled is true when the repository has the merge queue feature
	// enabled. Populated from GraphQL via FetchItemDetails; zero until first deep fetch.
	IsMergeQueueEnabled bool
	// IsInMergeQueue is true when the PR is currently in the merge queue.
	IsInMergeQueue bool
	// MergeQueueEntry holds queue position and state when the PR is enqueued.
	// Nil when not in queue; set to nil again after dequeueing.
	MergeQueueEntry *gh.MergeQueueEntry

	// HasHadChecks records whether this PR has ever had CI check runs reported.
	// Replaces engine.prHasHadChecks[iKey].
	HasHadChecks bool

	// CIMergePendingSince records when the engine began waiting for the merge
	// to complete after CI passed. Zero if not currently pending.
	// Replaces engine.ciMergePendingSince[iKey].
	CIMergePendingSince time.Time

	// ValidateCompletedSHA records the HEAD SHA of the linked PR at the moment
	// stage:Validate:complete was last applied. The SHA-invalidation scan in
	// engine/poll.go compares this against the current HeadSHA to detect force-pushes
	// or external commits after Validate finished. Empty string means "not recorded"
	// (pre-feature or no Validate completion in this session).
	ValidateCompletedSHA string

	// LastHeadSHAUpdate records when the linked PR's HeadSHA was last observed to
	// change via a PRHeadSHAUpdated mutation. Zero means the SHA has never changed
	// (cold start or post-restart). Used by the post-push dwell guard in checkCIGate
	// to block gate-clearance during the brief window after a force-push when GitHub
	// has not yet computed mergeability or started CI for the new SHA.
	LastHeadSHAUpdate time.Time

	// LastEnqueuedSHA records the PR head SHA at the moment the engine last enqueued
	// (or re-enqueued) this PR into GitHub's native merge queue (ADR-058 D4 FR-3).
	// The convergence monitor uses it to distinguish a genuine post-resolution
	// re-enqueue (head SHA changed since the last enqueue → enqueue fresh) from the
	// brief post-enqueue consistency window where GitHub has not yet reflected
	// isInMergeQueue=true (same SHA → suppress a spurious re-enqueue). Empty until the
	// first enqueue.
	LastEnqueuedSHA string

	// LastCIFixNoOpSHA records the head SHA for which the most recent CI-fix
	// reinvoke (engine.dispatchCIFixReinvoke) observed no new commit pushed
	// (HEAD unchanged before/after processComments). While the head SHA
	// stays at this value, handleMergeAndCIGates skips further CI-fix
	// dispatch/cycle-increment for it — a repeated no-op reinvoke burns
	// nothing further; CIWaitTimeout remains the backstop if CI never
	// resolves. Cleared implicitly once HeadSHA advances past it. Empty
	// means "no no-op recorded for the current SHA."
	LastCIFixNoOpSHA string
}

LinkedPRState holds the state of the closing pull request for an issue.

type LocalCommentAdded

type LocalCommentAdded struct {
	Repo    string
	Number  int
	Comment gh.Comment
}

LocalCommentAdded is applied after fabrik posts a comment on an issue.

type LocalLabelAdded

type LocalLabelAdded struct {
	Repo   string
	Number int
	Label  string
}

LocalLabelAdded is applied after fabrik adds a label to an issue.

type LocalLabelRemoved

type LocalLabelRemoved struct {
	Repo   string
	Number int
	Label  string
}

LocalLabelRemoved is applied after fabrik removes a label from an issue.

type LocalLockAcquired

type LocalLockAcquired struct {
	Repo       string
	Number     int
	User       string
	Worker     *WorkerHandle
	AcquiredAt time.Time // caller-provided time; enables idempotent/no-op detection
}

LocalLockAcquired is applied after fabrik adds the fabrik:locked:<user> label.

type LocalLockReleased

type LocalLockReleased struct {
	Repo   string
	Number int
}

LocalLockReleased is applied after fabrik removes the fabrik:locked:<user> label.

type LocalStatusUpdated

type LocalStatusUpdated struct {
	Repo      string
	Number    int
	NewStatus string
}

LocalStatusUpdated is applied after fabrik calls UpdateProjectItemStatus.

type LockState

type LockState struct {
	// HolderUser is the user identity from the fabrik:locked:<user> label.
	HolderUser string
	// HeldByThis is true if HolderUser matches this engine instance's user and Worker != nil.
	HeldByThis bool
	AcquiredAt time.Time
}

LockState describes who holds the fabrik:locked:<user> label on this issue.

type Logger

type Logger func(format string, args ...any)

Logger is an optional logging function for Store internals. Accepts a printf-style format string and arguments. The default (nil) produces no output.

type Mutation

type Mutation interface {
	// contains filtered or unexported methods
}

Mutation is a discriminated union of every possible state change. Every code path that wants to update ItemState expresses it as a Mutation and calls Store.Apply. There is no other write path.

type Observer

type Observer interface {
	OnChange(change Change, snapshot Snapshot)
}

Observer is implemented by any component that wants to react to ItemState changes.

OnChange is called by Store.Apply after every successful, non-no-op mutation. It is called outside the Store's write lock, so it is safe for observers to call Store.Get or Store.Apply from within OnChange without deadlocking.

Ordering note: when two goroutines call Apply concurrently, observers may see their changes in a different order than the goroutines submitted them. The Store does not guarantee total ordering of concurrent mutations; each Apply is atomic, but relative ordering across concurrent callers is undefined.

type ObserverFunc

type ObserverFunc func(change Change, snapshot Snapshot)

ObserverFunc adapts a plain function to the Observer interface.

func (ObserverFunc) OnChange

func (f ObserverFunc) OnChange(change Change, snapshot Snapshot)

OnChange implements Observer.

type PRChecksObserved

type PRChecksObserved struct {
	Repo   string
	Number int
}

PRChecksObserved records that the linked PR has had at least one CI check run returned by FetchCheckRuns (REST path). Sets LinkedPRState.HasHadChecks = true. Replaces engine.prHasHadChecks[iKey] = true.

type PRCreationFailedRecorded

type PRCreationFailedRecorded struct {
	Repo      string
	Number    int
	StageName string
}

PRCreationFailedRecorded records that Claude completed a stage but the draft PR could not be created. In-memory only — does not survive restart. Cleared by StageRetryCleared when the PR is eventually created or the stage succeeds.

type PRDetailsUpdated

type PRDetailsUpdated struct {
	Repo     string
	Number   int // issue number
	PRNumber int
	Title    string
	State    string
	Merged   bool
	Draft    bool
}

PRDetailsUpdated sets LinkedPR.Title, State, Merged, and Draft for the given item. These four fields were previously stored only in CacheImpl.linkedPRs; this mutation moves them into the Store so PR-detail changes are observable via LinkedPRChanged. PRNumber also ensures the prToKey reverse index is current.

type PREnqueueRecorded added in v0.0.71

type PREnqueueRecorded struct {
	Repo   string
	Number int
	SHA    string
}

PREnqueueRecorded sets LinkedPRState.LastEnqueuedSHA to the head SHA at which the engine last enqueued (or re-enqueued) the PR into GitHub's merge queue (ADR-058 D4 FR-3). Lets the convergence monitor distinguish a genuine post-resolution re-enqueue (SHA changed) from the post-enqueue consistency window (SHA unchanged).

type PRHeadSHAUpdated

type PRHeadSHAUpdated struct {
	Repo        string
	Number      int // issue number
	LinkedPRNum int // PR number to set (0 = leave unchanged)
	SHA         string
}

PRHeadSHAUpdated sets LinkedPR.HeadSHA for the given item and triggers the Store's shaToKey index to be updated. Used by delta handlers when a push event updates the PR's head commit. LinkedPRNum, if non-zero, also sets LinkedPR.Number (used in auto-heal paths where the PR↔issue linkage is being established for the first time).

type PRReviewCommentCreated

type PRReviewCommentCreated struct {
	Repo     string
	PRNumber int
	Comment  gh.Comment
}

PRReviewCommentCreated is emitted when a new inline review comment is added.

type PRReviewRequestRemoved

type PRReviewRequestRemoved struct {
	Repo   string
	Number int // issue number
	Login  string
}

PRReviewRequestRemoved is emitted when one reviewer is removed from the linked PR.

type PRReviewRequested

type PRReviewRequested struct {
	Repo      string
	Number    int // issue number
	Reviewers []gh.ReviewRequest
}

PRReviewRequested is emitted when reviewers are added to the linked PR. Reviewers is the full post-mutation list from the webhook payload.

type PRReviewSubmitted

type PRReviewSubmitted struct {
	Repo   string
	Number int
	Review gh.PRReview
}

PRReviewSubmitted is emitted when a reviewer submits a review on the linked PR.

type ProbeBoardItemUpdated

type ProbeBoardItemUpdated struct {
	Repo   string
	Number int
	Item   gh.BoardProbeItem
}

ProbeBoardItemUpdated updates only the probe-visible fields of an existing item from a ProbeProjectBoard result. Unlike ShallowBoardItemUpdated, it does NOT touch Labels (the probe query fetches no labels). Updates ID, ItemID, State, IsClosed, IsPR, Status, and UpdatedAt only.

type ProjectV2ItemEdited

type ProjectV2ItemEdited struct {
	// ItemID is the GitHub Project item node ID (matches ItemState.ItemID).
	ItemID    string
	NewStatus string
}

ProjectV2ItemEdited is emitted when the project board status field is changed.

type RebaseCycleIncremented

type RebaseCycleIncremented struct {
	Repo      string
	Number    int
	StageName string
}

RebaseCycleIncremented increments the rebase cycle counter for a stage.

type ReviewCycleIncremented

type ReviewCycleIncremented struct {
	Repo      string
	Number    int
	StageName string
}

ReviewCycleIncremented increments the review cycle counter for a stage.

type ReviewThreadCommentAdded

type ReviewThreadCommentAdded struct {
	Repo        string
	IssueNumber int
	Comment     gh.Comment
}

ReviewThreadCommentAdded appends an inline review-thread comment to an item's LinkedPR.ThreadComments, idempotent by NodeID. Uses the ISSUE number (not the PR number) as the routing key. Used by boardcache delta handlers.

type SelfWriteObserved added in v0.0.76

type SelfWriteObserved struct {
	Repo   string
	Number int
}

SelfWriteObserved advances an item's probe staleness baseline (LastSeenSourceUpdatedAt) to the current wall-clock time, without performing a deep fetch and without touching any other field. Applied at every call site where Fabrik performs a self-write (label add/remove, comment post, issue body edit, board status move) that is known to bump the item's real GitHub updatedAt but whose cache write-through already reflects the resulting state (#1090). Store.applyToItem enforces monotonicity: the baseline only ever advances, never moves backward relative to a prior deep-fetch or self-write, so a concurrent DeepFetchInvalidated (which zeroes the baseline) can never be "un-done" by a stale SelfWriteObserved.

type ShallowBoardItemUpdated

type ShallowBoardItemUpdated struct {
	Repo   string
	Number int
	Item   gh.ProjectItem
}

ShallowBoardItemUpdated updates only the shallow fields of an existing item during a Reconcile pass. Unlike ItemDeepFetched or IssueOpened, it does NOT overwrite deep fields (Comments, Body, Assignees, BlockedBy, LinkedPRReviews, etc.). Used exclusively by CacheImpl.Reconcile.

WARNING: do not use with probe data — the Labels field will be set to empty, wiping the cached label set. Use ProbeBoardItemUpdated for probe-loop updates.

type Snapshot

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

Snapshot is an immutable copy of an ItemState returned by Store.Get and Store.Apply. Because it wraps a value (not a pointer), callers can hold it as long as needed without blocking writes and without risk of concurrent mutation.

All slice and map fields are deep-copied when a Snapshot is constructed, so mutations to the Store's internal state do not bleed into held Snapshots.

func (Snapshot) Attempts

func (s Snapshot) Attempts(stageName string) int

Attempts returns the failed attempt count for a given stage, or zero.

func (Snapshot) CIFixCycles

func (s Snapshot) CIFixCycles(stageName string) int

CIFixCycles returns the CI-fix re-invocation cycle count for a given stage.

func (Snapshot) CommentBreakerInvocationsAt added in v0.0.76

func (s Snapshot) CommentBreakerInvocationsAt() []time.Time

CommentBreakerInvocationsAt returns a copy of the recorded comment-processing invocation timestamps since the last reset (#1089).

func (Snapshot) CommentBreakerLastAuthor added in v0.0.76

func (s Snapshot) CommentBreakerLastAuthor() string

CommentBreakerLastAuthor returns the author of the comment that triggered the most recent recorded comment-breaker invocation, or "" if none.

func (Snapshot) CommentProcessed

func (s Snapshot) CommentProcessed(commentID string) time.Time

CommentProcessed returns the timestamp when a comment was last processed, or zero if it has not been seen.

func (Snapshot) CooldownAt

func (s Snapshot) CooldownAt(reason string) time.Time

CooldownAt returns the expiry time for a given cooldown reason, or zero if none.

func (Snapshot) EnqueueCycles added in v0.0.71

func (s Snapshot) EnqueueCycles(stageName string) int

EnqueueCycles returns the merge-queue re-enqueue cycle count for a given stage.

func (Snapshot) HasActiveCooldown

func (s Snapshot) HasActiveCooldown(now time.Time) bool

HasActiveCooldown reports whether any CooldownAt entry has not yet expired relative to now. Reads directly from the snapshot's already-copied map — no additional allocation.

func (Snapshot) HasExpiredCooldown

func (s Snapshot) HasExpiredCooldown(now time.Time) bool

HasExpiredCooldown reports whether any CooldownAt entry is non-zero and has already expired relative to now. Reads directly from the snapshot's already-copied map — no additional allocation.

func (Snapshot) IsClosed

func (s Snapshot) IsClosed() bool

IsClosed reports whether the issue is closed.

func (Snapshot) IsTerminal

func (s Snapshot) IsTerminal() bool

IsTerminal reports whether the terminal flag is set for this item. When true, the poll loop skips deep-fetch as long as status remains in a cleanup stage.

func (Snapshot) LabelAppliedAt added in v0.0.77

func (s Snapshot) LabelAppliedAt(label string) time.Time

LabelAppliedAt returns the time the engine itself most recently applied the given label to this issue, or zero if no record-at-write has been made for it (cold cache — the caller should fall back to a live fetch). See ItemState.LabelAppliedAt's doc comment for why this is a distinct map from CooldownAt.

func (Snapshot) Labels

func (s Snapshot) Labels() []string

Labels returns a copy of the label slice.

func (Snapshot) LastAttemptAt

func (s Snapshot) LastAttemptAt(stageName string) time.Time

LastAttemptAt returns the last invocation timestamp for a given stage, or zero.

func (Snapshot) LastCIFixNoOpSHA added in v0.0.71

func (s Snapshot) LastCIFixNoOpSHA() string

LastCIFixNoOpSHA returns the PR head SHA for which the most recent CI-fix reinvoke completed without pushing a new commit, or "" if not recorded (LinkedPR is nil or no no-op has occurred). Used by handleMergeAndCIGates to skip a further CI-fix dispatch/cycle-increment for the same SHA (#958).

func (Snapshot) LastEnqueuedSHA added in v0.0.71

func (s Snapshot) LastEnqueuedSHA() string

LastEnqueuedSHA returns the PR head SHA recorded at the last merge-queue enqueue, or "" if not recorded (LinkedPR is nil or no enqueue has occurred).

func (Snapshot) LastTurnsCapped added in v0.0.76

func (s Snapshot) LastTurnsCapped(stageName string) bool

LastTurnsCapped reports whether the most recent incomplete invocation of a stage hit its turn cap without completing (#1146).

func (Snapshot) LastTurnsUsed added in v0.0.76

func (s Snapshot) LastTurnsUsed(stageName string) int

LastTurnsUsed returns the TurnsUsed recorded for the most recent incomplete invocation of a stage, or zero if none has been recorded (#1146).

func (Snapshot) LinkageHealAttempted added in v0.0.69

func (s Snapshot) LinkageHealAttempted(stageName, prSHA string) bool

LinkageHealAttempted returns true when a linkage auto-heal has already been attempted for the given stage and PR head SHA. Returns false if either is unknown.

func (Snapshot) LinkedPR

func (s Snapshot) LinkedPR() *LinkedPRState

LinkedPR returns the LinkedPRState, or nil if no closing PR exists.

func (Snapshot) Lock

func (s Snapshot) Lock() *LockState

Lock returns the current LockState, or nil if unlocked.

func (Snapshot) Number

func (s Snapshot) Number() int

Number returns the issue number.

func (Snapshot) PRCreationFailed

func (s Snapshot) PRCreationFailed(stageName string) bool

PRCreationFailed reports whether Claude completed a stage but the draft PR could not be created. In-memory only — does not survive restart.

func (Snapshot) PausedByEngine

func (s Snapshot) PausedByEngine(stageName string) bool

PausedByEngine reports whether the engine has paused this stage due to repeated failures. This flag is in-memory only and does not survive restart.

func (Snapshot) RebaseCycles

func (s Snapshot) RebaseCycles(stageName string) int

RebaseCycles returns the rebase re-invocation cycle count for a given stage.

func (Snapshot) Repo

func (s Snapshot) Repo() string

Repo returns the "owner/repo" identifier.

func (Snapshot) ReviewCycles

func (s Snapshot) ReviewCycles(stageName string) int

ReviewCycles returns the review re-invocation cycle count for a given stage.

func (Snapshot) StallHintPending added in v0.0.76

func (s Snapshot) StallHintPending(stageName string) bool

StallHintPending reports whether a stall was detected for a stage and its next invocation should receive a corrective hint (#1146).

func (Snapshot) State

func (s Snapshot) State() ItemState

State returns a deep copy of the underlying ItemState value. Mutating the returned value — including its slice and map fields — does not affect the Snapshot or the Store.

func (Snapshot) Status

func (s Snapshot) Status() string

Status returns the current project board column.

func (Snapshot) ValidateCompletedSHA added in v0.0.69

func (s Snapshot) ValidateCompletedSHA() string

ValidateCompletedSHA returns the HEAD SHA recorded when stage:Validate:complete was last applied, or "" if not recorded (LinkedPR is nil or field not set).

func (Snapshot) Worker

func (s Snapshot) Worker() *WorkerHandle

Worker returns the active WorkerHandle, or nil if no worker is in-flight.

type StageAttempted

type StageAttempted struct {
	Repo      string
	Number    int
	StageName string
	At        time.Time
}

StageAttempted records the start of a new Claude invocation for a stage.

type StageLastAttemptCleared

type StageLastAttemptCleared struct {
	Repo      string
	Number    int
	StageName string
}

StageLastAttemptCleared zeroes LastAttemptAt for a stage so it re-runs promptly. Used by unblockAwaitingInput to reset the dispatch cooldown after user input.

type StageRetryCleared

type StageRetryCleared struct {
	Repo      string
	Number    int
	StageName string
}

StageRetryCleared resets the attempt counter for a stage.

type StageRetryIncremented

type StageRetryIncremented struct {
	Repo      string
	Number    int
	StageName string
}

StageRetryIncremented increments the attempt counter for a stage.

type StageState

type StageState struct {
	// Attempts counts how many times Claude was invoked for each stage.
	// Replaces engine.retryCount[stageKey].
	Attempts map[string]int
	// LastAttemptAt records the last invocation timestamp per stage.
	// Replaces engine.processedSet[stageKey] for retry-suppression.
	LastAttemptAt map[string]time.Time
	// PausedByEngine records engine-initiated pauses (vs user-initiated).
	// Replaces engine.pausedDueToRetries[stageKey].
	PausedByEngine map[string]bool
	// PRCreationFailed records that Claude completed but the draft PR could not
	// be created. In-memory only — does not survive restart. When set, the next
	// retry first attempts PR creation before re-invoking Claude.
	PRCreationFailed map[string]bool
	// ReviewCycles counts how many review iterations each stage has completed.
	// Replaces engine.reviewCycleCount[stageKey].
	ReviewCycles map[string]int
	// CIFixCycles counts how many CI-fix iterations each stage has completed.
	// Replaces engine.ciFixCycleCount[stageKey].
	CIFixCycles map[string]int
	// RebaseCycles counts how many rebase iterations each stage has completed.
	// Replaces engine.rebaseCycleCount[stageKey].
	RebaseCycles map[string]int
	// EnqueueCycles counts how many times the engine has re-enqueued the linked PR
	// into GitHub's native merge queue after an ejection (ADR-058 D4 FR-3). Bounds a
	// queue-thrash loop (enqueue→eject→re-enqueue→eject) independently of the
	// RebaseCycles/CIFixCycles that the conflict/CI sub-paths increment.
	EnqueueCycles map[string]int
	// ProcessedComments maps comment ID to the time Fabrik finished processing it.
	ProcessedComments map[string]time.Time
	// LinkageHealAttempted maps stage name to the PR head SHA for which a linkage
	// auto-heal was attempted. In-memory only — does not survive restart. Keyed by
	// stage name so force-push (new SHA) clears the guard naturally.
	LinkageHealAttempted map[string]string
	// LastTurnsUsed records the most recently completed invocation's TurnsUsed for
	// each stage. In-memory only — does not survive restart. Used by the stall
	// detector (#1146) to compare consecutive incomplete attempts.
	LastTurnsUsed map[string]int
	// LastTurnsCapped records whether the most recently completed invocation for a
	// stage was turn-capped (TurnsUsed >= MaxTurns without completing). Overwritten
	// on every invocation, which makes stall detection self-limiting to a single
	// corrective hint per episode (#1146): the declining attempt that triggers
	// detection is itself uncapped, so it immediately clears the precondition for
	// the next comparison.
	LastTurnsCapped map[string]bool
	// StallHintPending marks a stage for which a stall was detected and a
	// corrective hint should be injected into the next invocation's prompt. Cleared
	// (consumed) as soon as that invocation is dispatched. In-memory only — does
	// not survive restart (#1146).
	StallHintPending map[string]bool
}

StageState holds per-stage attempt counters and cycle counts. Keys are stage names (e.g. "Implement", "Review").

type StageTurnUsageRecorded added in v0.0.76

type StageTurnUsageRecorded struct {
	Repo      string
	Number    int
	StageName string
	TurnsUsed int
	Capped    bool
}

StageTurnUsageRecorded records the TurnsUsed/capped status of the invocation that just finalized as incomplete for a stage (#1146). Applied once per incomplete invocation, always overwriting the prior value — this is what makes stall detection self-limiting: the very attempt that triggers detection (a decline) is itself uncapped, so it clears the precondition for a subsequent re-arm.

type StallHintArmed added in v0.0.76

type StallHintArmed struct {
	Repo      string
	Number    int
	StageName string
}

StallHintArmed records that a stall was detected for a stage (a turn-capped attempt followed by an incomplete attempt using strictly fewer turns) and the next invocation of that stage should receive a corrective hint (#1146).

type StallHintConsumed added in v0.0.76

type StallHintConsumed struct {
	Repo      string
	Number    int
	StageName string
}

StallHintConsumed clears a pending stall hint once it has been injected into an invocation's prompt (#1146), so the hint fires exactly once per detected episode.

type Store

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

Store is the single owner of all per-item state. All mutations flow through Apply; all reads flow through Get or Observer subscriptions.

Concurrency model:

  • mu guards items, shaToKey, itemIDToKey, and pendingCheckRuns.
  • observerMu guards the observers slice independently to avoid holding mu while calling observer callbacks (which may themselves call Apply or Get).
  • Apply holds mu for the write, captures the observer slice under observerMu, then calls observers after releasing both locks.

func NewStore

func NewStore(fallback FallbackFetcher, opts ...StoreOption) *Store

NewStore creates a new Store. fallback may be nil, in which case cache misses return ErrNotFound instead of triggering a live fetch.

func (*Store) All

func (s *Store) All() []Snapshot

All returns an immutable snapshot of every item currently in the Store. The returned slice is a point-in-time snapshot; subsequent mutations do not affect it. Safe to call concurrently with Apply.

func (*Store) Apply

func (s *Store) Apply(m Mutation) (Snapshot, []Change, error)

Apply mutates state. Every state change flows through here.

Returns the updated Snapshot for the affected item, a list of Changes (zero or one for single-item mutations; multiple for BoardReconciled), and any error.

If the mutation results in no field change (no-op), no Changes are returned and no Observers are notified (invariant I6).

For BoardReconciled, the returned Snapshot is zero-valued; each affected item produces one Change in the returned slice.

func (*Store) CheckRunsBySHA

func (s *Store) CheckRunsBySHA(sha string) []gh.CheckRun

CheckRunsBySHA returns all check runs known for a commit SHA, combining:

  • runs buffered in pendingCheckRuns (pre-linkage, SHA not yet linked to any item)
  • runs in the linked item's LinkedPR.CheckRuns (post-linkage)

Returns a deep copy; callers may mutate the result without affecting Store state. Returns nil if no runs are known for the SHA.

func (*Store) EnterRepoWorker added in v0.0.76

func (s *Store) EnterRepoWorker(repoKey string)

EnterRepoWorker marks repoKey ("owner/repo") as having an active repo-scoped worker (currently: a merge-train worker spanning a batch of items). Idempotent.

func (*Store) ExitRepoWorker added in v0.0.76

func (s *Store) ExitRepoWorker(repoKey string)

ExitRepoWorker clears the repo-scoped worker marker for repoKey. Safe to call even if no marker is set (no-op).

func (*Store) Get

func (s *Store) Get(repo string, number int) (Snapshot, error)

Get returns an immutable snapshot of the current ItemState for the given item.

On cache miss, Get calls FallbackFetcher.FetchItem (if set), applies the result as an IssueOpened mutation, and returns the new snapshot (invariant I9).

Returns ErrNotFound if:

  • the item is not in the cache AND
  • the fallback is nil OR the fallback also returns an error.

func (*Store) GetByPRKey

func (s *Store) GetByPRKey(repo string, prNum int) (string, bool)

GetByPRKey returns the item key for the issue that is closed by the given PR. Returns ("", false) if no item in the Store has a LinkedPR.Number matching prNum in the given repo. The returned key can be parsed with parseKey to extract repo and number for a subsequent Get call.

func (*Store) HasInFlightWorker added in v0.0.76

func (s *Store) HasInFlightWorker() bool

HasInFlightWorker reports whether any worker — per-item (WorkerEntered on a (Repo, Number)) or repo-scoped (EnterRepoWorker on a repo) — is currently in flight. This is the single authoritative answer to "is a worker running" consumed by the auto-upgrade idle guard: neither registry alone is sufficient, since a merge-train worker registers only the latter.

func (*Store) HasItems

func (s *Store) HasItems() bool

HasItems reports whether the Store contains at least one item. O(1) and non-allocating. Safe to call concurrently with Apply.

func (*Store) Remove

func (s *Store) Remove(repo string, number int)

Remove deletes the item identified by (repo, number) from the Store and updates the shaToKey, itemIDToKey, and prToKey indexes accordingly. No-op when the item is not present.

func (*Store) RemoveByItemID

func (s *Store) RemoveByItemID(itemID string) (repo string, number int, ok bool)

RemoveByItemID removes the item identified by its project ItemID (board-side ID). Updates shaToKey, itemIDToKey, and prToKey indexes accordingly. No-op and ok=false if the ItemID is not in the index.

func (*Store) RepoWorkerActive added in v0.0.76

func (s *Store) RepoWorkerActive(repoKey string) bool

RepoWorkerActive reports whether a repo-scoped worker is currently marked in-flight for repoKey ("owner/repo").

func (*Store) Reset

func (s *Store) Reset(items []gh.ProjectItem)

Reset atomically replaces all Store state with the items in the given slice. Existing items, indexes, and deep-fetch state are cleared. This is used by Bootstrap to ensure a clean slate (unlike Reconcile, which preserves deep state). Observers are notified outside the write lock, following the same pattern as Apply.

func (*Store) Subscribe

func (s *Store) Subscribe(o Observer) func()

Subscribe registers an observer that will be called after every successful non-no-op Apply. Returns an unsubscribe function; calling it removes the observer. The unsubscribe function is safe to call concurrently.

type StoreOption

type StoreOption func(*storeOptions)

StoreOption is a functional option for NewStore.

func WithLogger

func WithLogger(l Logger) StoreOption

WithLogger sets a diagnostic logger on the Store. The default is no-op.

type TerminalFlagSet

type TerminalFlagSet struct {
	Repo     string
	Number   int
	Terminal bool
}

TerminalFlagSet sets or clears the Terminal flag on an item. When Terminal is true, the poll loop skips deep-fetch for this item as long as its status remains in a cleanup (Done) stage. When Terminal is false, normal deep-fetch evaluation resumes.

type TokenUsage

type TokenUsage struct {
	InputTokens         int
	OutputTokens        int
	CacheCreationTokens int
	CacheReadTokens     int
	CostUSD             float64
	TurnsUsed           int
	MaxTurns            int
}

TokenUsage records Claude API token counts for a single invocation. Fields mirror engine.TokenUsage to enable zero-cost assignment in Phase 3-E.

type ValidateCompletedAtSHA added in v0.0.69

type ValidateCompletedAtSHA struct {
	Repo   string
	Number int
	SHA    string
}

ValidateCompletedAtSHA records the HEAD SHA of the linked PR at the moment stage:Validate:complete was applied. The SHA-invalidation scan uses this to detect force-pushes or external commits after Validate finished.

type ValidateCompletedAtSHACleared added in v0.0.69

type ValidateCompletedAtSHACleared struct {
	Repo   string
	Number int
}

ValidateCompletedAtSHACleared zeroes LinkedPRState.ValidateCompletedSHA after the SHA-invalidation scan fires and removes the stale completion labels.

type WorkerEntered

type WorkerEntered struct {
	Repo      string
	Number    int
	StageName string
	StartedAt time.Time
}

WorkerEntered sets a non-nil Worker placeholder immediately before the goroutine is launched. This makes snap.Worker() != nil an effective dispatch guard from the instant the goroutine is started, closing the window between goroutine launch and LocalLockAcquired (which fires only after the GitHub lock label is acquired). LocalLockAcquired overwrites the placeholder with full details.

type WorkerExited

type WorkerExited struct {
	Repo   string
	Number int
}

WorkerExited clears the Worker handle when a Claude invocation finishes.

type WorkerHandle

type WorkerHandle struct {
	PID       int
	StageName string
	StartedAt time.Time
	// LastSignAt is updated by worker heartbeats; a stale heartbeat implies the worker died.
	LastSignAt time.Time
}

WorkerHandle identifies an in-flight Claude invocation for this item.

type WorkerHeartbeat

type WorkerHeartbeat struct {
	Repo   string
	Number int
	At     time.Time
}

WorkerHeartbeat updates the LastSignAt timestamp for the active worker.

type WorkerPIDSet

type WorkerPIDSet struct {
	Repo   string
	Number int
	PID    int
}

WorkerPIDSet records the Claude subprocess PID once cmd.Start() returns. No-op if Worker is nil (e.g., WorkerExited arrived first).

Jump to

Keyboard shortcuts

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