@oss-autopilot/core 3.9.0 → 3.11.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.
Files changed (39) hide show
  1. package/dist/cli-registry.d.ts +7 -0
  2. package/dist/cli-registry.js +29 -5
  3. package/dist/cli.bundle.cjs +114 -114
  4. package/dist/cli.js +11 -3
  5. package/dist/commands/comments.js +31 -15
  6. package/dist/commands/compliance-score.js +12 -4
  7. package/dist/commands/daily.js +47 -2
  8. package/dist/commands/dashboard-data.d.ts +17 -0
  9. package/dist/commands/dashboard-data.js +49 -0
  10. package/dist/commands/dashboard-server.js +93 -24
  11. package/dist/commands/dismiss.d.ts +4 -0
  12. package/dist/commands/dismiss.js +4 -4
  13. package/dist/commands/guidelines.d.ts +19 -0
  14. package/dist/commands/guidelines.js +23 -4
  15. package/dist/commands/index.d.ts +3 -1
  16. package/dist/commands/index.js +2 -0
  17. package/dist/commands/move.d.ts +2 -0
  18. package/dist/commands/move.js +12 -8
  19. package/dist/commands/repo-vet.js +30 -8
  20. package/dist/commands/search.js +20 -2
  21. package/dist/commands/shelve.d.ts +4 -0
  22. package/dist/commands/shelve.js +4 -4
  23. package/dist/core/gist-state-store.js +42 -7
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/core/index.js +1 -1
  26. package/dist/core/issue-conversation.js +15 -2
  27. package/dist/core/paths.d.ts +12 -0
  28. package/dist/core/paths.js +16 -0
  29. package/dist/core/pr-comments-fetcher.d.ts +10 -2
  30. package/dist/core/pr-comments-fetcher.js +22 -4
  31. package/dist/core/state-persistence.d.ts +31 -9
  32. package/dist/core/state-persistence.js +51 -16
  33. package/dist/core/state.d.ts +18 -1
  34. package/dist/core/state.js +35 -3
  35. package/dist/core/untrusted-content.d.ts +24 -3
  36. package/dist/core/untrusted-content.js +31 -3
  37. package/dist/formatters/json.d.ts +15 -1
  38. package/dist/formatters/json.js +20 -0
  39. package/package.json +7 -7
@@ -45,16 +45,41 @@ export declare function migrateV3ToV4(rawState: Record<string, unknown>): Record
45
45
  * Leverages Zod schema defaults to produce a complete state.
46
46
  */
47
47
  export declare function createFreshState(): AgentState;
48
+ /**
49
+ * Details about a recovery that happened during {@link loadState} — set when
50
+ * the main state file was unreadable (JSON parse error) or structurally
51
+ * invalid (Zod rejection) and the loader fell back to a backup or fresh state.
52
+ * Surfaced through StateManager.getLoadRecovery() so commands can include the
53
+ * event in structured output instead of only stderr warn() lines (#1371).
54
+ */
55
+ export interface LoadRecoveryInfo {
56
+ /** Where the loaded state came from after the original file was rejected. */
57
+ source: 'backup' | 'fresh';
58
+ /** Path of the preserved `.rejected-<ts>` copy, or null if preservation failed. */
59
+ rejectedPath: string | null;
60
+ /** Short summary of why the original state file was rejected. */
61
+ reason: string;
62
+ }
63
+ /**
64
+ * Result of {@link loadState}: the loaded state, the file's mtime for change
65
+ * detection, and (only when the main file was rejected) recovery details.
66
+ */
67
+ export interface LoadStateResult {
68
+ state: AgentState;
69
+ mtimeMs: number;
70
+ recovery?: LoadRecoveryInfo;
71
+ }
48
72
  /**
49
73
  * Load state from file, or create initial state if none exists.
50
74
  * If the main state file is corrupted, attempts to restore from the most recent backup.
51
75
  * Performs migration from legacy ./data/ location if needed.
52
- * @returns Object with the loaded state and the file's mtime (for change detection).
76
+ * @returns Object with the loaded state, the file's mtime (for change detection),
77
+ * and recovery details when the main file was rejected.
78
+ * @throws Error when the state file exists but cannot be read due to a
79
+ * permission error (EACCES/EPERM) — that is an environment problem, not
80
+ * corruption, so restore-or-fresh would mask it and risk clobbering data.
53
81
  */
