@oss-autopilot/core 3.3.0 → 3.4.1

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.js CHANGED
@@ -32,9 +32,23 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
32
32
  enableDebug();
33
33
  debug('cli', `Running command: ${actionCommand.name()}`);
34
34
  }
35
- // actionCommand is the command being executed (e.g., 'status', 'daily')
36
- const commandName = actionCommand.name();
37
- if (!localOnlySet.has(commandName)) {
35
+ // actionCommand is the command being executed (e.g., 'status', 'daily').
36
+ // For subcommand groups (e.g. `guidelines view`), Commander returns the
37
+ // leaf name `view` — but the registry sets `localOnly` on the parent
38
+ // entry `guidelines`. Walk the parent chain so a `localOnly` ancestor
39
+ // covers all its leaves (#1208 M2). Without this, `guidelines view` —
40
+ // which works fine in local mode (returns storageMode: 'local-unavailable')
41
+ // — would still hit the auth gate and fail.
42
+ let cmd = actionCommand;
43
+ let isLocalOnly = false;
44
+ while (cmd) {
45
+ if (localOnlySet.has(cmd.name())) {
46
+ isLocalOnly = true;
47
+ break;
48
+ }
49
+ cmd = cmd.parent;
50
+ }
51
+ if (!isLocalOnly) {
38
52
  const token = await getGitHubTokenAsync();
39
53
  if (!token) {
40
54
  // Honor --json at the CLI boundary so machine consumers (plugins, MCP
@@ -6,17 +6,11 @@
6
6
  * Domain logic lives in src/core/daily-logic.ts; this file is a thin
7
7
  * orchestration layer that wires up the phases and handles I/O.
8
8
  */
9
- import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup, type AgentState, type StarFilter } from '../core/index.js';
9
+ import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup } from '../core/index.js';
10
10
  import { type DailyOutput, type DailyWarning, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
11
11
  export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
12
- /**
13
- * Build a star filter from state for use in fetchUserPRCounts.
14
- * Returns undefined if no star data is available (first run).
15
- *
16
- * @param state - Current agent state (read-only)
17
- * @returns Star filter with minimum threshold and known counts, or undefined on first run
18
- */
19
- export declare function buildStarFilter(state: Readonly<AgentState>): StarFilter | undefined;
12
+ import { buildStarFilter } from '../core/daily-logic.js';
13
+ export { buildStarFilter };
20
14
  /**
21
15
  * Internal result of the daily check, using full (non-deduplicated) types.
22
16
  * Consumed by printDigest() (text mode) and converted to DailyOutput (JSON mode)
@@ -39,26 +39,12 @@ function nonFatalCatchWithWarning(opts) {
39
39
  // Re-export domain functions so existing consumers (tests, dashboard, startup)
40
40
  // can continue importing from './daily.js' without changes.
41
41
  export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
42
- /**
43
- * Build a star filter from state for use in fetchUserPRCounts.
44
- * Returns undefined if no star data is available (first run).
45
- *
46
- * @param state - Current agent state (read-only)
47
- * @returns Star filter with minimum threshold and known counts, or undefined on first run
48
- */
49
- export function buildStarFilter(state) {
50
- const minStars = state.config.minStars ?? 50;
51
- const knownStarCounts = new Map();
52
- for (const [repo, score] of Object.entries(state.repoScores)) {
53
- if (score.stargazersCount !== undefined) {
54
- knownStarCounts.set(repo, score.stargazersCount);
55
- }
56
- }
57
- // Only filter if we have some star data to work with
58
- if (knownStarCounts.size === 0)
59
- return undefined;
60
- return { minStars, knownStarCounts };
61
- }
42
+ // buildStarFilter moved to core/daily-logic.ts so the dashboard layer can
43
+ // reuse it without crossing the commands sibling-command boundary
44
+ // (#1208 M7). Re-exported here for backward compatibility with anyone
45
+ // importing from this module.
46
+ import { buildStarFilter } from '../core/daily-logic.js';
47
+ export { buildStarFilter };
62
48
  // ---------------------------------------------------------------------------
63
49
  // Phase functions
64
50
  // ---------------------------------------------------------------------------
@@ -9,7 +9,7 @@ import { warn } from '../core/logger.js';
9
9
  import { emptyPRCountsResult, fetchMergedPRsSince, fetchClosedPRsSince } from '../core/github-stats.js';
10
10
  import { parseGitHubUrl } from '../core/urls.js';
11
11
  import { isBelowMinStars, } from '../core/types.js';
12
- import { toShelvedPRRef, buildStarFilter } from './daily.js';
12
+ import { toShelvedPRRef, buildStarFilter } from '../core/index.js';
13
13
  const MODULE = 'dashboard-data';
14
14
  export function buildDashboardStats(digest, state, storedMergedCount, storedClosedCount) {
15
15
  const summary = digest.summary || {
@@ -265,6 +265,11 @@ export async function startDashboardServer(options) {
265
265
  // not disappear when /api/data rebuilds after a state change or after a
266
266
  // POST /api/action completes.
267
267
  let cachedPartialFailures = undefined;
268
+ // Tracks the last background-refresh failure so /api/data can surface
269
+ // staleness to the SPA via the X-Dashboard-Stale header (#1205). Cleared
270
+ // when a refresh succeeds. Without this, token expiry / GitHub outage
271
+ // produces silent stale data hours old with no client-visible signal.
272
+ let lastBackgroundRefreshError = null;
268
273
  if (!cachedDigest) {
269
274
  throw new Error('No dashboard data available. Run the daily check first: GITHUB_TOKEN=$(gh auth token) npm start -- daily');
270
275
  }
@@ -327,6 +332,14 @@ export async function startDashboardServer(options) {
327
332
  res.setHeader('X-Dashboard-Stale', '1');
328
333
  }
329
334
  }
335
+ // Surface staleness from a failed background refresh too (#1205) so
336
+ // token expiry / GitHub outage produces a client-visible signal
337
+ // rather than silent stale data. Only set the header when a failure
338
+ // is recorded — successful refreshes clear it.
339
+ if (lastBackgroundRefreshError !== null) {
340
+ res.setHeader('X-Dashboard-Stale', '1');
341
+ res.setHeader('X-Dashboard-Stale-Reason', `background-refresh-failed: ${lastBackgroundRefreshError}`);
342
+ }
330
343
  sendJson(res, 200, cachedJsonData);
331
344
  return;
332
345
  }
@@ -598,11 +611,16 @@ export async function startDashboardServer(options) {
598
611
  cachedPartialFailures = result.partialFailures.length > 0 ? result.partialFailures : undefined;
599
612
  cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs, cachedPartialFailures);
600
613
  cachedIssueListMtimeMs = getIssueListMtimeMs();
614
+ // Successful refresh clears any prior failure signal (#1205).
615
+ lastBackgroundRefreshError = null;
601
616
  warn(MODULE, 'Background data refresh complete');
602
617
  return;
603
618
  })
604
619
  .catch((error) => {
605
- warn(MODULE, `Background data refresh failed (serving cached data): ${errorMessage(error)}`);
620
+ // Capture so /api/data can surface staleness via X-Dashboard-Stale
621
+ // header — previously the catch only logged to stderr (#1205).
622
+ lastBackgroundRefreshError = errorMessage(error);
623
+ warn(MODULE, `Background data refresh failed (serving cached data): ${lastBackgroundRefreshError}`);
606
624
  });
607
625
  }
