@oss-autopilot/core 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,14 +32,6 @@ export declare const IssueScopeSchema: z.ZodEnum<{
32
32
  beginner: "beginner";
33
33
  intermediate: "intermediate";
34
34
  }>;
35
- export declare const StateEventTypeSchema: z.ZodEnum<{
36
- pr_tracked: "pr_tracked";
37
- pr_merged: "pr_merged";
38
- pr_closed: "pr_closed";
39
- pr_dormant: "pr_dormant";
40
- daily_check: "daily_check";
41
- comment_posted: "comment_posted";
42
- }>;
43
35
  export declare const RepoSignalsSchema: z.ZodObject<{
44
36
  hasActiveMaintainers: z.ZodBoolean;
45
37
  isResponsive: z.ZodBoolean;
@@ -61,28 +53,22 @@ export declare const RepoScoreSchema: z.ZodObject<{
61
53
  stargazersCount: z.ZodOptional<z.ZodNumber>;
62
54
  language: z.ZodOptional<z.ZodNullable<z.ZodString>>;
63
55
  }, z.core.$strip>;
64
- export declare const StateEventSchema: z.ZodObject<{
65
- id: z.ZodString;
66
- type: z.ZodEnum<{
67
- pr_tracked: "pr_tracked";
68
- pr_merged: "pr_merged";
69
- pr_closed: "pr_closed";
70
- pr_dormant: "pr_dormant";
71
- daily_check: "daily_check";
72
- comment_posted: "comment_posted";
73
- }>;
74
- at: z.ZodString;
75
- data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
76
- }, z.core.$strip>;
77
56
  export declare const StoredMergedPRSchema: z.ZodObject<{
78
57
  url: z.ZodString;
79
58
  title: z.ZodString;
80
59
  mergedAt: z.ZodString;
60
+ learningsExtractedAt: z.ZodOptional<z.ZodString>;
81
61
  }, z.core.$strip>;
82
62
  export declare const StoredClosedPRSchema: z.ZodObject<{
83
63
  url: z.ZodString;
84
64
  title: z.ZodString;
85
65
  closedAt: z.ZodString;
66
+ learningsExtractedAt: z.ZodOptional<z.ZodString>;
67
+ }, z.core.$strip>;
68
+ export declare const AnalyzedIssueConversationSchema: z.ZodObject<{
69
+ url: z.ZodString;
70
+ repo: z.ZodString;
71
+ analyzedAt: z.ZodString;
86
72
  }, z.core.$strip>;
87
73
  export declare const ContributionGuidelinesSchema: z.ZodObject<{
88
74
  branchNamingConvention: z.ZodOptional<z.ZodString>;
@@ -209,10 +195,8 @@ export declare const AgentConfigSchema: z.ZodObject<{
209
195
  trustedProjects: z.ZodDefault<z.ZodArray<z.ZodString>>;
210
196
  githubUsername: z.ZodDefault<z.ZodString>;
211
197
  minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
212
- scoreThreshold: z.ZodDefault<z.ZodNumber>;
213
198
  starredRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
214
199
  starredReposLastFetched: z.ZodOptional<z.ZodString>;
215
- showHealthCheck: z.ZodOptional<z.ZodBoolean>;
216
200
  squashByDefault: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ask">]>>;
217
201
  localRepoScanPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
218
202
  minStars: z.ZodDefault<z.ZodNumber>;
@@ -229,6 +213,7 @@ export declare const AgentConfigSchema: z.ZodObject<{
229
213
  lastActivityAt: z.ZodString;
230
214
  }, z.core.$strip>>>;
231
215
  issueListPath: z.ZodOptional<z.ZodString>;
216
+ skippedIssuesPath: z.ZodOptional<z.ZodString>;
232
217
  projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
233
218
  nonprofit: "nonprofit";
234
219
  devtools: "devtools";
@@ -319,7 +304,8 @@ export declare const DailyDigestSchema: z.ZodObject<{
319
304
  }, z.core.$strip>;
320
305
  }, z.core.$strip>;
321
306
  export declare const AgentStateSchema: z.ZodObject<{
322
- version: z.ZodLiteral<2>;
307
+ version: z.ZodLiteral<3>;
308
+ gistId: z.ZodOptional<z.ZodString>;
323
309
  repoScores: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
324
310
  repo: z.ZodString;
325
311
  score: z.ZodNumber;
@@ -355,10 +341,8 @@ export declare const AgentStateSchema: z.ZodObject<{
355
341
  trustedProjects: z.ZodDefault<z.ZodArray<z.ZodString>>;
356
342
  githubUsername: z.ZodDefault<z.ZodString>;
357
343
  minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
358
- scoreThreshold: z.ZodDefault<z.ZodNumber>;
359
344
  starredRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
360
345
  starredReposLastFetched: z.ZodOptional<z.ZodString>;
361
- showHealthCheck: z.ZodOptional<z.ZodBoolean>;
362
346
  squashByDefault: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"ask">]>>;
363
347
  localRepoScanPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
364
348
  minStars: z.ZodDefault<z.ZodNumber>;
@@ -375,6 +359,7 @@ export declare const AgentStateSchema: z.ZodObject<{
375
359
  lastActivityAt: z.ZodString;
376
360
  }, z.core.$strip>>>;
377
361
  issueListPath: z.ZodOptional<z.ZodString>;
362
+ skippedIssuesPath: z.ZodOptional<z.ZodString>;
378
363
  projectCategories: z.ZodDefault<z.ZodArray<z.ZodEnum<{
379
364
  nonprofit: "nonprofit";
380
365
  devtools: "devtools";
@@ -385,19 +370,6 @@ export declare const AgentStateSchema: z.ZodObject<{
385
370
  }>>>;
386
371
  preferredOrgs: z.ZodDefault<z.ZodArray<z.ZodString>>;
387
372
  }, z.core.$strip>>;
388
- events: z.ZodDefault<z.ZodArray<z.ZodObject<{
389
- id: z.ZodString;
390
- type: z.ZodEnum<{
391
- pr_tracked: "pr_tracked";
392
- pr_merged: "pr_merged";
393
- pr_closed: "pr_closed";
394
- pr_dormant: "pr_dormant";
395
- daily_check: "daily_check";
396
- comment_posted: "comment_posted";
397
- }>;
398
- at: z.ZodString;
399
- data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
400
- }, z.core.$strip>>>;
401
373
  lastRunAt: z.ZodDefault<z.ZodString>;
402
374
  lastDigestAt: z.ZodOptional<z.ZodString>;
403
375
  lastDigest: z.ZodOptional<z.ZodObject<{
@@ -452,7 +424,6 @@ export declare const AgentStateSchema: z.ZodObject<{
452
424
  monthlyMergedCounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
453
425
  monthlyClosedCounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
454
426
  monthlyOpenedCounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
455
- dailyActivityCounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
456
427
  localRepoCache: z.ZodOptional<z.ZodObject<{
457
428
  repos: z.ZodRecord<z.ZodString, z.ZodObject<{
458
429
  path: z.ZodString;
@@ -466,11 +437,18 @@ export declare const AgentStateSchema: z.ZodObject<{
466
437
  url: z.ZodString;
467
438
  title: z.ZodString;
468
439
  mergedAt: z.ZodString;
440
+ learningsExtractedAt: z.ZodOptional<z.ZodString>;
469
441
  }, z.core.$strip>>>;
470
442
  closedPRs: z.ZodOptional<z.ZodArray<z.ZodObject<{
471
443
  url: z.ZodString;
472
444
  title: z.ZodString;
473
445
  closedAt: z.ZodString;
446
+ learningsExtractedAt: z.ZodOptional<z.ZodString>;
447
+ }, z.core.$strip>>>;
448
+ analyzedIssueConversations: z.ZodOptional<z.ZodArray<z.ZodObject<{
449
+ url: z.ZodString;
450
+ repo: z.ZodString;
451
+ analyzedAt: z.ZodString;
474
452
  }, z.core.$strip>>>;
475
453
  activeIssues: z.ZodDefault<z.ZodArray<z.ZodObject<{
476
454
  id: z.ZodNumber;
@@ -521,12 +499,11 @@ export type IssueStatus = z.infer<typeof IssueStatusSchema>;
521
499
  export type FetchedPRStatus = z.infer<typeof FetchedPRStatusSchema>;
522
500
  export type ProjectCategory = z.infer<typeof ProjectCategorySchema>;
523
501
  export type IssueScope = z.infer<typeof IssueScopeSchema>;
524
- export type StateEventType = z.infer<typeof StateEventTypeSchema>;
525
502
  export type RepoSignals = z.infer<typeof RepoSignalsSchema>;
526
503
  export type RepoScore = z.infer<typeof RepoScoreSchema>;
527
- export type StateEvent = z.infer<typeof StateEventSchema>;
528
504
  export type StoredMergedPR = z.infer<typeof StoredMergedPRSchema>;
529
505
  export type StoredClosedPR = z.infer<typeof StoredClosedPRSchema>;
506
+ export type AnalyzedIssueConversation = z.infer<typeof AnalyzedIssueConversationSchema>;
530
507
  export type ContributionGuidelines = z.infer<typeof ContributionGuidelinesSchema>;
531
508
  export type IssueVettingResult = z.infer<typeof IssueVettingResultSchema>;
532
509
  export type TrackedIssue = z.infer<typeof TrackedIssueSchema>;
@@ -21,14 +21,6 @@ export const ProjectCategorySchema = z.enum([
21
21
  'education',
22
22
  ]);
23
23
  export const IssueScopeSchema = z.enum(['beginner', 'intermediate', 'advanced']);
24
- export const StateEventTypeSchema = z.enum([
25
- 'pr_tracked',
26
- 'pr_merged',
27
- 'pr_closed',
28
- 'pr_dormant',
29
- 'daily_check',
30
- 'comment_posted',
31
- ]);
32
24
  // ── 2. Leaf schemas ──────────────────────────────────────────────────
33
25
  export const RepoSignalsSchema = z.object({
34
26
  hasActiveMaintainers: z.boolean(),
@@ -47,21 +39,22 @@ export const RepoScoreSchema = z.object({
47
39
  stargazersCount: z.number().optional(),
48
40
  language: z.string().nullable().optional(),
49
41
  });
50
- export const StateEventSchema = z.object({
51
- id: z.string(),
52
- type: StateEventTypeSchema,
53
- at: z.string(),
54
- data: z.record(z.string(), z.unknown()),
55
- });
56
42
  export const StoredMergedPRSchema = z.object({
57
43
  url: z.string(),
58
44
  title: z.string(),
59
45
  mergedAt: z.string(),
46
+ learningsExtractedAt: z.string().optional(),
60
47
  });
61
48
  export const StoredClosedPRSchema = z.object({
62
49
  url: z.string(),
63
50
  title: z.string(),
64
51
  closedAt: z.string(),
52
+ learningsExtractedAt: z.string().optional(),
53
+ });
54
+ export const AnalyzedIssueConversationSchema = z.object({
55
+ url: z.string(),
56
+ repo: z.string(),
57
+ analyzedAt: z.string(),
65
58
  });
66
59
  // ── 3. Contribution schemas ──────────────────────────────────────────
67
60
  export const ContributionGuidelinesSchema = z.object({
@@ -135,10 +128,8 @@ export const AgentConfigSchema = z.object({
135
128
  trustedProjects: z.array(z.string()).default([]),
136
129
  githubUsername: z.string().default(''),
137
130
  minRepoScoreThreshold: z.number().default(4),
138
- scoreThreshold: z.number().int().min(1).max(10).default(6),
139
131
  starredRepos: z.array(z.string()).default([]),
140
132
  starredReposLastFetched: z.string().optional(),
141
- showHealthCheck: z.boolean().optional(),
142
133
  squashByDefault: z.union([z.boolean(), z.literal('ask')]).default(true),
143
134
  localRepoScanPaths: z.array(z.string()).optional(),
144
135
  minStars: z.number().default(50),
@@ -148,6 +139,7 @@ export const AgentConfigSchema = z.object({
148
139
  dismissedIssues: z.record(z.string(), z.string()).default({}),
149
140
  statusOverrides: z.record(z.string(), StatusOverrideSchema).optional(),
150
141
  issueListPath: z.string().optional(),
142
+ skippedIssuesPath: z.string().optional(),
151
143
  projectCategories: z.array(ProjectCategorySchema).default([]),
152
144
  preferredOrgs: z.array(z.string()).default([]),
153
145
  });
@@ -197,19 +189,19 @@ export const DailyDigestSchema = z.object({
197
189
  });
198
190
  // ── 8. Root schema ───────────────────────────────────────────────────
199
191
  export const AgentStateSchema = z.object({
200
- version: z.literal(2),
192
+ version: z.literal(3),
193
+ gistId: z.string().optional(),
201
194
  repoScores: z.record(z.string(), RepoScoreSchema).default({}),
202
195
  config: AgentConfigSchema.default(() => AgentConfigSchema.parse({})),
203
- events: z.array(StateEventSchema).default([]),
204
196
  lastRunAt: z.string().default(() => new Date().toISOString()),
205
197
  lastDigestAt: z.string().optional(),
206
198
  lastDigest: DailyDigestSchema.optional(),
207
199
  monthlyMergedCounts: z.record(z.string(), z.number()).optional(),
208
200
  monthlyClosedCounts: z.record(z.string(), z.number()).optional(),
209
201
  monthlyOpenedCounts: z.record(z.string(), z.number()).optional(),
210
- dailyActivityCounts: z.record(z.string(), z.number()).optional(),
211
202
  localRepoCache: LocalRepoCacheSchema.optional(),
212
203
  mergedPRs: z.array(StoredMergedPRSchema).optional(),
213
204
  closedPRs: z.array(StoredClosedPRSchema).optional(),
205
+ analyzedIssueConversations: z.array(AnalyzedIssueConversationSchema).optional(),
214
206
  activeIssues: z.array(TrackedIssueSchema).default([]),
215
207
  });
@@ -3,23 +3,26 @@
3
3
  * Thin coordinator that delegates persistence to state-persistence.ts
4
4
  * and scoring logic to repo-score-manager.ts.
5
5
  */
6
- import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, StatusOverride, FetchedPRStatus, StoredMergedPR, StoredClosedPR } from './types.js';
6
+ import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, DailyDigest, LocalRepoCache, StatusOverride, FetchedPRStatus, StoredMergedPR, StoredClosedPR } from './types.js';
7
7
  import type { Stats } from './repo-score-manager.js';
8
+ import { GistStateStore } from './gist-state-store.js';
8
9
  export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
9
10
  export type { Stats } from './repo-score-manager.js';
10
11
  /**
11
12
  * Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
12
13
  *
13
14
  * Delegates file I/O to state-persistence.ts and scoring logic to repo-score-manager.ts.
14
- * Retains lightweight CRUD operations for config, events, issues, shelving, dismissal,
15
+ * Retains lightweight CRUD operations for config, issues, shelving, dismissal,
15
16
  * and status overrides.
16
17
  */
17
18
  export declare class StateManager {
18
- private state;
19
- private readonly inMemoryOnly;
19
+ protected state: AgentState;
20
+ protected inMemoryOnly: boolean;
20
21
  private lastLoadedMtimeMs;
21
22
  private _batching;
22
23
  private _batchDirty;
24
+ protected gistStore: GistStateStore | null;
25
+ protected gistDegraded: boolean;
23
26
  /**
24
27
  * Create a new StateManager instance.
25
28
  * @param inMemoryOnly - When true, state is held only in memory and never read from or
@@ -27,6 +30,15 @@ export declare class StateManager {
27
30
  * Defaults to false (normal persistent mode).
28
31
  */
29
32
  constructor(inMemoryOnly?: boolean);
33
+ /**
34
+ * Async factory that creates a StateManager backed by a GitHub Gist.
35
+ *
36
+ * The regular constructor is synchronous (for backwards-compat), but Gist
37
+ * bootstrapping requires network calls, so this factory is async.
38
+ *
39
+ * @param token - GitHub personal access token with `gist` scope
40
+ */
41
+ static createWithGist(token: string): Promise<StateManager>;
30
42
  /**
31
43
  * Attempt PR count reconciliation, logging a warning on failure.
32
44
  * Called after every state load from disk.
@@ -58,8 +70,17 @@ export declare class StateManager {
58
70
  /**
59
71
  * Persist the current state to disk, creating a timestamped backup of the previous
60
72
  * state file first. In in-memory mode, only updates `lastRunAt` without any file I/O.
73
+ *
74
+ * In Gist mode, writes to a local cache file (not the main state file) so the Gist
75
+ * remains the source of truth. Use `checkpoint()` to push state to the Gist.
61
76
  */
62
77
  save(): void;
78
+ /** Push current state to Gist (async). Call at well-defined moments (end of daily, after claim). */
79
+ checkpoint(): Promise<boolean>;
80
+ /** Whether this StateManager is backed by a Gist. */
81
+ isGistMode(): boolean;
82
+ /** Whether the Gist is in degraded mode (using local cache fallback). */
83
+ isGistDegraded(): boolean;
63
84
  /**
64
85
  * Get the current state as a read-only snapshot.
65
86
  */
@@ -89,11 +110,6 @@ export declare class StateManager {
89
110
  * @param counts - Monthly opened PR counts keyed by YYYY-MM
90
111
  */
91
112
  setMonthlyOpenedCounts(counts: Record<string, number>): void;
92
- /**
93
- * Update daily activity counts for dashboard display.
94
- * @param counts - Daily activity counts keyed by YYYY-MM-DD
95
- */
96
- setDailyActivityCounts(counts: Record<string, number>): void;
97
113
  /**
98
114
  * Update the local repository cache.
99
115
  * @param cache - Local repository cache mapping repo names to paths
@@ -122,26 +138,6 @@ export declare class StateManager {
122
138
  * @param config - Partial config object to merge
123
139
  */
124
140
  updateConfig(config: Partial<AgentState['config']>): void;
125
- /**
126
- * Append a new event to the event log and auto-persist.
127
- * Events are capped at 1000 to prevent unbounded growth.
128
- * @param type - The event type identifier
129
- * @param data - Arbitrary event payload
130
- */
131
- appendEvent(type: StateEventType, data: Record<string, unknown>): void;
132
- /**
133
- * Filter events by type.
134
- * @param type - The event type to filter by
135
- * @returns Events matching the given type
136
- */
137
- getEventsByType(type: StateEventType): StateEvent[];
138
- /**
139
- * Filter events within a date range.
140
- * @param since - Start of range (inclusive)
141
- * @param until - End of range (inclusive), defaults to now
142
- * @returns Events within the date range
143
- */
144
- getEventsInRange(since: Date, until?: Date): StateEvent[];
145
141
  /**
146
142
  * Track a new issue. No-op if the issue URL is already tracked.
147
143
  * @param issue - The issue to track
@@ -287,6 +283,19 @@ export declare class StateManager {
287
283
  * ```
288
284
  */
289
285
  export declare function getStateManager(): StateManager;
286
+ /**
287
+ * Get or create a StateManager with Gist-backed persistence.
288
+ * If a StateManager already exists (from sync init), returns it.
289
+ * If a token is provided and no manager exists, creates one with Gist backing.
290
+ * Falls back to sync initialization if no token is provided.
291
+ *
292
+ * **Important:** This must be called (and awaited) before any command runs for
293
+ * Gist mode to be active. It pre-sets the singleton so that subsequent
294
+ * `getStateManager()` calls return the Gist-backed instance. If this is not
295
+ * called first, `getStateManager()` will lazily create a local-only
296
+ * StateManager and Gist checkpoints will be no-ops.
297
+ */
298
+ export declare function getStateManagerAsync(token?: string): Promise<StateManager>;
290
299
  /**
291
300
  * Reset the singleton StateManager instance to null. Intended for test isolation.
292
301
  */
@@ -3,19 +3,20 @@
3
3
  * Thin coordinator that delegates persistence to state-persistence.ts
4
4
  * and scoring logic to repo-score-manager.ts.
5
5
  */
6
- import { loadState, saveState, reloadStateIfChanged, createFreshState } from './state-persistence.js';
6
+ import * as fs from 'fs';
7
+ import { loadState, saveState, reloadStateIfChanged, createFreshState, atomicWriteFileSync, } from './state-persistence.js';
7
8
  import * as repoScoring from './repo-score-manager.js';
8
9
  import { debug, warn } from './logger.js';
9
10
  import { errorMessage } from './errors.js';
11
+ import { GistStateStore } from './gist-state-store.js';
12
+ import { getStatePath, getStateCachePath } from './utils.js';
10
13
  export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
11
14
  const MODULE = 'state';
12
- // Maximum number of events to retain in the event log
13
- const MAX_EVENTS = 1000;
14
15
  /**
15
16
  * Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
16
17
  *
17
18
  * Delegates file I/O to state-persistence.ts and scoring logic to repo-score-manager.ts.
18
- * Retains lightweight CRUD operations for config, events, issues, shelving, dismissal,
19
+ * Retains lightweight CRUD operations for config, issues, shelving, dismissal,
19
20
  * and status overrides.
20
21
  */
21
22
  export class StateManager {
@@ -24,6 +25,8 @@ export class StateManager {
24
25
  lastLoadedMtimeMs = 0;
25
26
  _batching = false;
26
27
  _batchDirty = false;
28
+ gistStore = null;
29
+ gistDegraded = false;
27
30
  /**
28
31
  * Create a new StateManager instance.
29
32
  * @param inMemoryOnly - When true, state is held only in memory and never read from or
@@ -42,6 +45,53 @@ export class StateManager {
42
45
  this.tryReconcilePRCounts();
43
46
  }
44
47
  }
48
+ /**
49
+ * Async factory that creates a StateManager backed by a GitHub Gist.
50
+ *
51
+ * The regular constructor is synchronous (for backwards-compat), but Gist
52
+ * bootstrapping requires network calls, so this factory is async.
53
+ *
54
+ * @param token - GitHub personal access token with `gist` scope
55
+ */
56
+ static async createWithGist(token) {
57
+ // Dynamic import to avoid circular dependencies
58
+ const { getOctokit } = await import('./github.js');
59
+ const octokit = getOctokit(token);
60
+ const gistStore = new GistStateStore(octokit);
61
+ // Check if local state exists for migration
62
+ const statePath = getStatePath();
63
+ let result;
64
+ if (fs.existsSync(statePath)) {
65
+ // Existing user: load local state and migrate it into the Gist if no Gist exists yet.
66
+ const localStateResult = loadState();
67
+ const migrationResult = await gistStore.bootstrapWithMigration(localStateResult.state);
68
+ result = migrationResult;
69
+ // If a new Gist was just created from local state, rename the local file
70
+ // so it no longer competes as the source of truth on future startups.
71
+ if (migrationResult.migrated) {
72
+ try {
73
+ const preGistPath = statePath + '.pre-gist-migration';
74
+ fs.renameSync(statePath, preGistPath);
75
+ debug(MODULE, `Renamed ${statePath} to ${preGistPath} after Gist migration`);
76
+ }
77
+ catch (err) {
78
+ warn(MODULE, `Failed to rename state.json after Gist migration: ${err}`);
79
+ }
80
+ }
81
+ }
82
+ else {
83
+ result = await gistStore.bootstrap();
84
+ }
85
+ const manager = new StateManager(true); // start in-memory
86
+ manager.state = result.state;
87
+ if (result.gistId) {
88
+ manager.state.gistId = result.gistId;
89
+ }
90
+ manager.gistStore = gistStore;
91
+ manager.gistDegraded = result.degraded ?? false;
92
+ manager.inMemoryOnly = false; // re-enable persistence
93
+ return manager;
94
+ }
45
95
  /**
46
96
  * Attempt PR count reconciliation, logging a warning on failure.
47
97
  * Called after every state load from disk.
@@ -119,14 +169,44 @@ export class StateManager {
119
169
  /**
120
170
  * Persist the current state to disk, creating a timestamped backup of the previous
121
171
  * state file first. In in-memory mode, only updates `lastRunAt` without any file I/O.
172
+ *
173
+ * In Gist mode, writes to a local cache file (not the main state file) so the Gist
174
+ * remains the source of truth. Use `checkpoint()` to push state to the Gist.
122
175
  */
123
176
  save() {
124
177
  this.state.lastRunAt = new Date().toISOString();
125
178
  if (this.inMemoryOnly) {
126
179
  return;
127
180
  }
181
+ if (this.gistStore) {
182
+ // In Gist mode, write to local cache (not main state file).
183
+ // The Gist is the source of truth; local cache is for fallback.
184
+ try {
185
+ atomicWriteFileSync(getStateCachePath(), JSON.stringify(this.state, null, 2), 0o600);
186
+ }
187
+ catch {
188
+ // Best-effort cache write
189
+ }
190
+ return;
191
+ }
192
+ // Local file mode (existing behavior)
128
193
  this.lastLoadedMtimeMs = saveState(this.state);
129
194
  }
195
+ /** Push current state to Gist (async). Call at well-defined moments (end of daily, after claim). */
196
+ async checkpoint() {
197
+ if (!this.gistStore)
198
+ return true; // not in Gist mode
199
+ this.gistStore.setState(JSON.stringify(this.state, null, 2));
200
+ return this.gistStore.push();
201
+ }
202
+ /** Whether this StateManager is backed by a Gist. */
203
+ isGistMode() {
204
+ return this.gistStore !== null;
205
+ }
206
+ /** Whether the Gist is in degraded mode (using local cache fallback). */
207
+ isGistDegraded() {
208
+ return this.gistDegraded;
209
+ }
130
210
  /**
131
211
  * Get the current state as a read-only snapshot.
132
212
  */
@@ -140,6 +220,8 @@ export class StateManager {
140
220
  reloadIfChanged() {
141
221
  if (this.inMemoryOnly)
142
222
  return false;
223
+ if (this.gistStore)
224
+ return false; // Gist is the source of truth; skip local file reload
143
225
  const result = reloadStateIfChanged(this.lastLoadedMtimeMs);
144
226
  if (!result)
145
227
  return false;
@@ -182,14 +264,6 @@ export class StateManager {
182
264
  this.state.monthlyOpenedCounts = counts;
183
265
  this.autoSave();
184
266
  }
185
- /**
186
- * Update daily activity counts for dashboard display.
187
- * @param counts - Daily activity counts keyed by YYYY-MM-DD
188
- */
189
- setDailyActivityCounts(counts) {
190
- this.state.dailyActivityCounts = counts;
191
- this.autoSave();
192
- }
193
267
  /**
194
268
  * Update the local repository cache.
195
269
  * @param cache - Local repository cache mapping repo names to paths
@@ -261,47 +335,6 @@ export class StateManager {
261
335
  this.state.config = { ...this.state.config, ...config };
262
336
  this.autoSave();
263
337
  }
264
- // === Event Logging ===
265
- /**
266
- * Append a new event to the event log and auto-persist.
267
- * Events are capped at 1000 to prevent unbounded growth.
268
- * @param type - The event type identifier
269
- * @param data - Arbitrary event payload
270
- */
271
- appendEvent(type, data) {
272
- const event = {
273
- id: `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
274
- type,
275
- at: new Date().toISOString(),
276
- data,
277
- };
278
- this.state.events.push(event);
279
- // Cap the events array to prevent unbounded growth
280
- if (this.state.events.length > MAX_EVENTS) {
281
- this.state.events = this.state.events.slice(-MAX_EVENTS);
282
- }
283
- this.autoSave();
284
- }
285
- /**
286
- * Filter events by type.
287
- * @param type - The event type to filter by
288
- * @returns Events matching the given type
289
- */
290
- getEventsByType(type) {
291
- return this.state.events.filter((e) => e.type === type);
292
- }
293
- /**
294
- * Filter events within a date range.
295
- * @param since - Start of range (inclusive)
296
- * @param until - End of range (inclusive), defaults to now
297
- * @returns Events within the date range
298
- */
299
- getEventsInRange(since, until = new Date()) {
300
- return this.state.events.filter((e) => {
301
- const eventTime = new Date(e.at);
302
- return eventTime >= since && eventTime <= until;
303
- });
304
- }
305
338
  // === Issue Management ===
306
339
  /**
307
340
  * Track a new issue. No-op if the issue URL is already tracked.
@@ -590,6 +623,7 @@ export class StateManager {
590
623
  }
591
624
  // Singleton instance
592
625
  let stateManager = null;
626
+ let asyncManagerPromise = null;
593
627
  /**
594
628
  * Get the singleton StateManager instance, creating it on first call.
595
629
  * @returns The shared StateManager instance
@@ -609,9 +643,43 @@ export function getStateManager() {
609
643
  }
610
644
  return stateManager;
611
645
  }
646
+ /**
647
+ * Get or create a StateManager with Gist-backed persistence.
648
+ * If a StateManager already exists (from sync init), returns it.
649
+ * If a token is provided and no manager exists, creates one with Gist backing.
650
+ * Falls back to sync initialization if no token is provided.
651
+ *
652
+ * **Important:** This must be called (and awaited) before any command runs for
653
+ * Gist mode to be active. It pre-sets the singleton so that subsequent
654
+ * `getStateManager()` calls return the Gist-backed instance. If this is not
655
+ * called first, `getStateManager()` will lazily create a local-only
656
+ * StateManager and Gist checkpoints will be no-ops.
657
+ */
658
+ export async function getStateManagerAsync(token) {
659
+ if (stateManager)
660
+ return stateManager;
661
+ if (asyncManagerPromise)
662
+ return asyncManagerPromise;
663
+ if (token) {
664
+ asyncManagerPromise = StateManager.createWithGist(token)
665
+ .then((mgr) => {
666
+ stateManager = mgr;
667
+ asyncManagerPromise = null;
668
+ return mgr;
669
+ })
670
+ .catch((err) => {
671
+ asyncManagerPromise = null;
672
+ warn(MODULE, `Unhandled Gist initialization error, falling back to local-only mode (not a normal degraded bootstrap): ${err}`);
673
+ return getStateManager(); // fall back to sync/local
674
+ });
675
+ return asyncManagerPromise;
676
+ }
677
+ return getStateManager();
678
+ }
612
679
  /**
613
680
  * Reset the singleton StateManager instance to null. Intended for test isolation.
614
681
  */
615
682
  export function resetStateManager() {
616
683
  stateManager = null;
684
+ asyncManagerPromise = null;
617
685
  }
@@ -2,7 +2,7 @@
2
2
  * Core types for the Open Source Contribution Agent
3
3
  */
4
4
  import type { FetchedPRStatus, RepoSignals, TrackedIssue, IssueVettingResult, IssueScope, AgentConfig, AgentState } from './state-schema.js';
5
- export type { IssueStatus, FetchedPRStatus, ProjectCategory, IssueScope, StateEventType, RepoSignals, RepoScore, StateEvent, StoredMergedPR, StoredClosedPR, ContributionGuidelines, IssueVettingResult, TrackedIssue, ShelvedPRRef, StatusOverride, AgentConfig, LocalRepoCache, ClosedPR, MergedPR, DailyDigest, AgentState, } from './state-schema.js';
5
+ export type { IssueStatus, FetchedPRStatus, ProjectCategory, IssueScope, RepoSignals, RepoScore, StoredMergedPR, StoredClosedPR, AnalyzedIssueConversation, ContributionGuidelines, IssueVettingResult, TrackedIssue, ShelvedPRRef, StatusOverride, AgentConfig, LocalRepoCache, ClosedPR, MergedPR, DailyDigest, AgentState, } from './state-schema.js';
6
6
  /** CI pipeline status for a PR's latest commit. */
7
7
  export type CIStatus = 'passing' | 'failing' | 'pending' | 'unknown';
8
8
  /**
@@ -233,7 +233,7 @@ interface CommentedIssueWithoutResponse extends CommentedIssueBase {
233
233
  export type CommentedIssue = CommentedIssueWithResponse | CommentedIssueWithoutResponse;
234
234
  /** Default configuration applied to new state files. All fields can be overridden via `/setup-oss`. */
235
235
  export declare const DEFAULT_CONFIG: AgentConfig;
236
- /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v2 architecture. */
236
+ /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v3 architecture. */
237
237
  export declare const INITIAL_STATE: AgentState;
238
238
  export declare const PROJECT_CATEGORIES: ("nonprofit" | "devtools" | "infrastructure" | "web-frameworks" | "data-ml" | "education")[];
239
239
  export declare const ISSUE_SCOPES: ("advanced" | "beginner" | "intermediate")[];
@@ -12,8 +12,8 @@ export function isBelowMinStars(stargazersCount, minStars) {
12
12
  // ── Schema-derived constants ─────────────────────────────────────────
13
13
  /** Default configuration applied to new state files. All fields can be overridden via `/setup-oss`. */
14
14
  export const DEFAULT_CONFIG = AgentConfigSchema.parse({});
15
- /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v2 architecture. */
16
- export const INITIAL_STATE = AgentStateSchema.parse({ version: 2 });
15
+ /** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v3 architecture. */
16
+ export const INITIAL_STATE = AgentStateSchema.parse({ version: 3 });
17
17
  // ── Const arrays (derived from Zod schemas for runtime iteration) ────
18
18
  export const PROJECT_CATEGORIES = ProjectCategorySchema.options;
19
19
  export const ISSUE_SCOPES = IssueScopeSchema.options;