54
- export declare function loadState(): {
55
- state: AgentState;
56
- mtimeMs: number;
57
- };
82
+ export declare function loadState(): LoadStateResult;
58
83
  /**
59
84
  * Persist state to disk, creating a timestamped backup of the previous
60
85
  * state file first. Retains at most 10 backup files.
@@ -79,7 +104,4 @@ export declare function saveState(state: Readonly<AgentState>, expectedMtimeMs?:
79
104
  * Uses mtime comparison (single statSync call) to avoid unnecessary JSON parsing.
80
105
  * @returns The new state and mtime if reloaded, or null if no change detected.
81
106
  */
82
- export declare function reloadStateIfChanged(lastLoadedMtimeMs: number): {
83
- state: AgentState;
84
- mtimeMs: number;
85
- } | null;
107
+ export declare function reloadStateIfChanged(lastLoadedMtimeMs: number): LoadStateResult | null;
@@ -6,15 +6,12 @@
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
  import { AgentStateSchema } from './state-schema.js';
9
- import { getStatePath, getBackupDir, getDataDir } from './paths.js';
9
+ import { getStatePath, getBackupDir, getDataDir, getLegacyStatePath, getLegacyBackupDir } from './paths.js';
10
10
  import { errorMessage, ConcurrencyError } from './errors.js';
11
11
  import { debug, warn } from './logger.js';
12
12
  const MODULE = 'state';
13
13
  // Lock file timeout: if a lock is older than this, it is considered stale
14
14
  const LOCK_TIMEOUT_MS = 30_000; // 30 seconds
15
- // Legacy path for migration
16
- const LEGACY_STATE_FILE = path.join(process.cwd(), 'data', 'state.json');
17
- const LEGACY_BACKUP_DIR = path.join(process.cwd(), 'data', 'backups');
18
15
  /**
19
16
  * Check whether an existing lock file is stale (expired or corrupt).
20
17
  * Returns true if the lock should be considered stale and can be removed.
@@ -182,6 +179,10 @@ export function createFreshState() {
182
179
  */