608
626
  // ── Open browser ─────────────────────────────────────────────────────────
@@ -27,6 +27,16 @@ export interface FetchCorpusOutput {
27
27
  prCount: number;
28
28
  /** PRs skipped because `commentsFetchedAt` is already set (without --force). */
29
29
  skipped: number;
30
+ /**
31
+ * PRs that were attempted but errored (404, rate limit, transient API
32
+ * failure). Surfaced so the host can decide whether to retry or warn the
33
+ * user that the corpus is partial. Empty array when all attempted fetches
34
+ * succeeded. (#1209 L8)
35
+ */
36
+ failures: Array<{
37
+ prUrl: string;
38
+ error: string;
39
+ }>;
30
40
  }
31
41
  interface RepoOption {
32
42
  repo: string;
@@ -108,7 +108,11 @@ export async function runFetchCorpus(options) {
108
108
  const eligible = candidates.filter((c) => {
109
109
  if (!c.url.startsWith(repoUrlPrefix))
110
110
  return false;
111
- if (Date.parse(c.timestamp || '') < cutoffMs)
111
+ // Date.parse('') is NaN, and `NaN < cutoffMs` is false — the previous
112
+ // form silently passed PRs with empty/malformed timestamps through the
113
+ // recency cliff (#1204). Number.isFinite filters those out explicitly.
114
+ const ts = Date.parse(c.timestamp || '');
115
+ if (!Number.isFinite(ts) || ts < cutoffMs)
112
116
  return false;
113
117
  return true;
114
118
  });
@@ -118,10 +122,12 @@ export async function runFetchCorpus(options) {
118
122
  const skipped = options.forceRefetch ? 0 : eligible.filter((c) => c.alreadyFetched).length;
119
123
  const toFetch = (options.forceRefetch ? eligible : eligible.filter((c) => !c.alreadyFetched))
120
124
  // Most-recent first so the host always sees the freshest signal in its corpus window.
125
+ // After the eligibility filter (#1204), every entry has a finite Date.parse,
126
+ // so the comparator is well-defined.
121
127
  .sort((a, b) => Date.parse(b.timestamp || '') - Date.parse(a.timestamp || ''))
122
128
  .slice(0, limit);
123
129
  if (toFetch.length === 0) {
124
- return { repo: options.repo, bundles: [], prCount: 0, skipped };
130
+ return { repo: options.repo, bundles: [], prCount: 0, skipped, failures: [] };
125
131
  }
126
132
  const token = requireGitHubToken();
127
133
  const octokit = getOctokit(token);
@@ -129,7 +135,7 @@ export async function runFetchCorpus(options) {
129
135
  if (!username) {
130
136
  warn(MODULE, 'githubUsername is not set; bot/own-comment filtering will be incomplete');
131
137
  }
132
- const bundles = await fetchPRCommentBundlesBatch(octokit, toFetch.map((c) => c.url), username);
138
+ const { bundles, failures } = await fetchPRCommentBundlesBatch(octokit, toFetch.map((c) => c.url), username);
133
139
  // Stamp commentsFetchedAt on each PR we successfully fetched — the bundle
134
140
  // list mirrors the input order until paginateAll fan-out, so we mark on
135
141
  // a per-bundle basis to be safe.
@@ -148,6 +154,7 @@ export async function runFetchCorpus(options) {
148
154
  bundles,
149
155
  prCount: bundles.length,
150
156
  skipped,
157
+ failures,
151
158
  };
152
159
  }
153
160
  function clampLimit(limit) {
@@ -2,6 +2,7 @@
2
2
  * Shared validation patterns and helpers for CLI commands.
3
3
  */
4
4
  import { ValidationError } from '../core/errors.js';
5
+ import { isPlaceholderUsername } from '../core/placeholder-usernames.js';
5
6
  /** Matches GitHub PR URLs: https://github.com/owner/repo/pull/123 */
6
7
  export const PR_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+$/;
7
8
  /** Matches GitHub issue URLs: https://github.com/owner/repo/issues/123 */
@@ -106,5 +107,13 @@ export function validateGitHubUsername(username) {
106
107
  if (CONSECUTIVE_HYPHENS_PATTERN.test(trimmed)) {
107
108
  throw new ValidationError('GitHub username cannot contain consecutive hyphens.');
108
109
  }
110
+ // Reject the same placeholder strings that pr-monitor's runtime auto-repair
111
+ // catches. Without this guard `init`, `setup --set username=`, and the MCP
112
+ // `config` tool happily persist values like "example-user" copied from
113
+ // documentation, leaving auto-repair to clean up after the next fetch and
114
+ // surfacing a "showing partial data" banner on the dashboard in the meantime.
115
+ if (isPlaceholderUsername(trimmed)) {
116
+ throw new ValidationError(`"${trimmed}" looks like a placeholder from documentation, not a real GitHub username. Use your actual GitHub login.`);
117
+ }
109
118
  return trimmed;
110
119
  }
package/dist/core/auth.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { execFileSync, execFile } from 'node:child_process';
10
10
  import { ConfigurationError } from './errors.js';
11
- import { debug } from './logger.js';
11
+ import { debug, warn } from './logger.js';
12
12
  const MODULE = 'auth';
13
13
  // Cached GitHub token (fetched once per session)
14
14
  let cachedGitHubToken = null;
@@ -47,7 +47,10 @@ export function getGitHubToken() {
47
47
  }
48
48
  }
49
49
  catch (err) {
50
- debug(MODULE, 'gh auth token failed (CLI unavailable or not authenticated)', err);
50
+ // Promote to warn-once-per-session so a slow `gh` (2s timeout) or a
51
+ // misconfigured CLI is visible without DEBUG=1 (#1209 L6). The
52
+ // tokenFetchAttempted cache means subsequent calls don't re-warn.
53
+ warn(MODULE, `gh auth token failed (CLI unavailable or not authenticated): ${err instanceof Error ? err.message : String(err)}`);
51
54
  }
52
55
  return null;
53
56
  }
@@ -117,7 +120,8 @@ export async function getGitHubTokenAsync() {
117
120
  }
118
121
  }
119
122
  catch (err) {
120
- debug(MODULE, 'gh auth token failed (CLI unavailable or not authenticated)', err);
123
+ // Same warn-once promotion as the sync version (#1209 L6).
124
+ warn(MODULE, `gh auth token failed (CLI unavailable or not authenticated): ${err instanceof Error ? err.message : String(err)}`);
121
125
  }
122
126
  return null;
123
127
  }
@@ -14,7 +14,7 @@
14
14
  * re-exports them at the bottom so existing imports keep working without
15
15
  * a sweep.
16
16
  */
17
- import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, AgentState, ShelvedPRRef, ComputedRepoSignals, RepoGroup, CommentedIssue, CapacityAssessment, ActionableIssue, ActionMenu } from './types.js';
17
+ import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, AgentState, ShelvedPRRef, ComputedRepoSignals, RepoGroup, CommentedIssue, CapacityAssessment, ActionableIssue, ActionMenu, StarFilter } from './types.js';
18
18
  /**
19
19
  * Statuses indicating action needed from the contributor.
20
20
  * Used for auto-unshelving shelved PRs.
@@ -49,6 +49,18 @@ export declare function applyStatusOverrides(prs: FetchedPR[], state: Readonly<A
49
49
  * @returns Lightweight reference for display
50
50
  */
51
51
  export declare function toShelvedPRRef(pr: ShelvedPRRef): ShelvedPRRef;
52
+ /**
53
+ * Build a star filter from state for use in fetchUserPRCounts.
54
+ *
55
+ * Returns undefined if no star data is available (first run). Pure logic
56
+ * over `Readonly<AgentState>` — lives here in core/daily-logic.ts so the
57
+ * dashboard layer can reuse it without importing from a sibling command
58
+ * module (#1208 M7).
59
+ *
60
+ * @param state - Current agent state (read-only)
61
+ * @returns Star filter with minimum threshold and known counts, or undefined on first run
62
+ */
63
+ export declare function buildStarFilter(state: Readonly<AgentState>): StarFilter | undefined;
52
64
  /**
53
65
  * Group PRs by repository (#80).
54
66
  * Ensures one agent per repo during parallel dispatch, preventing branch checkout conflicts.
@@ -129,6 +129,29 @@ export function toShelvedPRRef(pr) {
129
129
  status: pr.status,
130
130
  };
131
131
  }
132
+ /**
133
+ * Build a star filter from state for use in fetchUserPRCounts.
134
+ *
135
+ * Returns undefined if no star data is available (first run). Pure logic
136
+ * over `Readonly<AgentState>` — lives here in core/daily-logic.ts so the
137
+ * dashboard layer can reuse it without importing from a sibling command
138
+ * module (#1208 M7).
139
+ *
140
+ * @param state - Current agent state (read-only)
141
+ * @returns Star filter with minimum threshold and known counts, or undefined on first run
142
+ */
143
+ export function buildStarFilter(state) {
144
+ const minStars = state.config.minStars ?? 50;
145
+ const knownStarCounts = new Map();
146
+ for (const [repo, score] of Object.entries(state.repoScores)) {
147
+ if (score.stargazersCount !== undefined) {
148
+ knownStarCounts.set(repo, score.stargazersCount);
149
+ }
150
+ }
151
+ if (knownStarCounts.size === 0)
152
+ return undefined;
153
+ return { minStars, knownStarCounts };
154
+ }
132
155
  /**
133
156
  * Group PRs by repository (#80).
134
157
  * Ensures one agent per repo during parallel dispatch, preventing branch checkout conflicts.
@@ -44,6 +44,22 @@ export interface BootstrapResult {
44
44
  /** True when state was loaded from local cache due to API failure. */
45
45
  degraded?: boolean;
46
46
  }
47
+ /**
48
+ * Discriminated outcome of {@link GistStateStore.refreshFromGist} (#1209 L9).
49
+ * Lets callers distinguish "got fresh data" from "throttled / no-op / failed"
50
+ * without conflating them as a single boolean.
51
+ */
52
+ export type RefreshResult = {
53
+ status: 'refreshed';
54
+ } | {
55
+ status: 'no-gist';
56
+ } | {
57
+ status: 'throttled';
58
+ sinceLastMs: number;
59
+ } | {
60
+ status: 'error';
61
+ error: Error;
62
+ };
47
63
  /**
48
64
  * Minimal Octokit-shaped interface for the Gist API methods we use.
49
65
  * Accepts the real ThrottledOctokit or a plain mock object in tests.
@@ -194,8 +210,15 @@ export declare class GistStateStore {
194
210
  /**
195
211
  * Re-fetch the Gist and update the in-memory cache.
196
212
  * Throttled to at most once per 30 seconds.
213
+ *
214
+ * Returns a discriminated union so callers can tell apart the four
215
+ * outcomes that previously collapsed into a single boolean (#1209 L9):
216
+ * - `{ status: 'refreshed' }` — fresh data loaded successfully.
217
+ * - `{ status: 'no-gist' }` — store not in Gist mode (e.g. degraded).
218
+ * - `{ status: 'throttled', sinceLastMs }` — within the 30s throttle.
219
+ * - `{ status: 'error', error }` — fetch attempt failed.
197
220
  */
198
- refreshFromGist(): Promise<boolean>;
221
+ refreshFromGist(): Promise<RefreshResult>;
199
222
  /**
200
223
  * Preflight check: verify the token has Gist API scope.
201
224
  * Costs one cheap API call; catches permission issues early with a clear message.
@@ -422,25 +422,35 @@ export class GistStateStore {
422
422
  /**
423
423
  * Re-fetch the Gist and update the in-memory cache.
424
424
  * Throttled to at most once per 30 seconds.
425
+ *
426
+ * Returns a discriminated union so callers can tell apart the four
427
+ * outcomes that previously collapsed into a single boolean (#1209 L9):
428
+ * - `{ status: 'refreshed' }` — fresh data loaded successfully.
429
+ * - `{ status: 'no-gist' }` — store not in Gist mode (e.g. degraded).
430
+ * - `{ status: 'throttled', sinceLastMs }` — within the 30s throttle.
431
+ * - `{ status: 'error', error }` — fetch attempt failed.
425
432
  */
426
433
  async refreshFromGist() {
427
434
  if (!this.gistId)
428
- return false;
435
+ return { status: 'no-gist' };
429
436
  const now = Date.now();
437
+ const sinceLastMs = now - this.lastRefreshAt;
430
438
  // Throttle hits are not failures — preserve any previous lastRefreshError
431
439
  // for the caller to inspect.
432
- if (now - this.lastRefreshAt < GistStateStore.REFRESH_THROTTLE_MS)
433
- return false;
440
+ if (sinceLastMs < GistStateStore.REFRESH_THROTTLE_MS) {
441
+ return { status: 'throttled', sinceLastMs };
442
+ }
434
443
  try {
435
444
  await this.fetchAndCache(this.gistId);
436
445
  this.lastRefreshAt = now;
437
446
  this.lastRefreshError = null;
438
- return true;
447
+ return { status: 'refreshed' };
439
448
  }
440
449
  catch (err) {
441
- warn(MODULE, `refreshFromGist failed: ${err}`);
442
- this.lastRefreshError = err instanceof Error ? err : new Error(String(err));
443
- return false;
450
+ const error = err instanceof Error ? err : new Error(String(err));
451
+ warn(MODULE, `refreshFromGist failed: ${error.message}`);
452
+ this.lastRefreshError = error;
453
+ return { status: 'error', error };
444
454
  }
445
455
  }
446
456
  // ── Private helpers ─────────────────────────────────────────────────
@@ -85,7 +85,16 @@ export class HttpCache {
85
85
  }
86
86
  return entry;
87
87
  }
88
- catch {
88
+ catch (err) {
89
+ // ENOENT (cache miss) is the common case and not worth logging — but
90
+ // EISDIR / JSON parse / disk corruption all looked identical to a miss
91
+ // before, hiding repeated cache misses caused by a corrupt entry. Log
92
+ // anything that's not "file not found" so the cause becomes
93
+ // diagnosable in DEBUG mode (#1209 L5).
94
+ if (err && typeof err === 'object' && err.code !== 'ENOENT') {
95
+ const msg = err instanceof Error ? err.message : 'unknown error';
96
+ debug(MODULE, `Cache read failed for ${url} (treating as miss): ${msg}`);
97
+ }
89
98
  return null;
90
99
  }
91
100
  }
@@ -18,7 +18,7 @@ export { DEFAULT_CONCURRENCY } from './concurrency.js';
18
18
  export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
19
19
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
20
20
  export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
21
- export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
21
+ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
22
22
  export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
23
23
  export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
24
24
  export { classifyLinkedPR, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
@@ -26,4 +26,5 @@ export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type Ant
26
26
  export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
27
27
  export { CONFIG_KEY_REGISTRY, type ConfigKeyDef, type SettableVia, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
28
28
  export { DashboardDataSchema, DashboardStatsSchema, validateDashboardData, type DashboardDataParsed, } from './dashboard-data-schema.js';
29
+ export { fetchPRCommentBundle, fetchPRCommentBundlesBatch, type PRCommentBundle, type PRReviewEntry, type PRReviewCommentEntry, type PRIssueCommentEntry, } from './pr-comments-fetcher.js';
29
30
  export * from './types.js';
@@ -19,7 +19,7 @@ export { DEFAULT_CONCURRENCY } from './concurrency.js';
19
19
  export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
20
20
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
21
21
  export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
22
- export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
22
+ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
23
23
  export { computeContributionStats } from './stats.js';
24
24
  export { fetchPRTemplate } from './pr-template.js';
25
25
  export { classifyLinkedPR, } from './linked-pr-classification.js';
@@ -27,4 +27,8 @@ export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
27
27
  export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
28
28
  export { CONFIG_KEY_REGISTRY, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
29
29
  export { DashboardDataSchema, DashboardStatsSchema, validateDashboardData, } from './dashboard-data-schema.js';
30
+ // PR comment bundle types — wire shape consumed by the MCP extract-learnings
31
+ // prompt. Re-exported so MCP doesn't have to redeclare the interface and
32
+ // silently drift (#1208 M5).
33
+ export { fetchPRCommentBundle, fetchPRCommentBundlesBatch, } from './pr-comments-fetcher.js';
30
34
  export * from './types.js';
@@ -0,0 +1 @@
1
+ export declare function isPlaceholderUsername(username: string): boolean;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Known placeholder values that can end up in `config.githubUsername` from
3
+ * doc snippets, example configs, or aborted setup flows.
4
+ *
5
+ * Two callers consult this list:
6
+ * - `pr-monitor.ts` — runtime auto-repair: if a fetch is about to run with
7
+ * a placeholder, the configured value is replaced with the authenticated
8
+ * viewer's login before the search hits GitHub. Without this, the search
9
+ * silently returns zero results and the dashboard looks like a fresh install.
10
+ * - `commands/validation.ts` — write-side rejection: prevents `init`,
11
+ * `setup --set username=`, and the MCP `config` tool from persisting one
12
+ * of these values in the first place, so the auto-repair only runs as a
13
+ * fallback for legacy state rather than masking a fresh user error.
14
+ *
15
+ * Entries must be lowercase — `Lowercase<string>` on the source tuple makes a
16
+ * non-lowercase entry a compile error, keeping the case-insensitive lookup
17
+ * contract type-checked instead of comment-documented.
18
+ */
19
+ const PLACEHOLDER_USERNAMES = [
20
+ 'example-user',
21
+ 'your-username',
22
+ 'your-github-username',
23
+ // GitHub's mascot accounts. Real users on github.com but seeded into countless
24
+ // example configs, READMEs, and SDK docs (Octokit's quickstarts use `octocat`
25
+ // as the canonical login). Treating them as placeholders keeps a stale example
26
+ // value from silently swapping a real user's PR feed with the mascot's open
27
+ // PRs in violet-org/boysenberry-repo (octocat's public test fixtures).
28
+ 'octocat',
29
+ 'monalisa',
30
+ ];
31
+ const KNOWN_PLACEHOLDER_USERNAMES = new Set(PLACEHOLDER_USERNAMES);
32
+ export function isPlaceholderUsername(username) {
33
+ return KNOWN_PLACEHOLDER_USERNAMES.has(username.toLowerCase());
34
+ }
@@ -58,10 +58,18 @@ export declare function fetchPRCommentBundle(octokit: Octokit, prUrl: string, gi
58
58
  /**
59
59
  * Fetch comment bundles for many PRs with a small concurrency cap (default 3).
60
60
  *
61
- * Failures on individual PRs are logged and skipped — the batch returns a
62
- * shorter array rather than aborting. Rationale: extraction quality is
63
- * already a partial-information problem (users contribute to many repos and
64
- * many PRs), so a single 404 / rate limit on one PR should not deny the
65
- * host the corpus from the other 4.
61
+ * Failures on individual PRs are logged and recorded — the batch returns
62
+ * `{ bundles, failures }` so the caller can decide whether to retry, surface
63
+ * a partial-data banner, or proceed. Rationale: extraction quality is already
64
+ * a partial-information problem (users contribute to many repos and many PRs),
65
+ * so a single 404 / rate limit on one PR should not deny the host the corpus
66
+ * from the other 4 — but the failure should still be visible (#1209 L8).
66
67
  */
67
- export declare function fetchPRCommentBundlesBatch(octokit: Octokit, prUrls: string[], githubUsername: string, concurrency?: number): Promise<PRCommentBundle[]>;
68
+ export interface PRCommentBundlesBatchResult {
69
+ bundles: PRCommentBundle[];
70
+ failures: Array<{
71
+ prUrl: string;
72
+ error: string;
73
+ }>;
74
+ }
75
+ export declare function fetchPRCommentBundlesBatch(octokit: Octokit, prUrls: string[], githubUsername: string, concurrency?: number): Promise<PRCommentBundlesBatchResult>;
@@ -92,17 +92,9 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
92
92
  })),
93
93
  };
