@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.
- package/dist/cli-registry.js +2 -2
- package/dist/cli.bundle.cjs +61 -61
- package/dist/commands/comments.js +11 -0
- package/dist/commands/config.js +0 -9
- package/dist/commands/daily.js +25 -2
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/setup.d.ts +0 -1
- package/dist/commands/setup.js +0 -26
- package/dist/commands/startup.js +25 -3
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.js +37 -0
- package/dist/core/gist-state-store.d.ts +169 -0
- package/dist/core/gist-state-store.js +424 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +3 -2
- package/dist/core/state-persistence.d.ts +14 -2
- package/dist/core/state-persistence.js +46 -12
- package/dist/core/state-schema.d.ts +19 -42
- package/dist/core/state-schema.js +11 -19
- package/dist/core/state.d.ts +38 -29
- package/dist/core/state.js +121 -53
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +2 -2
- package/dist/core/utils.d.ts +30 -0
- package/dist/core/utils.js +36 -0
- package/dist/formatters/json.d.ts +5 -2
- package/dist/formatters/json.js +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gist-based persistence layer for oss-autopilot state.
|
|
3
|
+
*
|
|
4
|
+
* Manages a single private GitHub Gist that stores `state.json` (structured state)
|
|
5
|
+
* and potentially freeform markdown documents. Provides an in-memory file cache
|
|
6
|
+
* for session-scoped reads and a local cache write-through for degraded-mode fallback.
|
|
7
|
+
*
|
|
8
|
+
* Bootstrap flow:
|
|
9
|
+
* 1. Check for locally stored Gist ID file (`~/.oss-autopilot/gist-id`)
|
|
10
|
+
* 2. If found, fetch that Gist directly via `GET /gists/:id`
|
|
11
|
+
* 3. If not found locally, search the user's Gists for description `oss-autopilot-state`
|
|
12
|
+
* 4. If found via search, store the ID locally and fetch it
|
|
13
|
+
* 5. If not found anywhere, create a new private Gist with seed files and store the ID
|
|
14
|
+
* 6. Cache all Gist file contents in memory for session-scoped reads
|
|
15
|
+
* 7. Write state to a local cache file for fallback
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import { AgentStateSchema } from './state-schema.js';
|
|
19
|
+
import { atomicWriteFileSync, createFreshState, migrateV1ToV2, migrateV2ToV3 } from './state-persistence.js';
|
|
20
|
+
import { getGistIdPath, getStateCachePath } from './utils.js';
|
|
21
|
+
import { debug, warn } from './logger.js';
|
|
22
|
+
const MODULE = 'gist-store';
|
|
23
|
+
/** Well-known Gist description used for search-based discovery. */
|
|
24
|
+
export const GIST_DESCRIPTION = 'oss-autopilot-state';
|
|
25
|
+
/** Primary state file name inside the Gist. */
|
|
26
|
+
export const STATE_FILE_NAME = 'state.json';
|
|
27
|
+
/**
|
|
28
|
+
* Gist-backed state store with in-memory file cache and local write-through.
|
|
29
|
+
*/
|
|
30
|
+
export class GistStateStore {
|
|
31
|
+
gistId = null;
|
|
32
|
+
cachedFiles = new Map();
|
|
33
|
+
dirtyFiles = new Set();
|
|
34
|
+
octokit;
|
|
35
|
+
constructor(octokit) {
|
|
36
|
+
this.octokit = octokit;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Bootstrap the Gist store: locate or create the backing Gist,
|
|
40
|
+
* populate the in-memory cache, and write the local cache file.
|
|
41
|
+
*/
|
|
42
|
+
async bootstrap() {
|
|
43
|
+
try {
|
|
44
|
+
// Step 1: Try loading Gist ID from local file
|
|
45
|
+
const localId = this.readLocalGistId();
|
|
46
|
+
if (localId) {
|
|
47
|
+
debug(MODULE, `Found local Gist ID: ${localId}`);
|
|
48
|
+
try {
|
|
49
|
+
this.gistId = localId;
|
|
50
|
+
const state = await this.fetchAndCache(localId);
|
|
51
|
+
return { gistId: localId, state, created: false };
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
warn(MODULE, `Failed to fetch Gist ${localId}, will search/create`, err);
|
|
55
|
+
// Fall through to search
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Step 2: Search user's Gists by description
|
|
59
|
+
const foundId = await this.searchForGist();
|
|
60
|
+
if (foundId) {
|
|
61
|
+
debug(MODULE, `Found Gist via search: ${foundId}`);
|
|
62
|
+
this.gistId = foundId;
|
|
63
|
+
this.writeLocalGistId(foundId);
|
|
64
|
+
const state = await this.fetchAndCache(foundId);
|
|
65
|
+
return { gistId: foundId, state, created: false };
|
|
66
|
+
}
|
|
67
|
+
// Step 3: Create a new Gist
|
|
68
|
+
debug(MODULE, 'No existing Gist found, creating new one');
|
|
69
|
+
const { id, state } = await this.createGist();
|
|
70
|
+
this.writeLocalGistId(id);
|
|
71
|
+
return { gistId: id, state, created: true };
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
// All API paths failed — enter degraded mode
|
|
75
|
+
warn(MODULE, 'All Gist API paths failed, entering degraded mode', err);
|
|
76
|
+
// Try reading from local cache file
|
|
77
|
+
try {
|
|
78
|
+
const cachePath = getStateCachePath();
|
|
79
|
+
if (fs.existsSync(cachePath)) {
|
|
80
|
+
let obj = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
81
|
+
// Chain migrations
|
|
82
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
83
|
+
const record = obj;
|
|
84
|
+
if (record.version === 1)
|
|
85
|
+
obj = migrateV1ToV2(record);
|
|
86
|
+
if (obj.version === 2)
|
|
87
|
+
obj = migrateV2ToV3(obj);
|
|
88
|
+
}
|
|
89
|
+
const cachedState = AgentStateSchema.parse(obj);
|
|
90
|
+
debug(MODULE, 'Loaded state from local cache in degraded mode');
|
|
91
|
+
return { gistId: '', state: cachedState, created: false, degraded: true };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (cacheErr) {
|
|
95
|
+
debug(MODULE, `Failed to read local cache in degraded mode: ${cacheErr}`);
|
|
96
|
+
}
|
|
97
|
+
// No cache either — return fresh state in degraded mode
|
|
98
|
+
debug(MODULE, 'No local cache found, returning fresh state in degraded mode');
|
|
99
|
+
return { gistId: '', state: createFreshState(), created: false, degraded: true };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Bootstrap with migration from an existing local state.
|
|
104
|
+
* If a Gist already exists (found via local ID or search), uses it — no migration needed.
|
|
105
|
+
* If no Gist exists, creates one seeded with the provided existingState instead of a fresh state.
|
|
106
|
+
* @returns BootstrapResult extended with `migrated: true` if a new Gist was created from local state
|
|
107
|
+
*/
|
|
108
|
+
async bootstrapWithMigration(existingState) {
|
|
109
|
+
try {
|
|
110
|
+
// Step 1: Try loading Gist ID from local file
|
|
111
|
+
const localId = this.readLocalGistId();
|
|
112
|
+
if (localId) {
|
|
113
|
+
debug(MODULE, `bootstrapWithMigration: found local Gist ID: ${localId}`);
|
|
114
|
+
try {
|
|
115
|
+
this.gistId = localId;
|
|
116
|
+
const state = await this.fetchAndCache(localId);
|
|
117
|
+
return { gistId: localId, state, created: false, migrated: false };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
warn(MODULE, `bootstrapWithMigration: failed to fetch Gist ${localId}, will search/create`, err);
|
|
121
|
+
// Fall through to search
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Step 2: Search user's Gists by description
|
|
125
|
+
const foundId = await this.searchForGist();
|
|
126
|
+
if (foundId) {
|
|
127
|
+
debug(MODULE, `bootstrapWithMigration: found Gist via search: ${foundId}`);
|
|
128
|
+
this.gistId = foundId;
|
|
129
|
+
this.writeLocalGistId(foundId);
|
|
130
|
+
const state = await this.fetchAndCache(foundId);
|
|
131
|
+
return { gistId: foundId, state, created: false, migrated: false };
|
|
132
|
+
}
|
|
133
|
+
// Step 3: No existing Gist found — create one seeded with the provided state
|
|
134
|
+
debug(MODULE, 'bootstrapWithMigration: no existing Gist found, creating one seeded with local state');
|
|
135
|
+
const { id, state } = await this.createGistFromState(existingState);
|
|
136
|
+
this.writeLocalGistId(id);
|
|
137
|
+
return { gistId: id, state, created: true, migrated: true };
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
// All API paths failed — enter degraded mode
|
|
141
|
+
warn(MODULE, 'bootstrapWithMigration: all Gist API paths failed, entering degraded mode', err);
|
|
142
|
+
// Try reading from local cache file
|
|
143
|
+
try {
|
|
144
|
+
const cachePath = getStateCachePath();
|
|
145
|
+
if (fs.existsSync(cachePath)) {
|
|
146
|
+
let obj = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
147
|
+
// Chain migrations
|
|
148
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
149
|
+
const record = obj;
|
|
150
|
+
if (record.version === 1)
|
|
151
|
+
obj = migrateV1ToV2(record);
|
|
152
|
+
if (obj.version === 2)
|
|
153
|
+
obj = migrateV2ToV3(obj);
|
|
154
|
+
}
|
|
155
|
+
const cachedState = AgentStateSchema.parse(obj);
|
|
156
|
+
debug(MODULE, 'bootstrapWithMigration: loaded state from local cache in degraded mode');
|
|
157
|
+
return { gistId: '', state: cachedState, created: false, degraded: true, migrated: false };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (cacheErr) {
|
|
161
|
+
debug(MODULE, `bootstrapWithMigration: failed to read local cache in degraded mode: ${cacheErr}`);
|
|
162
|
+
}
|
|
163
|
+
// No cache either — use the provided existingState in degraded mode
|
|
164
|
+
debug(MODULE, 'bootstrapWithMigration: no local cache found, returning existing state in degraded mode');
|
|
165
|
+
return { gistId: '', state: existingState, created: false, degraded: true, migrated: false };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Return the resolved Gist ID (available after bootstrap). */
|
|
169
|
+
getGistId() {
|
|
170
|
+
return this.gistId;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Mark a file as dirty so it will be included in the next `push()` call.
|
|
174
|
+
*/
|
|
175
|
+
markDirty(filename) {
|
|
176
|
+
this.dirtyFiles.add(filename);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Read a freeform document from the in-memory cache.
|
|
180
|
+
* Returns null if the file has not been loaded (or does not exist in the Gist).
|
|
181
|
+
* Synchronous — all Gist contents are loaded into memory at bootstrap.
|
|
182
|
+
*/
|
|
183
|
+
getDocument(filename) {
|
|
184
|
+
return this.cachedFiles.get(filename) ?? null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Write a freeform document into the in-memory cache and mark it dirty
|
|
188
|
+
* so it will be included in the next `push()` call.
|
|
189
|
+
*/
|
|
190
|
+
setDocument(filename, content) {
|
|
191
|
+
this.cachedFiles.set(filename, content);
|
|
192
|
+
this.markDirty(filename);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Return all filenames in the in-memory cache whose names start with `prefix`.
|
|
196
|
+
* Useful for listing all guidelines files (e.g. prefix `guidelines--`).
|
|
197
|
+
*/
|
|
198
|
+
listDocuments(prefix) {
|
|
199
|
+
const results = [];
|
|
200
|
+
for (const filename of this.cachedFiles.keys()) {
|
|
201
|
+
if (filename.startsWith(prefix)) {
|
|
202
|
+
results.push(filename);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return results;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Stage new state JSON for the next `push()`. Updates the in-memory cache
|
|
209
|
+
* for `state.json` and marks it dirty.
|
|
210
|
+
*/
|
|
211
|
+
setState(stateJson) {
|
|
212
|
+
this.cachedFiles.set(STATE_FILE_NAME, stateJson);
|
|
213
|
+
this.markDirty(STATE_FILE_NAME);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Push all dirty files to the backing Gist. Retries once on failure.
|
|
217
|
+
*
|
|
218
|
+
* Returns `true` on success (or when there is nothing to push).
|
|
219
|
+
* Returns `false` if both attempts fail.
|
|
220
|
+
* Throws if the Gist ID has not been resolved yet (bootstrap not called).
|
|
221
|
+
*/
|
|
222
|
+
async push() {
|
|
223
|
+
if (this.dirtyFiles.size === 0) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (this.gistId === null) {
|
|
227
|
+
throw new Error('GistStateStore: cannot push before bootstrap — gistId is null');
|
|
228
|
+
}
|
|
229
|
+
// Build PATCH payload from the dirty set
|
|
230
|
+
const files = {};
|
|
231
|
+
for (const filename of this.dirtyFiles) {
|
|
232
|
+
const content = this.cachedFiles.get(filename);
|
|
233
|
+
if (content !== undefined) {
|
|
234
|
+
files[filename] = { content };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const attempt = async () => {
|
|
238
|
+
await this.octokit.gists.update({ gist_id: this.gistId, files });
|
|
239
|
+
return true;
|
|
240
|
+
};
|
|
241
|
+
try {
|
|
242
|
+
await attempt();
|
|
243
|
+
}
|
|
244
|
+
catch (firstErr) {
|
|
245
|
+
debug(MODULE, `push failed on first attempt, retrying: ${firstErr}`);
|
|
246
|
+
try {
|
|
247
|
+
await attempt();
|
|
248
|
+
}
|
|
249
|
+
catch (secondErr) {
|
|
250
|
+
warn(MODULE, `push failed after retry, giving up: ${secondErr}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Success: flush dirty set and write local cache
|
|
255
|
+
this.dirtyFiles.clear();
|
|
256
|
+
const raw = this.cachedFiles.get(STATE_FILE_NAME);
|
|
257
|
+
if (raw) {
|
|
258
|
+
try {
|
|
259
|
+
const state = this.parseStateFromCache();
|
|
260
|
+
this.writeLocalStateCache(state);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
debug(MODULE, `push succeeded but local cache write failed: ${err}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
// ── Private helpers ─────────────────────────────────────────────────
|
|
269
|
+
/**
|
|
270
|
+
* Fetch a Gist by ID, populate the in-memory cache, parse state,
|
|
271
|
+
* and write the local cache file.
|
|
272
|
+
*/
|
|
273
|
+
async fetchAndCache(gistId) {
|
|
274
|
+
const { data } = await this.octokit.gists.get({ gist_id: gistId });
|
|
275
|
+
this.gistId = gistId;
|
|
276
|
+
// Populate in-memory cache with ALL files from the Gist
|
|
277
|
+
this.cachedFiles.clear();
|
|
278
|
+
for (const [filename, file] of Object.entries(data.files)) {
|
|
279
|
+
if (file && file.content != null) {
|
|
280
|
+
this.cachedFiles.set(filename, file.content);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Parse state.json
|
|
284
|
+
const state = this.parseStateFromCache();
|
|
285
|
+
// Write-through to local cache for degraded-mode fallback
|
|
286
|
+
this.writeLocalStateCache(state);
|
|
287
|
+
return state;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Parse `state.json` from the in-memory cache. Handles v2 migration
|
|
291
|
+
* by running through the Zod schema (which requires version: 3).
|
|
292
|
+
* Falls back to fresh state if the file is missing or unparseable.
|
|
293
|
+
*/
|
|
294
|
+
parseStateFromCache() {
|
|
295
|
+
const raw = this.cachedFiles.get(STATE_FILE_NAME);
|
|
296
|
+
if (!raw) {
|
|
297
|
+
debug(MODULE, 'No state.json found in Gist, using fresh state');
|
|
298
|
+
return createFreshState();
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
let obj = JSON.parse(raw);
|
|
302
|
+
// Chain migrations using shared helpers from state-persistence
|
|
303
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
304
|
+
const record = obj;
|
|
305
|
+
if (record.version === 1)
|
|
306
|
+
obj = migrateV1ToV2(record);
|
|
307
|
+
if (obj.version === 2)
|
|
308
|
+
obj = migrateV2ToV3(obj);
|
|
309
|
+
}
|
|
310
|
+
return AgentStateSchema.parse(obj);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
warn(MODULE, `Failed to parse state.json from Gist: ${err}`);
|
|
314
|
+
return createFreshState();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Search the authenticated user's Gists for one with the well-known description.
|
|
319
|
+
* Pages through up to 10 pages (100 Gists per page) to find it.
|
|
320
|
+
*/
|
|
321
|
+
async searchForGist() {
|
|
322
|
+
try {
|
|
323
|
+
const maxPages = 10;
|
|
324
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
325
|
+
const { data: gists } = await this.octokit.gists.list({ per_page: 100, page });
|
|
326
|
+
if (gists.length === 0)
|
|
327
|
+
break;
|
|
328
|
+
const match = gists.find((g) => g.description === GIST_DESCRIPTION);
|
|
329
|
+
if (match) {
|
|
330
|
+
return match.id;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
warn(MODULE, 'Failed to search Gists by description', err);
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Create a new private Gist with seed files and store it in memory.
|
|
341
|
+
*/
|
|
342
|
+
async createGist() {
|
|
343
|
+
const freshState = createFreshState();
|
|
344
|
+
const stateContent = JSON.stringify(freshState, null, 2);
|
|
345
|
+
const { data } = await this.octokit.gists.create({
|
|
346
|
+
description: GIST_DESCRIPTION,
|
|
347
|
+
public: false,
|
|
348
|
+
files: {
|
|
349
|
+
[STATE_FILE_NAME]: { content: stateContent },
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
this.gistId = data.id;
|
|
353
|
+
// Populate in-memory cache
|
|
354
|
+
this.cachedFiles.clear();
|
|
355
|
+
for (const [filename, file] of Object.entries(data.files)) {
|
|
356
|
+
if (file && file.content != null) {
|
|
357
|
+
this.cachedFiles.set(filename, file.content);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Write-through to local cache
|
|
361
|
+
this.writeLocalStateCache(freshState);
|
|
362
|
+
return { id: data.id, state: freshState };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Create a new private Gist seeded with the provided state (for migration).
|
|
366
|
+
*/
|
|
367
|
+
async createGistFromState(seedState) {
|
|
368
|
+
const stateContent = JSON.stringify(seedState, null, 2);
|
|
369
|
+
const { data } = await this.octokit.gists.create({
|
|
370
|
+
description: GIST_DESCRIPTION,
|
|
371
|
+
public: false,
|
|
372
|
+
files: {
|
|
373
|
+
[STATE_FILE_NAME]: { content: stateContent },
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
this.gistId = data.id;
|
|
377
|
+
// Populate in-memory cache
|
|
378
|
+
this.cachedFiles.clear();
|
|
379
|
+
for (const [filename, file] of Object.entries(data.files)) {
|
|
380
|
+
if (file && file.content != null) {
|
|
381
|
+
this.cachedFiles.set(filename, file.content);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Write-through to local cache
|
|
385
|
+
this.writeLocalStateCache(seedState);
|
|
386
|
+
return { id: data.id, state: seedState };
|
|
387
|
+
}
|
|
388
|
+
/** Read the locally persisted Gist ID, or return null if not found. */
|
|
389
|
+
readLocalGistId() {
|
|
390
|
+
try {
|
|
391
|
+
const gistIdPath = getGistIdPath();
|
|
392
|
+
if (fs.existsSync(gistIdPath)) {
|
|
393
|
+
const id = fs.readFileSync(gistIdPath, 'utf-8').trim();
|
|
394
|
+
return id || null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
debug(MODULE, 'Could not read local Gist ID file', err);
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
/** Persist the Gist ID locally for fast lookup on next session. */
|
|
403
|
+
writeLocalGistId(gistId) {
|
|
404
|
+
try {
|
|
405
|
+
const gistIdPath = getGistIdPath();
|
|
406
|
+
atomicWriteFileSync(gistIdPath, gistId, 0o600);
|
|
407
|
+
debug(MODULE, `Wrote Gist ID to ${gistIdPath}`);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
warn(MODULE, `Failed to write local Gist ID file: ${err}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Write state to the local cache file for degraded-mode fallback. */
|
|
414
|
+
writeLocalStateCache(state) {
|
|
415
|
+
try {
|
|
416
|
+
const cachePath = getStateCachePath();
|
|
417
|
+
atomicWriteFileSync(cachePath, JSON.stringify(state, null, 2), 0o600);
|
|
418
|
+
debug(MODULE, `Wrote state cache to ${cachePath}`);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
warn(MODULE, `Failed to write local state cache: ${err}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Core module exports
|
|
3
3
|
* Re-exports all core functionality for convenient imports
|
|
4
4
|
*/
|
|
5
|
-
export { StateManager, getStateManager, resetStateManager, type Stats } from './state.js';
|
|
5
|
+
export { StateManager, getStateManager, getStateManagerAsync, resetStateManager, type Stats } from './state.js';
|
|
6
|
+
export { GistStateStore } from './gist-state-store.js';
|
|
6
7
|
export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
|
7
8
|
export { IssueDiscovery } from './issue-discovery.js';
|
|
8
9
|
export { isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS } from './issue-filtering.js';
|
|
@@ -10,7 +11,7 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
10
11
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
11
12
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
12
13
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
13
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
|
|
14
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
14
15
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
15
16
|
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
16
17
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
package/dist/core/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Core module exports
|
|
3
3
|
* Re-exports all core functionality for convenient imports
|
|
4
4
|
*/
|
|
5
|
-
export { StateManager, getStateManager, resetStateManager } from './state.js';
|
|
5
|
+
export { StateManager, getStateManager, getStateManagerAsync, resetStateManager } from './state.js';
|
|
6
|
+
export { GistStateStore } from './gist-state-store.js';
|
|
6
7
|
export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
|
7
8
|
export { IssueDiscovery } from './issue-discovery.js';
|
|
8
9
|
export { isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS } from './issue-filtering.js';
|
|
@@ -10,7 +11,7 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
10
11
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
11
12
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
12
13
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
13
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
|
|
14
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
14
15
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
15
16
|
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
16
17
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* State persistence layer for the OSS Contribution Agent.
|
|
3
|
-
* Handles file I/O, locking, backup/restore, and v1
|
|
3
|
+
* Handles file I/O, locking, backup/restore, and schema migration (v1→v2→v3).
|
|
4
4
|
* No module-level mutable state — functions accept/return AgentState objects.
|
|
5
5
|
*/
|
|
6
6
|
import { AgentState } from './types.js';
|
|
@@ -23,7 +23,19 @@ export declare function releaseLock(lockPath: string): void;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function atomicWriteFileSync(filePath: string, data: string, mode?: number): void;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Migrate state from v1 (local PR tracking) to v2 (fresh GitHub fetching).
|
|
27
|
+
* Preserves repoScores and config; drops the legacy PR arrays.
|
|
28
|
+
*/
|
|
29
|
+
export declare function migrateV1ToV2(rawState: Record<string, unknown>): Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Migrate state from v2 to v3.
|
|
32
|
+
* Drops: events, dailyActivityCounts, config.showHealthCheck, config.scoreThreshold.
|
|
33
|
+
* Adds: analyzedIssueConversations, learningsExtractedAt on StoredMergedPR/StoredClosedPR.
|
|
34
|
+
* New optional fields are handled by Zod defaults (undefined/optional).
|
|
35
|
+
*/
|
|
36
|
+
export declare function migrateV2ToV3(rawState: Record<string, unknown>): Record<string, unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* Create a fresh state (v3).
|
|
27
39
|
* Leverages Zod schema defaults to produce a complete state.
|
|
28
40
|
*/
|
|
29
41
|
export declare function createFreshState(): AgentState;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* State persistence layer for the OSS Contribution Agent.
|
|
3
|
-
* Handles file I/O, locking, backup/restore, and v1
|
|
3
|
+
* Handles file I/O, locking, backup/restore, and schema migration (v1→v2→v3).
|
|
4
4
|
* No module-level mutable state — functions accept/return AgentState objects.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs';
|
|
@@ -101,7 +101,7 @@ export function atomicWriteFileSync(filePath, data, mode) {
|
|
|
101
101
|
* Migrate state from v1 (local PR tracking) to v2 (fresh GitHub fetching).
|
|
102
102
|
* Preserves repoScores and config; drops the legacy PR arrays.
|
|
103
103
|
*/
|
|
104
|
-
function migrateV1ToV2(rawState) {
|
|
104
|
+
export function migrateV1ToV2(rawState) {
|
|
105
105
|
debug(MODULE, 'Migrating state from v1 to v2 (fresh GitHub fetching)...');
|
|
106
106
|
// Extract merged/closed PR arrays from v1 state to seed repo scores.
|
|
107
107
|
// Don't increment counts here as the score may already reflect these PRs.
|
|
@@ -131,18 +131,39 @@ function migrateV1ToV2(rawState) {
|
|
|
131
131
|
activeIssues: rawState.activeIssues || [],
|
|
132
132
|
repoScores,
|
|
133
133
|
config: rawState.config,
|
|
134
|
-
events: rawState.events || [],
|
|
135
134
|
lastRunAt: new Date().toISOString(),
|
|
136
135
|
};
|
|
137
136
|
debug(MODULE, `Migration complete. Preserved ${Object.keys(repoScores).length} repo scores.`);
|
|
138
137
|
return migratedState;
|
|
139
138
|
}
|
|
140
139
|
/**
|
|
141
|
-
*
|
|
140
|
+
* Migrate state from v2 to v3.
|
|
141
|
+
* Drops: events, dailyActivityCounts, config.showHealthCheck, config.scoreThreshold.
|
|
142
|
+
* Adds: analyzedIssueConversations, learningsExtractedAt on StoredMergedPR/StoredClosedPR.
|
|
143
|
+
* New optional fields are handled by Zod defaults (undefined/optional).
|
|
144
|
+
*/
|
|
145
|
+
export function migrateV2ToV3(rawState) {
|
|
146
|
+
debug(MODULE, 'Migrating state from v2 to v3 (drop dead fields, add learnings tracking)...');
|
|
147
|
+
// Remove dead fields from root
|
|
148
|
+
delete rawState.events;
|
|
149
|
+
delete rawState.dailyActivityCounts;
|
|
150
|
+
// Remove dead fields from config
|
|
151
|
+
const config = rawState.config;
|
|
152
|
+
if (config) {
|
|
153
|
+
delete config.showHealthCheck;
|
|
154
|
+
delete config.scoreThreshold;
|
|
155
|
+
}
|
|
156
|
+
// Bump version
|
|
157
|
+
rawState.version = 3;
|
|
158
|
+
debug(MODULE, 'v2 to v3 migration complete.');
|
|
159
|
+
return rawState;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Create a fresh state (v3).
|
|
142
163
|
* Leverages Zod schema defaults to produce a complete state.
|
|
143
164
|
*/
|
|
144
165
|
export function createFreshState() {
|
|
145
|
-
return AgentStateSchema.parse({ version:
|
|
166
|
+
return AgentStateSchema.parse({ version: 3 });
|
|
146
167
|
}
|
|
147
168
|
/**
|
|
148
169
|
* Migrate state from legacy ./data/ location to ~/.oss-autopilot/.
|
|
@@ -243,9 +264,15 @@ function tryRestoreFromBackup() {
|
|
|
243
264
|
try {
|
|
244
265
|
const data = fs.readFileSync(backupPath, 'utf-8');
|
|
245
266
|
let raw = JSON.parse(data);
|
|
246
|
-
//
|
|
247
|
-
if (typeof raw === 'object' && raw !== null
|
|
248
|
-
|
|
267
|
+
// Chain migrations: v1 → v2 → v3
|
|
268
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
269
|
+
const rawObj = raw;
|
|
270
|
+
if (rawObj.version === 1) {
|
|
271
|
+
raw = migrateV1ToV2(rawObj);
|
|
272
|
+
}
|
|
273
|
+
if (raw.version === 2) {
|
|
274
|
+
raw = migrateV2ToV3(raw);
|
|
275
|
+
}
|
|
249
276
|
}
|
|
250
277
|
const parsed = AgentStateSchema.safeParse(raw);
|
|
251
278
|
if (parsed.success) {
|
|
@@ -286,11 +313,18 @@ export function loadState() {
|
|
|
286
313
|
if (fs.existsSync(statePath)) {
|
|
287
314
|
const data = fs.readFileSync(statePath, 'utf-8');
|
|
288
315
|
let raw = JSON.parse(data);
|
|
289
|
-
//
|
|
316
|
+
// Chain migrations: v1 → v2 → v3
|
|
290
317
|
let wasMigrated = false;
|
|
291
|
-
if (typeof raw === 'object' && raw !== null
|
|
292
|
-
|
|
293
|
-
|
|
318
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
319
|
+
const rawObj = raw;
|
|
320
|
+
if (rawObj.version === 1) {
|
|
321
|
+
raw = migrateV1ToV2(rawObj);
|
|
322
|
+
wasMigrated = true;
|
|
323
|
+
}
|
|
324
|
+
if (raw.version === 2) {
|
|
325
|
+
raw = migrateV2ToV3(raw);
|
|
326
|
+
wasMigrated = true;
|
|
327
|
+
}
|
|
294
328
|
}
|
|
295
329
|
// Validate through Zod schema (strips unknown keys in memory; stale keys persist on disk until next save)
|
|
296
330
|
const parsed = AgentStateSchema.safeParse(raw);
|