183
180
  function migrateFromLegacyLocation() {
184
181
  const newStatePath = getStatePath();
182
+ // Resolved via paths.js (not module constants) so tests can mock the
183
+ // legacy location into per-test temp dirs — see #1382.
184
+ const LEGACY_STATE_FILE = getLegacyStatePath();
185
+ const LEGACY_BACKUP_DIR = getLegacyBackupDir();
185
186
  // If new state already exists, no migration needed
186
187
  if (fs.existsSync(newStatePath)) {
187
188
  return false;
@@ -316,11 +317,33 @@ function tryRestoreFromBackup() {
316
317
  }
317
318
  return null;
318
319
  }
320
+ /**
321
+ * Copy a rejected state file to `<statePath>.rejected-<timestamp>` so the
322
+ * user can recover data manually before the file is overwritten by a backup
323
+ * restore or fresh state. Returns the preserved path, or null if the copy
324
+ * failed (preservation is best-effort and never throws).
325
+ */
326
+ function preserveRejectedStateFile(statePath) {
327
+ try {
328
+ const rejectedPath = statePath + '.rejected-' + Date.now();
329
+ fs.copyFileSync(statePath, rejectedPath);
330
+ warn(MODULE, `Previous state preserved at: ${rejectedPath}`);
331
+ return rejectedPath;
332
+ }
333
+ catch (preserveErr) {
334
+ warn(MODULE, `Could not preserve rejected state file: ${errorMessage(preserveErr)}`);
335
+ return null;
336
+ }
337
+ }
319
338
  /**
320
339
  * Load state from file, or create initial state if none exists.
321
340
  * If the main state file is corrupted, attempts to restore from the most recent backup.
322
341
  * Performs migration from legacy ./data/ location if needed.
323
- * @returns Object with the loaded state and the file's mtime (for change detection).
342
+ * @returns Object with the loaded state, the file's mtime (for change detection),
343
+ * and recovery details when the main file was rejected.
344
+ * @throws Error when the state file exists but cannot be read due to a
345
+ * permission error (EACCES/EPERM) — that is an environment problem, not
346
+ * corruption, so restore-or-fresh would mask it and risk clobbering data.
324
347
  */
325
348
  export function loadState() {
326
349
  // Try to migrate from legacy location first
@@ -355,21 +378,16 @@ export function loadState() {
355
378
  warn(MODULE, 'Attempting to restore from backup...');
356
379
  debug(MODULE, 'Full validation errors:', parsed.error.issues);
357
380
  // Preserve the rejected state file so the user can recover
358
- try {
359
- const rejectedPath = statePath + '.rejected-' + Date.now();
360
- fs.copyFileSync(statePath, rejectedPath);
361
- warn(MODULE, `Previous state preserved at: ${rejectedPath}`);
362
- }
363
- catch (preserveErr) {
364
- warn(MODULE, `Could not preserve rejected state file: ${errorMessage(preserveErr)}`);
365
- }
381
+ const rejectedPath = preserveRejectedStateFile(statePath);
382
+ const reason = `Invalid state file structure: ${summary}`;
366
383
  const restoredState = tryRestoreFromBackup();
367
384
  if (restoredState) {
368
385
  const mtimeMs = safeGetMtimeMs(statePath);
369
- return { state: restoredState, mtimeMs };
386
+ const recovery = { source: 'backup', rejectedPath, reason };
387
+ return { state: restoredState, mtimeMs, recovery };
370
388
  }
371
389
  warn(MODULE, 'No valid backup found, starting fresh');
372
- return { state: createFreshState(), mtimeMs: 0 };
390
+ return { state: createFreshState(), mtimeMs: 0, recovery: { source: 'fresh', rejectedPath, reason } };
373
391
  }
374
392
  // Save migrated state only after validation succeeds
375
393
  if (wasMigrated) {
@@ -408,14 +426,31 @@ export function loadState() {
408
426
  }
409
427
  }
410
428
  catch (error) {
429
+ // Permission errors are environmental, not corruption — restoring a
430
+ // backup or starting fresh here would mask the real problem and risk
431
+ // clobbering a perfectly valid state file. Rethrow with guidance (#1371).
432
+ const code = error?.code;
433
+ if (code === 'EACCES' || code === 'EPERM') {
434
+ throw new Error(`Permission denied reading state file (${code}): ${errorMessage(error)}. ` +
435
+ 'This is an environment problem, not state corruption. Check the ownership and ' +
436
+ 'permissions of ~/.oss-autopilot/state.json (e.g. run `ls -l ~/.oss-autopilot/state.json` ' +
437
+ 'and fix with chown/chmod), then retry.', { cause: error });
438
+ }
411
439
  warn(MODULE, 'Error loading state:', error);
412
440
  warn(MODULE, 'Attempting to restore from backup...');
441
+ // Preserve the unparseable file before tryRestoreFromBackup() overwrites
442
+ // it with restored backup contents (#1371). Only attempt when the file
443
+ // actually exists — the error may have come from elsewhere in the block.
444
+ const rejectedPath = fs.existsSync(statePath) ? preserveRejectedStateFile(statePath) : null;
445
+ const reason = `Error loading state: ${errorMessage(error)}`;
413
446
  const restoredState = tryRestoreFromBackup();
414
447
  if (restoredState) {
415
448
  const mtimeMs = safeGetMtimeMs(statePath);
416
- return { state: restoredState, mtimeMs };
449
+ const recovery = { source: 'backup', rejectedPath, reason };
450
+ return { state: restoredState, mtimeMs, recovery };
417
451
  }
418
452
  warn(MODULE, 'No valid backup found, starting fresh');
453
+ return { state: createFreshState(), mtimeMs: 0, recovery: { source: 'fresh', rejectedPath, reason } };
419
454
  }
420
455
  debug(MODULE, 'No existing state found, initializing...');
421
456
  return { state: createFreshState(), mtimeMs: 0 };
@@ -4,9 +4,11 @@
4
4
  * and scoring logic to repo-score-manager.ts.
5
5
  */
6
6
  import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, DailyDigest, LocalRepoCache, StatusOverride, FetchedPRStatus, StoredMergedPR, StoredClosedPR } from './types.js';
7
+ import { type LoadRecoveryInfo } from './state-persistence.js';
7
8
  import type { Stats } from './repo-score-manager.js';
8
9
  import { GistStateStore } from './gist-state-store.js';
9
10
  export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
11
+ export type { LoadRecoveryInfo } from './state-persistence.js';
10
12
  export type { Stats } from './repo-score-manager.js';
11
13
  /**
12
14
  * Push state to the backing Gist when Gist mode is active. Best-effort:
@@ -17,8 +19,15 @@ export type { Stats } from './repo-score-manager.js';
17
19
  * Intended for state-mutating PR-flow commands (shelve / unshelve / move /
18
20
  * dismiss / undismiss / claim) so Gist-sync users don't see day-long drift
19
21
  * between machines waiting for the next `daily` checkpoint. See issue #1036.
22
+ *
23
+ * @returns `null` when the push succeeded (or Gist mode is off); otherwise a
24
+ * human-readable warning the caller should surface in its structured output.
25
+ * `checkpoint()` resolves `false` without throwing when the push failed
26
+ * after its retry — previously that was discarded everywhere except
27
+ * `state --sync`, so a failed cross-machine sync reported clean success
28
+ * (#1370). stderr `warn()` alone is invisible to MCP/dashboard consumers.
20
29
  */
21
- export declare function maybeCheckpoint(stateManager: StateManager, callerModule: string): Promise<void>;
30
+ export declare function maybeCheckpoint(stateManager: StateManager, callerModule: string): Promise<string | null>;
22
31
  /**
23
32
  * Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
24
33
  *
@@ -54,6 +63,7 @@ export declare class StateManager {
54
63
  protected gistDegraded: boolean;
55
64
  private staleness;
56
65
  private lastSuccessfulRefreshAt;
66
+ private loadRecovery;
57
67
  /**
58
68
  * Create a new StateManager instance.
59
69
  * @param inMemoryOnly - When true, state is held only in memory and never read from or
@@ -157,6 +167,13 @@ export declare class StateManager {
157
167
  * Returns true if state was reloaded, false if unchanged or in-memory mode.
158
168
  */
159
169
  reloadIfChanged(): boolean;
170
+ /**
171
+ * Recovery details from the most recent state load, or null when the last
172
+ * load read the state file cleanly (or no load from disk has happened,
173
+ * e.g. in-memory mode). Set when loadState() fell back to a backup or
174
+ * fresh state because the main file was corrupt or invalid (#1371).
175
+ */
176
+ getLoadRecovery(): LoadRecoveryInfo | null;
160
177
  /**
161
178
  * Re-fetch state from the backing Gist (if in Gist mode).
162
179
  * Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
@@ -42,15 +42,30 @@ function filterValidUrlEntries(entries, kind) {
42
42
  * Intended for state-mutating PR-flow commands (shelve / unshelve / move /
43
43
  * dismiss / undismiss / claim) so Gist-sync users don't see day-long drift
44
44
  * between machines waiting for the next `daily` checkpoint. See issue #1036.
45
+ *
46
+ * @returns `null` when the push succeeded (or Gist mode is off); otherwise a
47
+ * human-readable warning the caller should surface in its structured output.
48
+ * `checkpoint()` resolves `false` without throwing when the push failed
49
+ * after its retry — previously that was discarded everywhere except
50
+ * `state --sync`, so a failed cross-machine sync reported clean success
51
+ * (#1370). stderr `warn()` alone is invisible to MCP/dashboard consumers.
45
52
  */
46
53
  export async function maybeCheckpoint(stateManager, callerModule) {
47
54
  if (!stateManager.isGistMode())
48
- return;
55
+ return null;
49
56
  try {
50
- await stateManager.checkpoint();
57
+ const pushed = await stateManager.checkpoint();
58
+ if (!pushed) {
59
+ const msg = 'Gist checkpoint push failed after retry; the local mutation is saved and will sync on the next successful push';
60
+ warn(callerModule, msg);
61
+ return msg;
62
+ }
63
+ return null;
51
64
  }
52
65
  catch (err) {
53
- warn(callerModule, `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`);
66
+ const msg = `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`;
67
+ warn(callerModule, msg);
68
+ return msg;
54
69
  }
55
70
  }
56
71
  export class StateManager {
@@ -63,6 +78,7 @@ export class StateManager {
63
78
  gistDegraded = false;
64
79
  staleness = null;
65
80
  lastSuccessfulRefreshAt = null;
81
+ loadRecovery = null;
66
82
  /**
67
83
  * Create a new StateManager instance.
68
84
  * @param inMemoryOnly - When true, state is held only in memory and never read from or
@@ -78,6 +94,7 @@ export class StateManager {
78
94
  const result = loadState();
79
95
  this.state = result.state;
80
96
  this.lastLoadedMtimeMs = result.mtimeMs;
97
+ this.loadRecovery = result.recovery ?? null;
81
98
  this.tryReconcilePRCounts();
82
99
  }
83
100
  }
@@ -97,9 +114,11 @@ export class StateManager {
97
114
  // Check if local state exists for migration
98
115
  const statePath = getStatePath();
99
116
  let result;
117
+ let localRecovery = null;
100
118
  if (fs.existsSync(statePath)) {
101
119
  // Existing user: load local state and migrate it into the Gist if no Gist exists yet.
102
120
  const localStateResult = loadState();
121
+ localRecovery = localStateResult.recovery ?? null;
103
122
  const migrationResult = await gistStore.bootstrapWithMigration(localStateResult.state);
104
123
  result = migrationResult;
105
124
  // If a new Gist was just created from local state, rename the local file
@@ -126,6 +145,9 @@ export class StateManager {
126
145
  manager.gistStore = gistStore;
127
146
  manager.gistDegraded = result.degraded ?? false;
128
147
  manager.inMemoryOnly = false; // re-enable persistence
148
+ // Surface a recovery that happened while loading the local file for Gist
149
+ // migration — the recovered (or fresh) state is what got migrated.
150
+ manager.loadRecovery = localRecovery;
129
151
  // Seed the staleness marker if bootstrap fell back to the local cache —
130
152
  // a `daily` running on a cron right after this start needs to know.
131
153
  if (result.degraded) {
@@ -335,9 +357,19 @@ export class StateManager {
335
357
  return false;
336
358
  this.state = result.state;
337
359
  this.lastLoadedMtimeMs = result.mtimeMs;
360
+ this.loadRecovery = result.recovery ?? null;
338
361
  this.tryReconcilePRCounts();
339
362
  return true;
340
363
  }
364
+ /**
365
+ * Recovery details from the most recent state load, or null when the last
366
+ * load read the state file cleanly (or no load from disk has happened,
367
+ * e.g. in-memory mode). Set when loadState() fell back to a backup or
368
+ * fresh state because the main file was corrupt or invalid (#1371).
369
+ */
370
+ getLoadRecovery() {
371
+ return this.loadRecovery;
372
+ }
341
373
  /**
342
374
  * Re-fetch state from the backing Gist (if in Gist mode).
343
375
  * Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
@@ -13,9 +13,22 @@
13
13
  * 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
14
14
  * unchanged for any input — the wrapping is lossless.
15
15
  *
16
- * Consumers should pair this with the agent-side guidance in
17
- * `workflows/reference.md` ("Prompt Injection Awareness"), which tells
18
- * the LLM to ignore instructions inside `<github-content>` blocks.
16
+ * Runtime wiring (#1372) body fields are fenced at the last serialization
17
+ * boundary into agent-facing output:
18
+ *
19
+ * - `runComments` (commands/comments.ts): review / inline / discussion
20
+ * comment bodies in the `comments` CLI `--json` + MCP tool output.
21
+ * - `fetchPRCommentBundle` (core/pr-comments-fetcher.ts): bundle bodies
22
+ * feeding `guidelines fetch-corpus` (agent-only consumer).
23
+ * - `toDailyOutput` (commands/daily.ts): `commentedIssues[].lastResponseBody`
24
+ * / `userLastCommentBody` in the daily/startup JSON. The producer
25
+ * (`issue-conversation.ts`) stays raw because the dashboard SPA and the
26
+ * CLI text renderers consume the same objects.
27
+ *
28
+ * Human-facing display paths unwrap with {@link safeExtractFromFence}.
29
+ * This pairs with the agent-side guidance in `workflows/reference.md`
30
+ * ("Prompt Injection Awareness"), which tells the LLM to treat anything
31
+ * inside `<github-content>` blocks as data, not instructions.
19
32
  *
20
33
  * Non-goals:
21
34
  * - This is NOT a content filter. We do not detect or strip prompt-
@@ -46,3 +59,11 @@ export declare function wrapUntrustedContent(text: string, label: string, meta?:
46
59
  * markdown with this; it's for tests + symmetric reasoning only.
47
60
  */
48
61
  export declare function extractFromFence(fenced: string): string;
62
+ /**
63
+ * Tolerant unwrap for human-facing display paths (CLI text mode). Returns
64
+ * the original body when `text` is a well-formed fence, and `text` unchanged
65
+ * otherwise. Display code must not crash on unfenced input (older state,
66
+ * fields that were never wrapped), so unlike {@link extractFromFence} this
67
+ * never throws.
68
+ */
69
+ export declare function safeExtractFromFence(text: string): string;
@@ -13,9 +13,22 @@
13
13
  * 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
14
14
  * unchanged for any input — the wrapping is lossless.
15
15
  *
16
- * Consumers should pair this with the agent-side guidance in
17
- * `workflows/reference.md` ("Prompt Injection Awareness"), which tells
18
- * the LLM to ignore instructions inside `<github-content>` blocks.
16
+ * Runtime wiring (#1372) body fields are fenced at the last serialization
17
+ * boundary into agent-facing output:
18
+ *
19
+ * - `runComments` (commands/comments.ts): review / inline / discussion
20
+ * comment bodies in the `comments` CLI `--json` + MCP tool output.
21
+ * - `fetchPRCommentBundle` (core/pr-comments-fetcher.ts): bundle bodies
22
+ * feeding `guidelines fetch-corpus` (agent-only consumer).
23
+ * - `toDailyOutput` (commands/daily.ts): `commentedIssues[].lastResponseBody`
24
+ * / `userLastCommentBody` in the daily/startup JSON. The producer
25
+ * (`issue-conversation.ts`) stays raw because the dashboard SPA and the
26
+ * CLI text renderers consume the same objects.
27
+ *
28
+ * Human-facing display paths unwrap with {@link safeExtractFromFence}.
29
+ * This pairs with the agent-side guidance in `workflows/reference.md`
30
+ * ("Prompt Injection Awareness"), which tells the LLM to treat anything
31
+ * inside `<github-content>` blocks as data, not instructions.
19
32
  *
20
33
  * Non-goals:
21
34
  * - This is NOT a content filter. We do not detect or strip prompt-
@@ -104,3 +117,18 @@ export function extractFromFence(fenced) {
104
117
  .split(AMP_ESCAPE)
105
118
  .join('&');
106
119
  }
120
+ /**
121
+ * Tolerant unwrap for human-facing display paths (CLI text mode). Returns
122
+ * the original body when `text` is a well-formed fence, and `text` unchanged
123
+ * otherwise. Display code must not crash on unfenced input (older state,
124
+ * fields that were never wrapped), so unlike {@link extractFromFence} this
125
+ * never throws.
126
+ */
127
+ export function safeExtractFromFence(text) {
128
+ try {
129
+ return extractFromFence(text);
130
+ }
131
+ catch {
132
+ return text;
133
+ }
134
+ }
@@ -50,7 +50,7 @@ export interface CompactRepoGroup {
50
50
  * See `DailyWarning` and issue #1042 for the rationale — keeping this a
51
51
  * fixed union so downstream consumers can switch on it without drift.
52
52
  */
53
- export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness';
53
+ export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness' | 'state-load';
54
54
  /**
55
55
  * A single non-fatal failure surfaced from the `daily` pipeline. Unlike
56
56
  * `PRCheckFailure` (which is scoped to per-PR fetch errors), this covers
@@ -176,6 +176,7 @@ export declare const StatusOutputSchema: z.ZodObject<{
176
176
  "dismiss-filter": "dismiss-filter";
177
177
  "gist-checkpoint": "gist-checkpoint";
178
178
  "gist-staleness": "gist-staleness";
179
+ "state-load": "state-load";
179
180
  }>;
180
181
  operation: z.ZodString;
181
182
  message: z.ZodString;
@@ -378,6 +379,7 @@ export declare const DailyOutputSchema: z.ZodObject<{
378
379
  "dismiss-filter": "dismiss-filter";
379
380
  "gist-checkpoint": "gist-checkpoint";
380
381
  "gist-staleness": "gist-staleness";
382
+ "state-load": "state-load";
381
383
  }>;
382
384
  operation: z.ZodString;
383
385
  message: z.ZodString;
@@ -518,6 +520,7 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
518
520
  "dismiss-filter": "dismiss-filter";
519
521
  "gist-checkpoint": "gist-checkpoint";
520
522
  "gist-staleness": "gist-staleness";
523
+ "state-load": "state-load";
521
524
  }>;
522
525
  operation: z.ZodString;
523
526
  message: z.ZodString;
@@ -620,6 +623,8 @@ export declare const SearchOutputSchema: z.ZodObject<{
620
623
  updatedAt: z.ZodOptional<z.ZodString>;
621
624
  isStalled: z.ZodBoolean;
622
625
  }, z.core.$strip>>;
626
+ boostScore: z.ZodOptional<z.ZodNumber>;
627
+ boostReasons: z.ZodOptional<z.ZodArray<z.ZodString>>;
623
628
  }, z.core.$strip>>;
624
629
  excludedRepos: z.ZodArray<z.ZodString>;
625
630
  aiPolicyBlocklist: z.ZodArray<z.ZodString>;
@@ -676,6 +681,8 @@ export declare const FeaturesOutputSchema: z.ZodObject<{
676
681
  updatedAt: z.ZodOptional<z.ZodString>;
677
682
  isStalled: z.ZodBoolean;
678
683
  }, z.core.$strip>>;
684
+ boostScore: z.ZodOptional<z.ZodNumber>;
685
+ boostReasons: z.ZodOptional<z.ZodArray<z.ZodString>>;
679
686
  horizon: z.ZodEnum<{
680
687
  "quick-win": "quick-win";
681
688
  "bigger-bet": "bigger-bet";
@@ -731,6 +738,8 @@ export declare const FeaturesOutputSchema: z.ZodObject<{
731
738
  updatedAt: z.ZodOptional<z.ZodString>;
732
739
  isStalled: z.ZodBoolean;
733
740
  }, z.core.$strip>>;
741
+ boostScore: z.ZodOptional<z.ZodNumber>;
742
+ boostReasons: z.ZodOptional<z.ZodArray<z.ZodString>>;
734
743
  horizon: z.ZodEnum<{
735
744
  "quick-win": "quick-win";
736
745
  "bigger-bet": "bigger-bet";
@@ -792,6 +801,7 @@ export declare const PostOutputSchema: z.ZodObject<{
792
801
  export declare const ClaimOutputSchema: z.ZodObject<{
793
802
  commentUrl: z.ZodString;
794
803
  issueUrl: z.ZodString;
804
+ gistSyncWarning: z.ZodOptional<z.ZodString>;
795
805
  }, z.core.$strip>;
796
806
  export declare const InitOutputSchema: z.ZodObject<{
797
807
  username: z.ZodString;
@@ -859,6 +869,7 @@ export declare const MoveOutputSchema: z.ZodObject<{
859
869
  shelved: "shelved";
860
870
  }>;
861
871
  description: z.ZodString;
872
+ gistSyncWarning: z.ZodOptional<z.ZodString>;
862
873
  }, z.core.$strip>;
863
874
  export declare const PRTemplateOutputSchema: z.ZodObject<{
864
875
  template: z.ZodNullable<z.ZodString>;
@@ -893,6 +904,7 @@ export declare const RepoVetOutputSchema: z.ZodObject<{
893
904
  lastCommitISO: z.ZodNullable<z.ZodString>;
894
905
  contributorsLast90d: z.ZodNumber;
895
906
  lastReleaseISO: z.ZodNullable<z.ZodString>;
907
+ releasesIncomplete: z.ZodOptional<z.ZodBoolean>;
896
908
  }, z.core.$strip>;
897
909
  communityHealth: z.ZodObject<{
898
910
  contributing: z.ZodBoolean;
@@ -1392,6 +1404,8 @@ export interface PostOutput {
1392
1404
  export interface ClaimOutput {
1393
1405
  commentUrl: string;
1394
1406
  issueUrl: string;
1407
+ /** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1370). */
1408
+ gistSyncWarning?: string;
1395
1409
  }
1396
1410
  /** Info about a local git clone (#84) */
1397
1411
  export interface LocalRepoInfo {
@@ -87,6 +87,7 @@ const DailyWarningPhaseSchema = z.enum([
87
87
  'dismiss-filter',
88
88
  'gist-checkpoint',
89
89
  'gist-staleness',
90
+ 'state-load',
90
91
  ]);
91
92
  const DailyWarningSchema = z.object({
92
93
  phase: DailyWarningPhaseSchema,
@@ -335,6 +336,14 @@ const SearchCandidateSchema = z.object({
335
336
  })
336
337
  .optional(),
337
338
  linkedPR: CandidateLinkedPRSchema.optional(),
339
+ /**
340
+ * Personalization sort-tier signal threaded up from oss-scout (#1244).
341
+ * Present only when local strategy data biased the search and this
342
+ * candidate matched. `boostReasons` is the human-readable explanation
343
+ * (e.g. `"repo affinity: vercel/next.js"`, `"language match: TypeScript"`).
344
+ */
345
+ boostScore: z.number().optional(),
346
+ boostReasons: z.array(z.string()).optional(),
338
347
  });
339
348
  export const SearchOutputSchema = z.object({
340
349
  candidates: z.array(SearchCandidateSchema),
@@ -409,6 +418,8 @@ export const PostOutputSchema = z.object({
409
418
  export const ClaimOutputSchema = z.object({
410
419
  commentUrl: z.string(),
411
420
  issueUrl: z.string(),
421
+ // Post-mutation Gist checkpoint failure (#1370). Optional: absent on clean runs.
422
+ gistSyncWarning: z.string().optional(),
412
423
  });
413
424
  export const InitOutputSchema = z.object({
414
425
  username: z.string(),
@@ -484,6 +495,8 @@ export const MoveOutputSchema = z.object({
484
495
  url: z.string(),
485
496
  target: z.enum(['attention', 'waiting', 'shelved', 'auto']),
486
497
  description: z.string(),
498
+ // Post-mutation Gist checkpoint failure (#1370). Optional: absent on clean runs.
499
+ gistSyncWarning: z.string().optional(),
487
500
  });
488
501
  export const PRTemplateOutputSchema = z.object({
489
502
  template: z.string().nullable(),
@@ -527,6 +540,13 @@ export const RepoVetOutputSchema = z.object({
527
540
  lastCommitISO: z.string().nullable(),
528
541
  contributorsLast90d: z.number().int().nonnegative(),
529
542
  lastReleaseISO: z.string().nullable(),
543
+ /**
544
+ * True when the release listing failed for a non-404 reason (5xx,
545
+ * network), so a `null` lastReleaseISO means "could not determine"
546
+ * rather than "confirmed no releases". The release-list analogue of
547
+ * `communityHealth.incomplete` (#1373).
548
+ */
549
+ releasesIncomplete: z.boolean().optional(),
530
550
  }),
531
551
  communityHealth: z.object({
532
552
  contributing: z.boolean(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "3.9.0",
3
+ "version": "3.11.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,18 +54,18 @@
54
54
  "dependencies": {
55
55
  "@octokit/plugin-throttling": "^11.0.3",
56
56
  "@octokit/rest": "^22.0.1",
57
- "@oss-scout/core": "^0.9.0",
58
- "commander": "^14.0.3",
57
+ "@oss-scout/core": "^0.11.0",
58
+ "commander": "^15.0.0",
59
59
  "zod": "^4.4.3"
60
60
  },
61
61
  "devDependencies": {
62
- "@types/node": "^25.7.0",
63
- "@vitest/coverage-v8": "^4.1.6",
62
+ "@types/node": "^25.9.1",
63
+ "@vitest/coverage-v8": "^4.1.8",
64
64
  "esbuild": "^0.28.0",
65
- "tsx": "^4.21.0",
65
+ "tsx": "^4.22.4",
66
66
  "typedoc": "^0.28.19",
67
67
  "typescript": "^5.9.3",
68
- "vitest": "^4.1.6"
68
+ "vitest": "^4.1.8"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsc",