@oss-autopilot/core 1.16.1 → 1.17.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 (58) hide show
  1. package/dist/cli-registry.js +53 -11
  2. package/dist/cli.bundle.cjs +82 -69
  3. package/dist/cli.js +22 -10
  4. package/dist/commands/comments.js +38 -20
  5. package/dist/commands/config.d.ts +9 -2
  6. package/dist/commands/config.js +12 -3
  7. package/dist/commands/daily.d.ts +3 -1
  8. package/dist/commands/daily.js +126 -37
  9. package/dist/commands/dashboard-data.d.ts +26 -2
  10. package/dist/commands/dashboard-data.js +45 -19
  11. package/dist/commands/dashboard-server.d.ts +1 -1
  12. package/dist/commands/dashboard-server.js +109 -20
  13. package/dist/commands/dismiss.js +4 -1
  14. package/dist/commands/doctor.d.ts +49 -0
  15. package/dist/commands/doctor.js +358 -0
  16. package/dist/commands/index.d.ts +2 -0
  17. package/dist/commands/index.js +2 -0
  18. package/dist/commands/move.d.ts +1 -2
  19. package/dist/commands/move.js +8 -4
  20. package/dist/commands/read.js +2 -1
  21. package/dist/commands/search.d.ts +0 -18
  22. package/dist/commands/search.js +38 -1
  23. package/dist/commands/setup.js +42 -2
  24. package/dist/commands/shelve.js +4 -1
  25. package/dist/commands/skip-add.js +1 -1
  26. package/dist/commands/startup.js +7 -3
  27. package/dist/commands/track.js +2 -1
  28. package/dist/commands/vet-list.d.ts +23 -2
  29. package/dist/commands/vet-list.js +57 -10
  30. package/dist/core/anti-llm-policy.d.ts +5 -0
  31. package/dist/core/anti-llm-policy.js +5 -0
  32. package/dist/core/ci-analysis.js +6 -1
  33. package/dist/core/config-registry.d.ts +44 -0
  34. package/dist/core/config-registry.js +286 -0
  35. package/dist/core/dashboard-data-schema.d.ts +78 -0
  36. package/dist/core/dashboard-data-schema.js +80 -0
  37. package/dist/core/errors.d.ts +14 -0
  38. package/dist/core/errors.js +22 -0
  39. package/dist/core/http-cache.d.ts +8 -1
  40. package/dist/core/http-cache.js +59 -1
  41. package/dist/core/index.d.ts +3 -1
  42. package/dist/core/index.js +3 -1
  43. package/dist/core/maintainer-analysis.js +9 -3
  44. package/dist/core/pr-monitor.d.ts +7 -0
  45. package/dist/core/pr-monitor.js +16 -3
  46. package/dist/core/repo-score-manager.d.ts +17 -3
  47. package/dist/core/repo-score-manager.js +48 -19
  48. package/dist/core/state-persistence.d.ts +14 -1
  49. package/dist/core/state-persistence.js +24 -2
  50. package/dist/core/state-schema.d.ts +2 -0
  51. package/dist/core/state-schema.js +5 -0
  52. package/dist/core/state.d.ts +26 -2
  53. package/dist/core/state.js +50 -5
  54. package/dist/core/status-determination.d.ts +16 -0
  55. package/dist/core/status-determination.js +44 -11
  56. package/dist/formatters/json.d.ts +40 -2
  57. package/dist/formatters/json.js +1 -0
  58. package/package.json +1 -1
@@ -8,11 +8,31 @@ import { AgentStateSchema } from './state-schema.js';
8
8
  import { loadState, saveState, reloadStateIfChanged, createFreshState, atomicWriteFileSync, } from './state-persistence.js';
9
9
  import * as repoScoring from './repo-score-manager.js';
10
10
  import { debug, warn } from './logger.js';
11
- import { errorMessage, ConfigurationError } from './errors.js';
11
+ import { errorMessage, ConfigurationError, ConcurrencyError } from './errors.js';
12
12
  import { GistStateStore } from './gist-state-store.js';
13
13
  import { getStatePath, getStateCachePath } from './utils.js';
14
14
  export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
15
15
  const MODULE = 'state';
16
+ /**
17
+ * Push state to the backing Gist when Gist mode is active. Best-effort:
18
+ * network/auth failures are logged via `warn()` but never propagated —
19
+ * the caller's primary mutation has already succeeded locally and the
20
+ * next successful push (or a daily run) will re-sync.
21
+ *
22
+ * Intended for state-mutating PR-flow commands (shelve / unshelve / move /
23
+ * dismiss / undismiss / claim) so Gist-sync users don't see day-long drift
24
+ * between machines waiting for the next `daily` checkpoint. See issue #1036.
25
+ */
26
+ export async function maybeCheckpoint(stateManager, callerModule) {
27
+ if (!stateManager.isGistMode())
28
+ return;
29
+ try {
30
+ await stateManager.checkpoint();
31
+ }
32
+ catch (err) {
33
+ warn(callerModule, `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`);
34
+ }
35
+ }
16
36
  /**
17
37
  * Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
18
38
  *
@@ -173,8 +193,19 @@ export class StateManager {
173
193
  *
174
194
  * In Gist mode, writes to a local cache file (not the main state file) so the Gist
175
195
  * remains the source of truth. Use `checkpoint()` to push state to the Gist.
176
- */
177
- save() {
196
+ *
197
+ * Local-file mode uses optimistic compare-and-swap: if another process has
198
+ * written `state.json` since we last loaded/saved, `saveState` throws
199
+ * `ConcurrencyError`. See issue #1030.
200
+ *
201
+ * @throws ConcurrencyError (local file mode only) when the on-disk state
202
+ * was modified externally between this StateManager's last load/save
203
+ * and now. Callers that tolerate silent merge can set
204
+ * `{ allowReloadAndLoseMutation: true }` to downgrade the error into a
205
+ * warning — but note that doing so abandons any in-memory mutation
206
+ * made since the last load. Fail-loud (the default) is preferred.
207
+ */
208
+ save(options = {}) {
178
209
  this.state.lastRunAt = new Date().toISOString();
179
210
  if (this.inMemoryOnly) {
180
211
  return;
@@ -182,6 +213,10 @@ export class StateManager {
182
213
  if (this.gistStore) {
183
214
  // In Gist mode, write to local cache (not main state file).
184
215
  // The Gist is the source of truth; local cache is for fallback.
216
+ // Intentionally NO compare-and-swap on this cache path — two processes
217
+ // racing on state-cache.json can clobber each other, but the next
218
+ // `refreshFromGist()` or `checkpoint()` re-syncs from the authoritative
219
+ // Gist. The trade-off is a transiently-stale offline bootstrap (#1030).
185
220
  try {
186
221
  atomicWriteFileSync(getStateCachePath(), JSON.stringify(this.state, null, 2), 0o600);
187
222
  }
@@ -192,8 +227,18 @@ export class StateManager {
192
227
  }
193
228
  return;
194
229
  }
195
- // Local file mode (existing behavior)
196
- this.lastLoadedMtimeMs = saveState(this.state);
230
+ // Local file mode with optimistic compare-and-swap.
231
+ try {
232
+ this.lastLoadedMtimeMs = saveState(this.state, this.lastLoadedMtimeMs);
233
+ }
234
+ catch (err) {
235
+ if (err instanceof ConcurrencyError && options.allowReloadAndLoseMutation) {
236
+ warn(MODULE, `Concurrent external write detected; reloading and discarding in-memory mutation. ${err.message}`);
237
+ this.reloadIfChanged();
238
+ return;
239
+ }
240
+ throw err;
241
+ }
197
242
  }
198
243
  /** Push current state to Gist (async). Call at well-defined moments (end of daily, after claim). */
199
244
  async checkpoint() {
@@ -26,9 +26,25 @@ export declare const MIN_RESPONSE_GAP_MS: number;
26
26
  * bot (#568), or when author info is unavailable (graceful degradation).
27
27
  */
28
28
  export declare function isContributorCommit(commitAuthor?: string, contributorUsername?: string): boolean;
29
+ /**
30
+ * Parse an ISO-8601-ish date string into epoch milliseconds.
31
+ *
32
+ * Central helper so the whole module compares dates numerically via `getTime()`
33
+ * rather than relying on the lexicographic property of UTC ISO strings — the
34
+ * latter happens to sort correctly today but silently breaks on any other date
35
+ * format (local-time ISO, `Date.toString()`, non-standard). See #1044.
36
+ *
37
+ * Returns `NaN` for `undefined`, empty, or unparseable inputs so callers can
38
+ * make an explicit fail-closed decision; never throws.
39
+ */
40
+ export declare function normalizeToEpochMs(date: string | undefined | null): number;
29
41
  /**
30
42
  * Check whether the contributor's commit is meaningfully after the maintainer's
31
43
  * comment — i.e. the commit timestamp is at least MIN_RESPONSE_GAP_MS later (#547).
44
+ *
45
+ * Fails closed (returns `false`) if either date is unparseable, avoiding the
46
+ * silent lexicographic fallback that would produce wrong results for non-UTC
47
+ * ISO inputs. See #1044.
32
48
  */
33
49
  export declare function isCommitAfterComment(commitDate: string, commentDate: string): boolean;
34
50
  /**
@@ -32,17 +32,35 @@ export function isContributorCommit(commitAuthor, contributorUsername) {
32
32
  return true; // CI-fix bots act on behalf of the contributor (#568)
33
33
  return author === contributorUsername.toLowerCase();
34
34
  }
35
+ /**
36
+ * Parse an ISO-8601-ish date string into epoch milliseconds.
37
+ *
38
+ * Central helper so the whole module compares dates numerically via `getTime()`
39
+ * rather than relying on the lexicographic property of UTC ISO strings — the
40
+ * latter happens to sort correctly today but silently breaks on any other date
41
+ * format (local-time ISO, `Date.toString()`, non-standard). See #1044.
42
+ *
43
+ * Returns `NaN` for `undefined`, empty, or unparseable inputs so callers can
44
+ * make an explicit fail-closed decision; never throws.
45
+ */
46
+ export function normalizeToEpochMs(date) {
47
+ if (!date)
48
+ return Number.NaN;
49
+ return new Date(date).getTime();
50
+ }
35
51
  /**
36
52
  * Check whether the contributor's commit is meaningfully after the maintainer's
37
53
  * comment — i.e. the commit timestamp is at least MIN_RESPONSE_GAP_MS later (#547).
54
+ *
55
+ * Fails closed (returns `false`) if either date is unparseable, avoiding the
56
+ * silent lexicographic fallback that would produce wrong results for non-UTC
57
+ * ISO inputs. See #1044.
38
58
  */
39
59
  export function isCommitAfterComment(commitDate, commentDate) {
40
- const commitMs = new Date(commitDate).getTime();
41
- const commentMs = new Date(commentDate).getTime();
42
- if (Number.isNaN(commitMs) || Number.isNaN(commentMs)) {
43
- // Fall back to simple string comparison (pre-#547 behavior)
44
- return commitDate > commentDate;
45
- }
60
+ const commitMs = normalizeToEpochMs(commitDate);
61
+ const commentMs = normalizeToEpochMs(commentDate);
62
+ if (!Number.isFinite(commitMs) || !Number.isFinite(commentMs))
63
+ return false;
46
64
  return commitMs - commentMs >= MIN_RESPONSE_GAP_MS;
47
65
  }
48
66
  /**
@@ -61,16 +79,31 @@ function isCommentAddressedByCommit(commitDate, commentDate, changesRequestedDat
61
79
  return false;
62
80
  if (!isCommitAfterComment(commitDate, commentDate))
63
81
  return false;
64
- // Safety net (#431): if a CHANGES_REQUESTED review came after the commit, it's not addressed
65
- if (changesRequestedDate && commitDate < changesRequestedDate)
66
- return false;
82
+ // Safety net (#431): if a CHANGES_REQUESTED review came after the commit, it's not addressed.
83
+ // Fail-closed on unparseable `changesRequestedDate` (#1044): treat unknown ordering as "not addressed".
84
+ if (changesRequestedDate) {
85
+ const commitMs = normalizeToEpochMs(commitDate);
86
+ const changesRequestedMs = normalizeToEpochMs(changesRequestedDate);
87
+ if (!Number.isFinite(commitMs) || !Number.isFinite(changesRequestedMs))
88
+ return false;
89
+ if (commitMs < changesRequestedMs)
90
+ return false;
91
+ }
67
92
  return true;
68
93
  }
69
- /** Check whether a changes_requested review has been addressed by a subsequent contributor commit. */
94
+ /**
95
+ * Check whether a changes_requested review has been addressed by a subsequent contributor commit.
96
+ *
97
+ * Fails closed (returns `false`) on unparseable inputs (#1044).
98
+ */
70
99
  function isChangesAddressedByCommit(commitDate, changesRequestedDate) {
71
100
  if (!commitDate || !changesRequestedDate)
72
101
  return false;
73
- return commitDate >= changesRequestedDate;
102
+ const commitMs = normalizeToEpochMs(commitDate);
103
+ const changesRequestedMs = normalizeToEpochMs(changesRequestedDate);
104
+ if (!Number.isFinite(commitMs) || !Number.isFinite(changesRequestedMs))
105
+ return false;
106
+ return commitMs >= changesRequestedMs;
74
107
  }
75
108
  /**
76
109
  * Collect all applicable action reasons independently, without short-circuiting (#675).
@@ -7,7 +7,7 @@ import type { ContributionStats } from '../core/stats.js';
7
7
  import type { PRCheckFailure } from '../core/pr-monitor.js';
8
8
  import type { SearchPriority } from '../core/types.js';
9
9
  import type { CIFormatterDiagnosis, FormatterDetectionResult } from '../core/formatter-detection.js';
10
- export type ErrorCode = 'AUTH_REQUIRED' | 'RATE_LIMITED' | 'VALIDATION' | 'CONFIGURATION' | 'NETWORK' | 'NOT_FOUND' | 'STATE_CORRUPTED' | 'UNKNOWN';
10
+ export type ErrorCode = 'AUTH_REQUIRED' | 'RATE_LIMITED' | 'VALIDATION' | 'CONFIGURATION' | 'NETWORK' | 'NOT_FOUND' | 'STATE_CORRUPTED' | 'CONCURRENCY' | 'UNKNOWN';
11
11
  export interface JsonOutput<T = unknown> {
12
12
  success: boolean;
13
13
  data?: T;
@@ -104,6 +104,23 @@ export interface CompactRepoGroup {
104
104
  repo: string;
105
105
  prUrls: string[];
106
106
  }
107
+ /**
108
+ * Phase tags for non-fatal warnings emitted during a `daily` run.
109
+ * See `DailyWarning` and issue #1042 for the rationale — keeping this a
110
+ * fixed union so downstream consumers can switch on it without drift.
111
+ */
112
+ export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint';
113
+ /**
114
+ * A single non-fatal failure surfaced from the `daily` pipeline. Unlike
115
+ * `PRCheckFailure` (which is scoped to per-PR fetch errors), this covers
116
+ * ancillary fetches that previously demoted to a log-only `warn()` — repo
117
+ * metadata, monthly analytics, scout sync, Gist checkpoint, etc.
118
+ */
119
+ export interface DailyWarning {
120
+ phase: DailyWarningPhase;
121
+ operation: string;
122
+ message: string;
123
+ }
107
124
  export interface DailyOutput {
108
125
  digest: DailyDigestCompact;
109
126
  capacity: CapacityAssessment;
@@ -114,6 +131,12 @@ export interface DailyOutput {
114
131
  commentedIssues: CommentedIssue[];
115
132
  repoGroups: CompactRepoGroup[];
116
133
  failures: PRCheckFailure[];
134
+ /**
135
+ * Non-fatal warnings from ancillary pipeline phases (repo metadata,
136
+ * analytics, scout sync, Gist checkpoint, etc.). Always an array — empty
137
+ * on clean runs. See #1042.
138
+ */
139
+ warnings: DailyWarning[];
117
140
  }
118
141
  /**
119
142
  * Compact version of DailyOutput for reduced JSON payload size (#763).
@@ -133,6 +156,12 @@ export interface CompactDailyOutput {
133
156
  commentedIssues: CommentedIssue[];
134
157
  /** Number of PRs that failed to fetch. Non-zero indicates partial results. */
135
158
  failureCount: number;
159
+ /**
160
+ * Non-fatal warnings from ancillary pipeline phases — full list retained so
161
+ * downstream consumers (dashboard, MCP) can surface degradation even under
162
+ * the `--compact` payload. See #1042.
163
+ */
164
+ warnings: DailyWarning[];
136
165
  }
137
166
  /**
138
167
  * Strip a full DailyOutput down to the compact subset (#763).
@@ -183,8 +212,17 @@ export interface SearchOutput {
183
212
  reasonsToApprove: string[];
184
213
  reasonsToSkip: string[];
185
214
  searchPriority: SearchPriority;
186
- /** 0-100 scale composite viability score */
215
+ /** 0-100 scale composite viability score. Sanitized on the boundary (#1043): out-of-contract values are coerced to 0 and logged. */
187
216
  viabilityScore: number;
217
+ /**
218
+ * Letter grade (A/B/C/F) computed from the autopilot-tracked repoScore.
219
+ * Scout's `search` does not emit per-candidate projectHealth, so scout-side
220
+ * signals are treated as unknown; unscored repos grade 'F'. See #1043.
221
+ */
222
+ grade: {
223
+ letter: 'A' | 'B' | 'C' | 'F';
224
+ reason: string;
225
+ };
188
226
  repoScore?: {
189
227
  /** 1-10 scale repository quality score */
190
228
  score: number;
@@ -16,6 +16,7 @@ export function toCompactDailyOutput(output) {
16
16
  actionMenu: output.actionMenu,
17
17
  commentedIssues: output.commentedIssues,
18
18
  failureCount: output.failures.length,
19
+ warnings: output.warnings,
19
20
  };
20
21
  }
21
22
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {