@oss-autopilot/core - v3.7.0
    Preparing search index...

    Class GistStateStore

    Gist-backed state store with in-memory file cache and local write-through.

    Index

    Constructors

    Properties

    cachedFiles: Map<string, string> = ...
    dirtyFiles: Set<string> = ...
    lastRefreshError: Error | null = null

    Most recent error from a refreshFromGist() attempt, or null when the last attempt succeeded or was skipped by the throttle. Lets callers (StateManager) distinguish "throttled, nothing new to see" from "fetch failed, you're now operating on stale state" without changing the existing boolean return contract (#1193).

    Methods

    • Bootstrap the Gist store: locate or create the backing Gist, populate the in-memory cache, and write the local cache file.

      Returns Promise<BootstrapResult>

    • Bootstrap with migration from an existing local state. If a Gist already exists (found via local ID or search), uses it — no migration needed. If no Gist exists, creates one seeded with the provided existingState instead of a fresh state.

      Parameters

      • existingState: {
            activeIssues: {
                createdAt: string;
                id: number;
                labels: string[];
                number: number;
                repo: string;
                status: "candidate" | "claimed" | "in_progress" | "pr_submitted";
                title: string;
                updatedAt: string;
                url: string;
                vetted: boolean;
                vettingResult?: {
                    checks: {
                        clearRequirements: boolean;
                        contributionGuidelinesFound: boolean;
                        noExistingPR: boolean;
                        notClaimed: boolean;
                        projectActive: boolean;
                    };
                    contributionGuidelines?: {
                        branchNamingConvention?: string;
                        claRequired?: boolean;
                        commitMessageFormat?: string;
                        formatter?: string;
                        issueClaimProcess?: string;
                        linter?: string;
                        prTitleFormat?: string;
                        rawContent?: string;
                        requiredChecks?: string[];
                        reviewProcess?: string;
                        styleGuideUrl?: string;
                        testCoverageRequired?: boolean;
                        testFileNaming?: string;
                        testFramework?: string;
                    };
                    notes: string[];
                    passedAllChecks: boolean;
                };
            }[];
            analyzedIssueConversations?: {
                analyzedAt: string;
                repo: string;
                url: string;
            }[];
            closedPRs?: {
                closedAt: string;
                commentsFetchedAt?: string;
                learningsExtractedAt?: string;
                title: string;
                url: string;
            }[];
            config: {
                aiPolicyBlocklist: string[];
                approachingDormantDays: number;
                autoFormatBeforePush: boolean;
                diffTool: "inline"
                | "custom"
                | "sourcetree"
                | "vscode";
                diffToolCustomCommand?: string;
                dismissedIssues: Record<string, string>;
                dormantThresholdDays: number;
                excludeOrgs?: string[];
                excludeRepos: string[];
                githubUsername: string;
                healthCheckFreshnessMinutes: number;
                includeDocIssues: boolean;
                issueListPath?: string;
                labels: string[];
                languages: string[];
                localRepoScanPaths?: string[];
                maxActivePRs: number;
                maxIssueAgeDays: number;
                minRepoScoreThreshold: number;
                minStars: number;
                persistence: "local" | "gist";
                preferredOrgs: string[];
                projectCategories: (
                    | "nonprofit"
                    | "devtools"
                    | "infrastructure"
                    | "web-frameworks"
                    | "data-ml"
                    | "education"
                )[];
                reviewMaxPasses?: number;
                scope?: ("advanced" | "beginner" | "intermediate")[];
                setupComplete: boolean;
                setupCompletedAt?: string;
                shelvedPRUrls: string[];
                skippedIssuesPath?: string;
                slmTriageHost: string;
                slmTriageModel: string;
                squashByDefault: boolean | "ask";
                starredRepos: string[];
                starredReposLastFetched?: string;
                statusOverrides?: Record<
                    string,
                    {
                        lastActivityAt: string;
                        setAt: string;
                        status: "needs_addressing"
                        | "waiting_on_maintainer";
                    },
                >;
                trustedProjects: string[];
            };
            gistId?: string;
            lastDigest?: {
                autoUnshelvedPRs: {
                    daysSinceActivity: number;
                    number: number;
                    repo: string;
                    status: "needs_addressing"
                    | "waiting_on_maintainer";
                    title: string;
                    url: string;
                }[];
                generatedAt: string;
                needsAddressingPRs: any[];
                openPRs: any[];
                recentlyClosedPRs: {
                    closedAt: string;
                    closedBy?: string;
                    number: number;
                    repo: string;
                    title: string;
                    url: string;
                }[];
                recentlyMergedPRs: {
                    mergedAt: string;
                    number: number;
                    repo: string;
                    title: string;
                    url: string;
                }[];
                shelvedPRs: {
                    daysSinceActivity: number;
                    number: number;
                    repo: string;
                    status: "needs_addressing"
                    | "waiting_on_maintainer";
                    title: string;
                    url: string;
                }[];
                summary: {
                    mergeRate: number;
                    totalActivePRs: number;
                    totalMergedAllTime: number;
                    totalNeedingAttention: number;
                };
                waitingOnMaintainerPRs: any[];
            };
            lastDigestAt?: string;
            lastRunAt: string;
            lastStrategyAt?: string;
            localRepoCache?: {
                cachedAt: string;
                repos: Record<
                    string,
                    { currentBranch: string
                    | null; exists: boolean; path: string },
                >;
                scanPaths: string[];
            };
            mergedPRs?: {
                commentsFetchedAt?: string;
                learningsExtractedAt?: string;
                mergedAt: string;
                title: string;
                url: string;
            }[];
            monthlyClosedCounts?: Record<string, number>;
            monthlyMergedCounts?: Record<string, number>;
            monthlyOpenedCounts?: Record<string, number>;
            prFollowUpHistory?: Record<
                string,
                {
                    draftPath?: string;
                    tier: "light_check_in"
                    | "direct_check_in"
                    | "final_check_in";
                    timestamp: string;
                }[],
            >;
            repoScores: Record<
                string,
                {
                    avgResponseDays: number
                    | null;
                    closedWithoutMergeCount: number;
                    language?: string | null;
                    lastEvaluatedAt: string;
                    lastMergedAt?: string;
                    mergedPRCount: number;
                    repo: string;
                    score: number;
                    signals: {
                        hasActiveMaintainers: boolean;
                        hasHostileComments: boolean;
                        isResponsive: boolean;
                    };
                    stargazersCount?: number;
                },
            >;
            version: 4;
            workflowState?: {
                branchName?: string;
                completedSteps: string[];
                currentStep: string;
                issueContext?: { title: string; url: string };
                lastUpdatedAt: string;
                stepData: Record<string, unknown>;
                workflowName: "draft-first" | "work-through-issues" | "pre-commit-review";
            };
        }
        • activeIssues: {
              createdAt: string;
              id: number;
              labels: string[];
              number: number;
              repo: string;
              status: "candidate" | "claimed" | "in_progress" | "pr_submitted";
              title: string;
              updatedAt: string;
              url: string;
              vetted: boolean;
              vettingResult?: {
                  checks: {
                      clearRequirements: boolean;
                      contributionGuidelinesFound: boolean;
                      noExistingPR: boolean;
                      notClaimed: boolean;
                      projectActive: boolean;
                  };
                  contributionGuidelines?: {
                      branchNamingConvention?: string;
                      claRequired?: boolean;
                      commitMessageFormat?: string;
                      formatter?: string;
                      issueClaimProcess?: string;
                      linter?: string;
                      prTitleFormat?: string;
                      rawContent?: string;
                      requiredChecks?: string[];
                      reviewProcess?: string;
                      styleGuideUrl?: string;
                      testCoverageRequired?: boolean;
                      testFileNaming?: string;
                      testFramework?: string;
                  };
                  notes: string[];
                  passedAllChecks: boolean;
              };
          }[]
        • OptionalanalyzedIssueConversations?: { analyzedAt: string; repo: string; url: string }[]
        • OptionalclosedPRs?: {
              closedAt: string;
              commentsFetchedAt?: string;
              learningsExtractedAt?: string;
              title: string;
              url: string;
          }[]
        • config: {
              aiPolicyBlocklist: string[];
              approachingDormantDays: number;
              autoFormatBeforePush: boolean;
              diffTool: "inline" | "custom" | "sourcetree" | "vscode";
              diffToolCustomCommand?: string;
              dismissedIssues: Record<string, string>;
              dormantThresholdDays: number;
              excludeOrgs?: string[];
              excludeRepos: string[];
              githubUsername: string;
              healthCheckFreshnessMinutes: number;
              includeDocIssues: boolean;
              issueListPath?: string;
              labels: string[];
              languages: string[];
              localRepoScanPaths?: string[];
              maxActivePRs: number;
              maxIssueAgeDays: number;
              minRepoScoreThreshold: number;
              minStars: number;
              persistence: "local" | "gist";
              preferredOrgs: string[];
              projectCategories: (
                  | "nonprofit"
                  | "devtools"
                  | "infrastructure"
                  | "web-frameworks"
                  | "data-ml"
                  | "education"
              )[];
              reviewMaxPasses?: number;
              scope?: ("advanced" | "beginner" | "intermediate")[];
              setupComplete: boolean;
              setupCompletedAt?: string;
              shelvedPRUrls: string[];
              skippedIssuesPath?: string;
              slmTriageHost: string;
              slmTriageModel: string;
              squashByDefault: boolean | "ask";
              starredRepos: string[];
              starredReposLastFetched?: string;
              statusOverrides?: Record<
                  string,
                  {
                      lastActivityAt: string;
                      setAt: string;
                      status: "needs_addressing"
                      | "waiting_on_maintainer";
                  },
              >;
              trustedProjects: string[];
          }
          • aiPolicyBlocklist: string[]
          • approachingDormantDays: number
          • autoFormatBeforePush: boolean

            Opt-in gate for the auto-format-before-push hook (#1045). Default false: the hook does nothing on every push unless the user explicitly enables it.

          • diffTool: "inline" | "custom" | "sourcetree" | "vscode"
          • OptionaldiffToolCustomCommand?: string
          • dismissedIssues: Record<string, string>
          • dormantThresholdDays: number
          • OptionalexcludeOrgs?: string[]
          • excludeRepos: string[]
          • githubUsername: string
          • healthCheckFreshnessMinutes: number

            Threshold (in minutes) for the SessionStart PR health one-liner (#1255). The cached digest only refreshes when the user runs /oss; SessionStart fires every session. Without a freshness gate the line drifts arbitrarily stale between runs. When the cache is older than this many minutes (and not yet 7 days old, which keeps the existing catch-up nudge), the line is suppressed entirely. Default 30 minutes.

          • includeDocIssues: boolean
          • OptionalissueListPath?: string
          • labels: string[]
          • languages: string[]
          • OptionallocalRepoScanPaths?: string[]
          • maxActivePRs: number
          • maxIssueAgeDays: number
          • minRepoScoreThreshold: number
          • minStars: number
          • persistence: "local" | "gist"
          • preferredOrgs: string[]
          • projectCategories: (
                | "nonprofit"
                | "devtools"
                | "infrastructure"
                | "web-frameworks"
                | "data-ml"
                | "education"
            )[]
          • OptionalreviewMaxPasses?: number

            Convergence cap for the multi-agent review loop in workflows/dispatch-review.md (#1275). When unset, the workflow falls back to per-mode defaults (5 for diff, 3 for plan). Lower values shorten the loop at the cost of skipping later iterations if findings persist; higher values give the loop more chances to converge before bailing. Optional — leave unset to use the defaults.

          • Optionalscope?: ("advanced" | "beginner" | "intermediate")[]
          • setupComplete: boolean
          • OptionalsetupCompletedAt?: string
          • shelvedPRUrls: string[]
          • OptionalskippedIssuesPath?: string
          • slmTriageHost: string

            Optional Ollama HTTP host override. Defaults to http://127.0.0.1:11434 when empty. Useful when Ollama runs on a different machine on the local network.

          • slmTriageModel: string

            Optional Ollama model for SLM pre-triage during issue vetting (#1122). Empty disables the feature. Recommended: gemma4:e4b (default for capable hardware), gemma4:e2b or qwen3:1.7b for low-RAM machines. Threaded through to scout via the bridge in scout-bridge.ts.

          • squashByDefault: boolean | "ask"
          • starredRepos: string[]
          • OptionalstarredReposLastFetched?: string
          • OptionalstatusOverrides?: Record<
                string,
                {
                    lastActivityAt: string;
                    setAt: string;
                    status: "needs_addressing"
                    | "waiting_on_maintainer";
                },
            >
          • trustedProjects: string[]
        • OptionalgistId?: string
        • OptionallastDigest?: {
              autoUnshelvedPRs: {
                  daysSinceActivity: number;
                  number: number;
                  repo: string;
                  status: "needs_addressing" | "waiting_on_maintainer";
                  title: string;
                  url: string;
              }[];
              generatedAt: string;
              needsAddressingPRs: any[];
              openPRs: any[];
              recentlyClosedPRs: {
                  closedAt: string;
                  closedBy?: string;
                  number: number;
                  repo: string;
                  title: string;
                  url: string;
              }[];
              recentlyMergedPRs: {
                  mergedAt: string;
                  number: number;
                  repo: string;
                  title: string;
                  url: string;
              }[];
              shelvedPRs: {
                  daysSinceActivity: number;
                  number: number;
                  repo: string;
                  status: "needs_addressing"
                  | "waiting_on_maintainer";
                  title: string;
                  url: string;
              }[];
              summary: {
                  mergeRate: number;
                  totalActivePRs: number;
                  totalMergedAllTime: number;
                  totalNeedingAttention: number;
              };
              waitingOnMaintainerPRs: any[];
          }
        • OptionallastDigestAt?: string
        • lastRunAt: string
        • OptionallastStrategyAt?: string

          ISO timestamp of the most recent computeStrategy invocation embedded in a daily run output (#1270). The cadence gate in daily.ts consults this — strategy snapshots fire every 30 days OR when 5+ PRs have merged since the last snapshot, whichever comes first. Below STRATEGY_MIN_PRS merged PRs the gate stays silent regardless of cadence.

        • OptionallocalRepoCache?: {
              cachedAt: string;
              repos: Record<
                  string,
                  { currentBranch: string
                  | null; exists: boolean; path: string },
              >;
              scanPaths: string[];
          }
        • OptionalmergedPRs?: {
              commentsFetchedAt?: string;
              learningsExtractedAt?: string;
              mergedAt: string;
              title: string;
              url: string;
          }[]
        • OptionalmonthlyClosedCounts?: Record<string, number>
        • OptionalmonthlyMergedCounts?: Record<string, number>
        • OptionalmonthlyOpenedCounts?: Record<string, number>
        • OptionalprFollowUpHistory?: Record<
              string,
              {
                  draftPath?: string;
                  tier: "light_check_in"
                  | "direct_check_in"
                  | "final_check_in";
                  timestamp: string;
              }[],
          >

          Per-PR follow-up history (#1277). Keyed by PR URL. Each entry records a tier-bucketed follow-up that the user drafted (and presumably posted via draft-review-post). The dormant-pr workflow reads this before drafting to enforce the one-follow-up-per-timeframe rule documented in skills/pr-etiquette/SKILL.md.

        • repoScores: Record<
              string,
              {
                  avgResponseDays: number
                  | null;
                  closedWithoutMergeCount: number;
                  language?: string | null;
                  lastEvaluatedAt: string;
                  lastMergedAt?: string;
                  mergedPRCount: number;
                  repo: string;
                  score: number;
                  signals: {
                      hasActiveMaintainers: boolean;
                      hasHostileComments: boolean;
                      isResponsive: boolean;
                  };
                  stargazersCount?: number;
              },
          >
        • version: 4
        • OptionalworkflowState?: {
              branchName?: string;
              completedSteps: string[];
              currentStep: string;
              issueContext?: { title: string; url: string };
              lastUpdatedAt: string;
              stepData: Record<string, unknown>;
              workflowName: "draft-first" | "work-through-issues" | "pre-commit-review";
          }

          Pause-point snapshot for resumable workflows (#1280). Set when the user picks "Done for now" mid-workflow; cleared when the workflow completes or the user explicitly discards. The router reads this on every /oss invocation and offers Resume / Restart / Discard when present.

      Returns Promise<BootstrapResult & { migrated: boolean }>

      BootstrapResult extended with migrated: true if a new Gist was created from local state

    • Read a freeform document from the in-memory cache. Returns null if the file has not been loaded (or does not exist in the Gist). Synchronous — all Gist contents are loaded into memory at bootstrap.

      Parameters

      • filename: string

      Returns string | null

    • Return the resolved Gist ID (available after bootstrap).

      Returns string | null

    • Return all filenames in the in-memory cache whose names start with prefix. Useful for listing all guidelines files (e.g. prefix guidelines--).

      Parameters

      • prefix: string

      Returns string[]

    • Mark a file as dirty so it will be included in the next push() call.

      Parameters

      • filename: string

      Returns void

    • Push all dirty files to the backing Gist.

      Behavior:

      • When an ETag is available from the previous fetch, sends If-Match so a 412 surfaces if another machine pushed since we last fetched.
      • On 412, attempts a single merge: re-fetches the Gist (refreshing the ETag), re-applies the in-memory dirty content on top of the remote, and pushes once more. If the second push also 412s, throws GistConcurrencyError — the caller can decide whether to refreshFromGist + retry.
      • On any other error, retries once (existing behavior). If the retry also fails, returns false.

      Returns true on success (or when there is nothing to push). Returns false if a non-conflict push fails on both attempts. Throws GistConcurrencyError on unresolvable conflicts (#1115). Throws if the Gist ID has not been resolved yet (bootstrap not called).

      Returns Promise<boolean>

    • Re-fetch the Gist and update the in-memory cache. Throttled to at most once per 30 seconds.

      Returns a discriminated union so callers can tell apart the four outcomes that previously collapsed into a single boolean (#1209 L9):

      • { status: 'refreshed' } — fresh data loaded successfully.
      • { status: 'no-gist' } — store not in Gist mode (e.g. degraded).
      • { status: 'throttled', sinceLastMs } — within the 30s throttle.
      • { status: 'error', error } — fetch attempt failed.

      Returns Promise<RefreshResult>

    • Write a freeform document into the in-memory cache and mark it dirty so it will be included in the next push() call.

      Parameters

      • filename: string
      • content: string

      Returns void

    • Stage new state JSON for the next push(). Updates the in-memory cache for state.json and marks it dirty.

      Parameters

      • stateJson: string

      Returns void