94
94
  }
95
- /**
96
- * Fetch comment bundles for many PRs with a small concurrency cap (default 3).
97
- *
98
- * Failures on individual PRs are logged and skipped — the batch returns a
99
- * shorter array rather than aborting. Rationale: extraction quality is
100
- * already a partial-information problem (users contribute to many repos and
101
- * many PRs), so a single 404 / rate limit on one PR should not deny the
102
- * host the corpus from the other 4.
103
- */
104
95
  export async function fetchPRCommentBundlesBatch(octokit, prUrls, githubUsername, concurrency = DEFAULT_BATCH_CONCURRENCY) {
105
- const results = [];
96
+ const bundles = [];
97
+ const failures = [];
106
98
  const queue = [...prUrls];
107
99
  async function worker() {
108
100
  while (queue.length > 0) {
@@ -111,15 +103,17 @@ export async function fetchPRCommentBundlesBatch(octokit, prUrls, githubUsername
111
103
  return;
112
104
  try {
113
105
  const bundle = await fetchPRCommentBundle(octokit, url, githubUsername);
114
- results.push(bundle);
106
+ bundles.push(bundle);
115
107
  }
116
108
  catch (err) {
117
- warn(MODULE, `Skipping ${url}: ${errorMessage(err)}`);
109
+ const errorMsg = errorMessage(err);
110
+ failures.push({ prUrl: url, error: errorMsg });
111
+ warn(MODULE, `Skipping ${url}: ${errorMsg}`);
118
112
  }
119
113
  }
120
114
  }
121
115
  const workers = Array.from({ length: Math.min(concurrency, prUrls.length) }, worker);
122
116
  await Promise.all(workers);
123
- debug(MODULE, `Fetched ${results.length}/${prUrls.length} comment bundles`);
124
- return results;
117
+ debug(MODULE, `Fetched ${bundles.length}/${prUrls.length} comment bundles (${failures.length} failed)`);
118
+ return { bundles, failures };
125
119
  }
@@ -18,8 +18,6 @@ export { computeDisplayLabel } from './display-utils.js';
18
18
  export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
19
19
  export { isConditionalChecklistItem } from './checklist-analysis.js';
20
20
  export { determineStatus } from './status-determination.js';
21
- declare function isPlaceholderUsername(username: string): boolean;
22
- export { isPlaceholderUsername };
23
21
  /**
24
22
  * Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
25
23
  * Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
@@ -28,34 +28,12 @@ import { analyzeChecklist } from './checklist-analysis.js';
28
28
  import { extractMaintainerActionHints } from './maintainer-analysis.js';
29
29
  import { computeDisplayLabel } from './display-utils.js';
30
30
  import { fetchUserMergedPRCounts as fetchUserMergedPRCountsImpl, fetchUserClosedPRCounts as fetchUserClosedPRCountsImpl, fetchRecentlyClosedPRs as fetchRecentlyClosedPRsImpl, fetchRecentlyMergedPRs as fetchRecentlyMergedPRsImpl, } from './github-stats.js';
31
+ import { isPlaceholderUsername } from './placeholder-usernames.js';
31
32
  // Re-export so existing consumers can still import from pr-monitor
32
33
  export { computeDisplayLabel } from './display-utils.js';
33
34
  export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
34
35
  export { isConditionalChecklistItem } from './checklist-analysis.js';
35
36
  export { determineStatus } from './status-determination.js';
36
- /**
37
- * Known placeholder values that can end up in `config.githubUsername` from
38
- * doc snippets, example configs, or aborted setup flows. When the configured
39
- * username matches one of these, the PR fetch silently returns zero results
40
- * and the dashboard looks like a fresh install. Detecting these lets us
41
- * auto-repair the config from the authenticated viewer before fetching.
42
- *
43
- * Entries must be lowercase — `Lowercase<string>` on the source tuple makes
44
- * a non-lowercase entry a compile error, keeping the case-insensitive lookup
45
- * contract type-checked instead of comment-documented.
46
- */
47
- const PLACEHOLDER_USERNAMES = [
48
- 'example-user',
49
- 'your-username',
50
- 'your-github-username',
51
- ];
52
- const KNOWN_PLACEHOLDER_USERNAMES = new Set(PLACEHOLDER_USERNAMES);
53
- function isPlaceholderUsername(username) {
54
- return KNOWN_PLACEHOLDER_USERNAMES.has(username.toLowerCase());
55
- }
56
- // Module-private on purpose: callers should only use the predicate so the
57
- // `.toLowerCase()` contract can't be bypassed by reading the set directly.
58
- export { isPlaceholderUsername };
59
37
  /**
60
38
  * Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
61
39
  * Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
@@ -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 schema migration (v1→v2→v3).
3
+ * Handles file I/O, locking, backup/restore, and schema migration (v1→v2→v3→v4).
4
4
  * No module-level mutable state — functions accept/return AgentState objects.
5
5
  */
6
6
  import { AgentState } from './types.js';
@@ -41,7 +41,7 @@ export declare function migrateV2ToV3(rawState: Record<string, unknown>): Record
41
41
  */
42
42
  export declare function migrateV3ToV4(rawState: Record<string, unknown>): Record<string, unknown>;
43
43
  /**
44
- * Create a fresh state (v3).
44
+ * Create a fresh state (v4).
45
45
  * Leverages Zod schema defaults to produce a complete state.
46
46
  */
47
47
  export declare function createFreshState(): AgentState;