Create a new StateManager instance.
When true, state is held only in memory and never read from or written to disk. Useful for unit tests that need isolated state without side effects. Defaults to false (normal persistent mode).
Number of times a Gist refresh replaced the in-memory state. A successful push resolves earlier un-pushed changes only if this did not advance in between (the #1443 loss-notice invariant); a long run compares it to the value at its start to learn its mutations were swapped out.
Add closed PRs to storage, deduplicating by URL. Entries with URLs that fail parseGitHubUrl are dropped before persistence (read-side filters in dashboard-data already skip them, but this prevents the bad data from reaching disk in the first place).
Re-seen URLs upgrade the stored entry's missing ledger fields in place (#1461) — see addMergedPRs for the full semantics.
Closed PRs to add (duplicates by URL upgrade in place)
count of entries added vs. dropped (invalid URL)
Track a new issue, or refresh the claim on one already tracked.
Matching uses issueRefKey (same normalization as
releaseIssue), so a re-claim through a differently cased or
suffixed URL updates the existing entry instead of adding a duplicate.
On a match only status and updatedAt are taken from issue:
updatedAt is the claim stamp releaseIssue compares against the PR's
close time (#1863), so a re-claim after the close must move it forward
or the daily run would release the new claim as if it were the old one.
The issue to track
Add merged PRs to storage, deduplicating by URL. Entries with URLs that fail parseGitHubUrl are dropped before persistence (read-side filters in dashboard-data already skip them, but this prevents the bad data from reaching disk in the first place).
Re-seen URLs are not re-added, but upgrade the stored entry in place
(#1461): outcome-ledger fields (openedAt, firstMaintainerResponseAt)
missing on the stored entry are filled from the richer incoming one.
Existing values are never overwritten, so stamps that live only on the
stored entry (commentsFetchedAt, learningsExtractedAt) and earlier
enriched writes stay intact.
Merged PRs to add (duplicates by URL upgrade in place)
count of entries added vs. dropped (invalid URL)
Add a repository to the trusted projects list. No-op if already trusted.
Repository in "owner/repo" format
Execute multiple mutations as a single batch, deferring disk I/O until the
batch completes. Nested batch() calls are flattened — only the outermost saves.
The function containing mutations to batch
Push current state to Gist (async). Call at well-defined moments (end of daily, after claim).
Remove excluded repos/orgs from trusted projects.
Repository names to exclude
Organization names to exclude
Remove a manual status override for a PR.
The PR URL
true if an override was removed, false if none existed
Tombstone the guidelines file for repo so subsequent reads return null.
Throws when not in Gist mode.
Dismiss an issue's (or merged PR's, #1861) notifications. Auto-resurfaces on new activity.
The issue or PR URL to dismiss
ISO timestamp of dismissal
OptionalheadSha?: stringFor a reviewed PR (#1857): the head SHA the PR
had when it was dismissed, recorded in config.dismissedPRHeads so the
follow-up stays hidden until the head moves. Any head recorded earlier
is replaced, or dropped when this dismiss names none.
Optionalrefresh?: booleanWhen set, an existing dismissal is re-stamped with
timestamp instead of being left alone, so a second "skip" after newer
activity moves the cutoff forward. Default keeps the original stamp.
true if newly dismissed (or re-stamped under refresh), false if
already dismissed
Mark the live claim on url finished at at (#1856): the branch is ready
to collect, and expiresAt is extended so it stays collectable (see
finishWorkClaim). Expired claims are pruned in the same write.
Without a live claim nothing is written: a prune alone would mark the
state un-pushed and raise a sync alarm for a change nobody made.
the updated claim, or undefined when no live claim exists
Gist mode: treat the current un-pushed saves as disposable, so the next refresh replaces them without raising the lost-changes staleness marker. Only for re-derivable data (the dashboard's GitHub fetch caches).
The claims still live at nowMs, in stored order.
Returns all stored closed-without-merge PRs (sorted by close date descending via addClosedPRs).
Returns the most recent close date, used as a watermark for incremental fetching.
The head SHA recorded when a reviewed PR's follow-up was dismissed (#1857), or undefined when the PR is not dismissed or was dismissed without one.
The PR URL to check
Single source of truth for gist persistence health (#1444). Surfaces
that need "is this process reliably syncing to the Gist?" (daily
warnings, dashboard banners/recovery gates) derive it from here instead
of re-combining config.persistence / isGistMode() /
isGistDegraded() at each call site.
Composes the same primitive fields those accessors expose:
configured-but-localbootstrap-degraded
(since comes from the staleness marker the degraded bootstrap seeds)degraded: null)Read the per-repo guidelines for repo (#867). Returns null when in
local mode, when no file exists, or when the file is empty (tombstoned).
Returns repos above the score threshold.
OptionalminScore: number
Minimum score (default: config.minRepoScoreThreshold)
Get the timestamp when an issue was dismissed, or undefined if not dismissed.
The issue URL to check
Latest overnight run (#1574); undefined before the first one.
Recovery details from the most recent state load, or null when the last load read the state file cleanly (or no load from disk has happened, e.g. in-memory mode). Set when loadState() fell back to a backup or fresh state because the main file was corrupt or invalid (#1371).
Returns repos below the score threshold.
OptionalmaxScore: number
Maximum score (default: config.minRepoScoreThreshold)
Returns all stored merged PRs (sorted by merge date descending via addMergedPRs).
Returns the most recent merge date, used as a watermark for incremental fetching.
The rendered morning report as last published to the Gist (#1698), so a machine other than the one that ran overnight can read it. Null in local mode or before the first publish.
Vet-time predictions awaiting a ledger join (#1824), oldest first.
Get the score record for a repository.
Repository in "owner/repo" format
Read-only score record, or undefined if not tracked
Returns repository names that have at least one merged PR.
Returns repository names with open PRs but no merged PRs yet.
Broad-phase language rotation cursor (#1630); undefined before the first search.
The durable search seen-set (#1528) — issue URLs surfaced by recent
searches, fed into oss-scout for result rotation. Returns [] when none
recorded yet.
Returns cached starred repository names.
Get the current state as a read-only snapshot.
Returns a staleness marker when the in-memory state diverged from the
canonical Gist (refresh failure or degraded bootstrap), or null when
state is current. Commands surface this via their --json warnings
envelope (#1193).
Returns aggregate contribution statistics (merge rate, PR counts, repo breakdown).
Get the status override for a PR, auto-clearing if new activity has occurred.
The PR URL
OptionalcurrentUpdatedAt: string
PR's current updatedAt timestamp for staleness check
The override if still valid, undefined otherwise
Every stored claim, expired ones included; getActiveWorkClaims is the usual read.
Gist mode: whether a saved mutation has not reached the Gist yet.
Initialize state with sensible defaults for zero-config onboarding. No-op if setup is already complete.
Whether the Gist is in degraded mode (using local cache fallback).
Whether this StateManager is backed by a Gist.
Whether per-repo guidelines (#867) are available. True iff the Gist store is initialized — in local-only mode, guidelines are unavailable and write operations would throw GuidelinesNotAvailableError.
Check if a PR is currently shelved.
The PR URL to check
true if the PR is shelved
Check if initial setup has been completed.
Returns true if starred repos cache is older than 24 hours.
List repos with non-empty guidelines stored in the Gist.
Stamp commentsFetchedAt on the merged or closed PR matching url (#867).
No-op when no PR with that URL is stored.
Stamp learningsExtractedAt on the merged or closed PR matching url (#867).
No-op when no PR with that URL is stored.
Mark setup as complete and record the completion timestamp.
Record vet-time predictions (#1824). The first prediction for an issue wins: a later re-vet (often after the user's own PR exists) must not replace the one that picked it. The oldest entries are dropped past MAX_PICK_SNAPSHOTS.
ponytail: never refreshed, so an issue vetted months before it is picked keeps its old grade. Replace snapshots older than N days if that skews calibration.
Re-fetch state from the backing Gist (if in Gist mode).
Throttled to once per 30 seconds by GistStateStore unless force is set.
Returns true if state was refreshed.
Release a claim: drop the issue from activeIssues (#1863). Called by
the daily run when the user closed their own PR for the issue unmerged.
What changes: the issue leaves the tracked set that
IssueConversationMonitor.fetchCommentedIssues excludes, so from the
next run it re-enters the commented-issue monitor like any other issue
the user has commented on, and it stops reading as in progress wherever
activeIssues is listed. Capacity is NOT affected: assessCapacity
counts open PRs only, never tracked issues.
Matching uses issueRefKey (case-insensitive owner/repo, trailing
slash and fragment ignored), falling back to the exact string for a URL
that does not parse. The removal cannot be undone, so with
claimedBefore set the entry is kept when its updatedAt is at or after
that instant: updatedAt is the claim stamp the claim command writes
(createdAt there is the issue's GitHub creation date), so a claim made
after the PR closed is a deliberate re-claim that must survive.
Unparseable timestamps keep the entry for the same reason.
URL of the issue to release
OptionalclaimedBefore: string
ISO instant (the PR's close time); only a claim recorded strictly before it is released
Drop claims: the one on url, or every claim held by session (optionally
only of one kind). Expired claims are pruned in the same write; when
nothing matches, nothing is written (see finishWorkClaim).
the URLs of the live claims released
Re-read state from disk if the file has been modified since the last load/save. Returns true if state was reloaded, false if unchanged or in-memory mode.
Move a transferred repo's stored PRs and score to its new name. See repoScoring.renameRepo.
Run fn as an in-flight mutation: non-forced refreshFromGist() calls
are skipped until it settles so a concurrent refresh cannot swap
this.state out from under it (audit 2026-10-30 M2).
Deliberately a counter, not a serializing promise chain: parallel MCP
tool bodies from one host message may depend on each other through the
host, so queuing them would deadlock. The gap that leaves (a sibling's
forced conflict-recovery refresh swaps the state object mid-body) is
closed by maybeCheckpoint re-applying every mutation right before its
push; bodies that cannot re-apply (daily) watch refreshGeneration.
Persist the current state to disk, creating a timestamped backup of the previous
state file first. In in-memory mode, only updates lastRunAt without any file I/O.
In Gist mode, writes to a local cache file (not the main state file) so the Gist
remains the source of truth. Use checkpoint() to push state to the Gist.
Local-file mode uses optimistic compare-and-swap: if another process has
written state.json since we last loaded/saved, saveState throws
ConcurrencyError. See issue #1030.
ConcurrencyError (local file mode only) when the on-disk state
was modified externally between this StateManager's last load/save
and now. Callers that tolerate silent merge can set
{ allowReloadAndLoseMutation: true } to downgrade the error into a
warning — but note that doing so abandons any in-memory mutation
made since the last load. Fail-loud (the default) is preferred.
Persist per-repo guidelines for repo. Throws when not in Gist mode or
when content exceeds the byte budget.
Store the latest daily digest and update the digest timestamp.
The daily digest to store
Replace the overnight record (#1574): a new run, or a run plus a recorded branch.
Persist the timestamp of the most recent strategy snapshot embedded
in a daily run output (#1270). Called from the daily pipeline after
computeStrategy() succeeds; the cadence gate in
shouldComputeStrategy reads this on the next run.
Overwrite the first-response lookup on the merged or closed PR matching
url. Unlike addMergedPRs/addClosedPRs, which only fill missing fields,
this replaces firstMaintainerResponseAt (clearing it when at is
undefined) so ledger-backfill can recompute values recorded under an older
maintainer rule. No-op when no PR with that URL is stored.
Update the local repository cache.
Local repository cache mapping repo names to paths
Update monthly closed PR counts for dashboard display.
Monthly closed PR counts keyed by YYYY-MM
Update monthly merged PR counts for dashboard display.
Monthly merged PR counts keyed by YYYY-MM
Update monthly opened PR counts for dashboard display.
Monthly opened PR counts keyed by YYYY-MM
Stage the rendered morning report for the next Gist push (#1698). Freeform document, last-write-wins like guidelines. No-op in local mode: the file on disk is the only copy there, which is fine on one machine.
Persist the cursor scout advanced during a search (#1630).
Replace the search seen-set (#1528). Callers merge the just-surfaced URLs and cull stale entries before calling this; the manager only persists.
Update the cached starred repositories and timestamp.
Repository names in "owner/repo" format
Shelve a PR URL, hiding it from daily digest and capacity.
The PR URL to shelve
true if newly shelved, false if already shelved
Attempt PR count reconciliation, logging a warning on failure. Called after every state load and ledger addition, and by daily after it overwrites counts from the pagination-capped search (#1897).
Restore a dismissed issue to notifications. A recorded dismiss head (#1857) goes with it.
The issue URL to undismiss
true if undismissed, false if not currently dismissed
Unshelve a PR URL, restoring it to daily digest.
The PR URL to unshelve
true if removed from shelf, false if not shelved
Merge partial config updates into the current configuration.
Partial config object to merge
Update scoring data for a repository.
Repository in "owner/repo" format
Partial score fields to merge
Record a claim, replacing any claim on the same item (#1856). Expired claims are pruned in the same write. An absolute set: re-running it on a refreshed Gist baseline keeps every other machine's claims.
true when a live claim on the item was replaced
StaticcreateAsync factory that creates a StateManager backed by a GitHub Gist.
The regular constructor is synchronous (for backwards-compat), but Gist bootstrapping requires network calls, so this factory is async.
GitHub personal access token with gist scope
Core module exports Re-exports all core functionality for convenient imports