@oss-autopilot/core 3.5.0 → 3.6.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.
@@ -18,11 +18,11 @@ import { daysBetween } from './dates.js';
18
18
  import { parseGitHubUrl, extractOwnerRepo, isOwnRepo } from './urls.js';
19
19
  import { DEFAULT_CONCURRENCY, runWorkerPool } from './concurrency.js';
20
20
  import { determineStatus } from './status-determination.js';
21
- import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitOrAuthError, } from './errors.js';
21
+ import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isInvalidUserSearchError, isRateLimitOrAuthError, } from './errors.js';
22
22
  import { paginateAll } from './pagination.js';
23
23
  import { debug, warn, timed } from './logger.js';
24
24
  import { getHttpCache, cachedRequest } from './http-cache.js';
25
- import { classifyFailingChecks, getCIStatus } from './ci-analysis.js';
25
+ import { categorizeCIStatus, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
26
26
  import { determineReviewDecision, getLatestChangesRequestedDate, checkUnrespondedComments, } from './review-analysis.js';
27
27
  import { analyzeChecklist } from './checklist-analysis.js';
28
28
  import { extractMaintainerActionHints } from './maintainer-analysis.js';
@@ -31,7 +31,7 @@ import { fetchUserMergedPRCounts as fetchUserMergedPRCountsImpl, fetchUserClosed
31
31
  import { isPlaceholderUsername } from './placeholder-usernames.js';
32
32
  // Re-export so existing consumers can still import from pr-monitor
33
33
  export { computeDisplayLabel } from './display-utils.js';
34
- export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
34
+ export { categorizeCIStatus, classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
35
35
  export { isConditionalChecklistItem } from './checklist-analysis.js';
36
36
  export { determineStatus } from './status-determination.js';
37
37
  /**
@@ -140,16 +140,31 @@ export class PRMonitor {
140
140
  }
141
141
  debug('pr-monitor', `Fetching open PRs for @${searchUsername}...`);
142
142
  // Search for all open PRs authored by the user with pagination
143
- const allItems = [];
144
143
  let page = 1;
145
144
  const perPage = 100;
146
- const firstPage = await this.octokit.search.issuesAndPullRequests({
147
- q: `is:pr is:open is:public author:${searchUsername}`,
148
- sort: 'updated',
149
- order: 'desc',
150
- per_page: perPage,
151
- page: 1,
152
- });
145
+ let firstPage;
146
+ try {
147
+ firstPage = await this.octokit.search.issuesAndPullRequests({
148
+ q: `is:pr is:open is:public author:${searchUsername}`,
149
+ sort: 'updated',
150
+ order: 'desc',
151
+ per_page: perPage,
152
+ page: 1,
153
+ });
154
+ }
155
+ catch (err) {
156
+ // Rewrite the Search API's "users do not exist" 422 into an actionable
157
+ // ConfigurationError naming the configured username (#1323). The raw
158
+ // Octokit message ("Validation Failed: ...") gives the user no signal
159
+ // that the cause is local config rather than a transient GitHub issue.
160
+ if (isInvalidUserSearchError(err)) {
161
+ throw new ConfigurationError(`Configured GitHub username "${searchUsername}" was not found on GitHub. ` +
162
+ `Run \`/setup-oss\` to reconfigure, or edit \`config.githubUsername\` in ` +
163
+ `\`~/.oss-autopilot/state.json\` directly.`);
164
+ }
165
+ throw err;
166
+ }
167
+ const allItems = [];
153
168
  allItems.push(...firstPage.data.items);
154
169
  const totalCount = firstPage.data.total_count;
155
170
  debug(MODULE, `Found ${totalCount} open PRs`);
@@ -341,6 +356,10 @@ export class PRMonitor {
341
356
  const latestChangesRequestedDate = getLatestChangesRequestedDate(reviews);
342
357
  // Classify failing checks (delegated to ci-analysis module)
343
358
  const classifiedChecks = classifyFailingChecks(failingCheckNames, failingCheckConclusions);
359
+ // Aggregate 5-state CI categorization (#1272). Computed once here so
360
+ // agents read pr.ciCategorization rather than re-deriving the truth
361
+ // table in three separate prose forms.
362
+ const ciCategorization = categorizeCIStatus({ ciStatus, failingCheckNames, classifiedChecks });
344
363
  // Determine status
345
364
  const hasActionableCIFailure = ciStatus === 'failing' && classifiedChecks.some((c) => c.category === 'actionable');
346
365
  const { status, actionReason, waitReason, stalenessTier, actionReasons } = determineStatus({
@@ -376,6 +395,7 @@ export class PRMonitor {
376
395
  ciStatus,
377
396
  failingCheckNames,
378
397
  classifiedChecks,
398
+ ciCategorization,
379
399
  hasMergeConflict: mergeConflict,
380
400
  reviewDecision,
381
401
  hasUnrespondedComment,
@@ -459,6 +459,7 @@ export declare const AgentStateSchema: z.ZodObject<{
459
459
  }, z.core.$strip>>;
460
460
  lastRunAt: z.ZodDefault<z.ZodString>;
461
461
  lastDigestAt: z.ZodOptional<z.ZodString>;
462
+ lastStrategyAt: z.ZodOptional<z.ZodString>;
462
463
  lastDigest: z.ZodOptional<z.ZodObject<{
463
464
  generatedAt: z.ZodString;
464
465
  openPRs: z.ZodArray<z.ZodAny>;
@@ -285,6 +285,15 @@ export const AgentStateSchema = z.object({
285
285
  config: AgentConfigSchema.default(() => AgentConfigSchema.parse({})),
286
286
  lastRunAt: z.string().default(() => new Date().toISOString()),
287
287
  lastDigestAt: z.string().optional(),
288
+ /**
289
+ * ISO timestamp of the most recent {@link computeStrategy} invocation
290
+ * embedded in a daily run output (#1270). The cadence gate in
291
+ * `daily.ts` consults this — strategy snapshots fire every 30 days OR
292
+ * when 5+ PRs have merged since the last snapshot, whichever comes
293
+ * first. Below {@link STRATEGY_MIN_PRS} merged PRs the gate stays
294
+ * silent regardless of cadence.
295
+ */
296
+ lastStrategyAt: z.string().optional(),
288
297
  lastDigest: DailyDigestSchema.optional(),
289
298
  monthlyMergedCounts: z.record(z.string(), z.number()).optional(),
290
299
  monthlyClosedCounts: z.record(z.string(), z.number()).optional(),
@@ -174,6 +174,13 @@ export declare class StateManager {
174
174
  * @param digest - The daily digest to store
175
175
  */
176
176
  setLastDigest(digest: DailyDigest): void;
177
+ /**
178
+ * Persist the timestamp of the most recent strategy snapshot embedded
179
+ * in a daily run output (#1270). Called from the daily pipeline after
180
+ * `computeStrategy()` succeeds; the cadence gate in
181
+ * {@link shouldComputeStrategy} reads this on the next run.
182
+ */
183
+ setLastStrategyAt(iso: string): void;
177
184
  /**
178
185
  * Update monthly merged PR counts for dashboard display.
179
186
  * @param counts - Monthly merged PR counts keyed by YYYY-MM
@@ -416,6 +416,16 @@ export class StateManager {
416
416
  this.state.lastDigestAt = digest.generatedAt;
417
417
  this.autoSave();
418
418
  }
419
+ /**
420
+ * Persist the timestamp of the most recent strategy snapshot embedded
421
+ * in a daily run output (#1270). Called from the daily pipeline after
422
+ * `computeStrategy()` succeeds; the cadence gate in
423
+ * {@link shouldComputeStrategy} reads this on the next run.
424
+ */
425
+ setLastStrategyAt(iso) {
426
+ this.state.lastStrategyAt = iso;
427
+ this.autoSave();
428
+ }
419
429
  /**
420
430
  * Update monthly merged PR counts for dashboard display.
421
431
  * @param counts - Monthly merged PR counts keyed by YYYY-MM
@@ -32,9 +32,15 @@ export interface StrategyProfile {
32
32
  export interface StrategyCapacity {
33
33
  openPRCount: number;
34
34
  dormantPRCount: number;
35
+ /** Distinct repos hosting at least one dormant PR. Surfaces alongside
36
+ * `dormantPRCount` so consumers can render "N PRs across M repos"
37
+ * without re-deriving from `openPRs` themselves. */
38
+ dormantRepoCount: number;
35
39
  /** True when dormant PRs span >=2 distinct repos (a signal that the
36
40
  * contributor is awaiting reviews from multiple maintainers, not just
37
- * one slow project). */
41
+ * one slow project). Equivalent to `dormantPRCount >= 2 &&
42
+ * dormantRepoCount >= 2` — exposed as a separate boolean so callers
43
+ * with a strict yes/no presentation don't have to recompute. */
38
44
  overExtended: boolean;
39
45
  /** The single highest-priority next action — null when state is too
40
46
  * thin to recommend anything. */
@@ -73,3 +79,17 @@ export declare const STRATEGY_MIN_PRS = 10;
73
79
  * minimum-data gate (#1243).
74
80
  */
75
81
  export declare function computeStrategy(state: AgentState): StrategyResult | null;
82
+ /** Cadence trigger thresholds for the auto-display in `/oss` (#1270). */
83
+ export declare const STRATEGY_CADENCE_DAYS = 30;
84
+ export declare const STRATEGY_CADENCE_MERGED_DELTA = 5;
85
+ /**
86
+ * Decide whether a strategy snapshot should be embedded in this daily run.
87
+ * Returns true when EITHER 30 days have elapsed since the last snapshot OR
88
+ * 5+ PRs have merged since then, AND the merge floor in
89
+ * {@link STRATEGY_MIN_PRS} is met. The caller is responsible for calling
90
+ * {@link computeStrategy} when this returns true and for persisting
91
+ * `state.lastStrategyAt` after a successful compute.
92
+ *
93
+ * `nowIso` is injected so tests can pin time without mocking `Date`.
94
+ */
95
+ export declare function shouldComputeStrategy(state: AgentState, nowIso: string): boolean;
@@ -182,6 +182,7 @@ export function computeStrategy(state) {
182
182
  const capacity = {
183
183
  openPRCount,
184
184
  dormantPRCount,
185
+ dormantRepoCount: dormantRepos.size,
185
186
  overExtended,
186
187
  suggestedAction: recommendForOverExtension(openPRCount, dormantPRCount, overExtended),
187
188
  };
@@ -204,6 +205,49 @@ export function computeStrategy(state) {
204
205
  };
205
206
  return { profile, capacity, patterns, recommendations };
206
207
  }
208
+ /** Cadence trigger thresholds for the auto-display in `/oss` (#1270). */
209
+ export const STRATEGY_CADENCE_DAYS = 30;
210
+ export const STRATEGY_CADENCE_MERGED_DELTA = 5;
211
+ /**
212
+ * Decide whether a strategy snapshot should be embedded in this daily run.
213
+ * Returns true when EITHER 30 days have elapsed since the last snapshot OR
214
+ * 5+ PRs have merged since then, AND the merge floor in
215
+ * {@link STRATEGY_MIN_PRS} is met. The caller is responsible for calling
216
+ * {@link computeStrategy} when this returns true and for persisting
217
+ * `state.lastStrategyAt` after a successful compute.
218
+ *
219
+ * `nowIso` is injected so tests can pin time without mocking `Date`.
220
+ */
221
+ export function shouldComputeStrategy(state, nowIso) {
222
+ const merged = state.mergedPRs ?? [];
223
+ if (merged.length < STRATEGY_MIN_PRS)
224
+ return false;
225
+ const lastIso = state.lastStrategyAt;
226
+ if (!lastIso)
227
+ return true;
228
+ // Time-based trigger: 30+ days since last snapshot.
229
+ const lastMs = Date.parse(lastIso);
230
+ const nowMs = Date.parse(nowIso);
231
+ if (Number.isFinite(lastMs) && Number.isFinite(nowMs)) {
232
+ const daysSince = (nowMs - lastMs) / (1000 * 60 * 60 * 24);
233
+ if (daysSince >= STRATEGY_CADENCE_DAYS)
234
+ return true;
235
+ }
236
+ else {
237
+ // Unparseable timestamps — fail open and recompute rather than
238
+ // silently never re-firing. The lastStrategyAt write below will
239
+ // refresh to a valid ISO string.
240
+ return true;
241
+ }
242
+ // Merge-count trigger: count PRs merged after `lastStrategyAt`.
243
+ // `mergedAt` is required on StoredMergedPRSchema; `Date.parse` returns
244
+ // NaN for malformed-but-stored data and Number.isFinite excludes those.
245
+ const mergedSince = merged.filter((pr) => {
246
+ const mergedMs = Date.parse(pr.mergedAt);
247
+ return Number.isFinite(mergedMs) && mergedMs > lastMs;
248
+ }).length;
249
+ return mergedSince >= STRATEGY_CADENCE_MERGED_DELTA;
250
+ }
207
251
  function deriveIssueTypePreferences(distribution) {
208
252
  // Recommend the user's two strongest PR types — they have a track
209
253
  // record there, so issues in those buckets are higher-yield.
@@ -19,6 +19,46 @@ export interface ClassifiedCheck {
19
19
  category: CIFailureCategory;
20
20
  conclusion?: string;
21
21
  }
22
+ /**
23
+ * Mutually exclusive overall-CI categories produced by
24
+ * {@link categorizeCIStatus} (#1272). The 5-row truth table that lived
25
+ * as prose in `agents/pr-health-checker.md` — extracted so any consumer
26
+ * (the agent, the dashboard, future MCP surfaces) reads one typed field
27
+ * instead of re-deriving the table.
28
+ *
29
+ * - `all_passing` — every reported check is green
30
+ * - `failing` — at least one actionable failure (real test/lint/build
31
+ * issue), OR ciStatus reported failing without per-check detail (the
32
+ * honest answer when the legacy combined-status endpoint can't tell
33
+ * us what failed)
34
+ * - `fork_limitation` — failures exist but ALL of them are
35
+ * `fork_limitation` / `auth_gate` (Vercel preview, internal CI) — purely
36
+ * informational
37
+ * - `blocked` — checks are pending (awaiting trigger / completion), OR
38
+ * non-actionable failures include `infrastructure` (cancelled /
39
+ * timed-out runner — re-running often resolves)
40
+ * - `not_running` — no checks reported
41
+ */
42
+ export type CIStatusCategory = 'all_passing' | 'failing' | 'fork_limitation' | 'blocked' | 'not_running';
43
+ /**
44
+ * Suggested action for the {@link CIStatusCategorization}. Hint, not
45
+ * enforcement — the consuming agent may still escalate or skip based on
46
+ * other PR context.
47
+ */
48
+ export type CIStatusAction = 'none' | 'investigate' | 'request_rerun' | 'check_workflows' | 'informational';
49
+ /**
50
+ * Aggregate CI status produced by {@link categorizeCIStatus} (#1272).
51
+ * Derived from `ciStatus + failingCheckNames + classifiedChecks` —
52
+ * exposed on {@link FetchedPR} so agents read a single field instead
53
+ * of re-implementing the truth table.
54
+ */
55
+ export interface CIStatusCategorization {
56
+ category: CIStatusCategory;
57
+ /** Short human-readable summary suitable for inline display. */
58
+ summary: string;
59
+ /** Suggested next action (hint, not enforcement). */
60
+ action: CIStatusAction;
61
+ }
22
62
  /** CI status result returned by getCIStatus(). */
23
63
  export interface CIStatusResult {
24
64
  status: CIStatus;
@@ -115,6 +155,15 @@ export interface FetchedPR {
115
155
  failingCheckNames: string[];
116
156
  /** Failing checks with category classification (#81). Separates actionable failures from fork limitations and auth gates. */
117
157
  classifiedChecks: ClassifiedCheck[];
158
+ /**
159
+ * Aggregate 5-state CI categorization (#1272). Derived from `ciStatus`,
160
+ * `failingCheckNames`, and `classifiedChecks` via `categorizeCIStatus()`
161
+ * — agents read this directly instead of re-deriving the truth table.
162
+ * Always populated on a fresh fetch (v2 architecture has no cached
163
+ * `FetchedPR` to migrate); pr-monitor's `fetchPRDetails` sets it on
164
+ * every PR before construction.
165
+ */
166
+ ciCategorization: CIStatusCategorization;
118
167
  hasMergeConflict: boolean;
119
168
  reviewDecision: ReviewDecision;
120
169
  /** How many commits the PR branch is behind the base branch. */
@@ -97,6 +97,13 @@ export interface DailyOutput {
97
97
  * on clean runs. See #1042.
98
98
  */
99
99
  warnings: DailyWarning[];
100
+ /**
101
+ * Periodic contribution-strategy snapshot (#1270). Populated when the
102
+ * cadence trigger fires AND the user has crossed the merge floor. The
103
+ * `/oss` action menu renders this inline ahead of the action options;
104
+ * absent or null on runs where the gate stays silent.
105
+ */
106
+ strategySummary?: import('../core/strategy.js').StrategyResult | null;
100
107
  }
101
108
  /**
102
109
  * Compact version of DailyOutput for reduced JSON payload size (#763).
@@ -122,6 +129,8 @@ export interface CompactDailyOutput {
122
129
  * the `--compact` payload. See #1042.
123
130
  */
124
131
  warnings: DailyWarning[];
132
+ /** Periodic strategy snapshot, threaded through compact mode for parity. See {@link DailyOutput.strategySummary}. */
133
+ strategySummary?: import('../core/strategy.js').StrategyResult | null;
125
134
  }
126
135
  /**
127
136
  * Strip a full DailyOutput down to the compact subset (#763).
@@ -275,6 +284,50 @@ export declare const DailyOutputSchema: z.ZodObject<{
275
284
  timestamp: z.ZodOptional<z.ZodString>;
276
285
  details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
277
286
  }, z.core.$strip>>;
287
+ strategySummary: z.ZodOptional<z.ZodNullable<z.ZodObject<{
288
+ profile: z.ZodObject<{
289
+ style: z.ZodEnum<{
290
+ maintainer: "maintainer";
291
+ explorer: "explorer";
292
+ specialist: "specialist";
293
+ generalist: "generalist";
294
+ }>;
295
+ totalPRs: z.ZodNumber;
296
+ mergedCount: z.ZodNumber;
297
+ mergeRate: z.ZodNumber;
298
+ primaryLanguages: z.ZodArray<z.ZodString>;
299
+ favoriteRepos: z.ZodArray<z.ZodString>;
300
+ }, z.core.$loose>;
301
+ capacity: z.ZodObject<{
302
+ openPRCount: z.ZodNumber;
303
+ dormantPRCount: z.ZodNumber;
304
+ dormantRepoCount: z.ZodNumber;
305
+ overExtended: z.ZodBoolean;
306
+ suggestedAction: z.ZodUnion<readonly [z.ZodLiteral<"open_more">, z.ZodLiteral<"follow_up_dormant">, z.ZodLiteral<"wait_on_maintainers">, z.ZodNull]>;
307
+ }, z.core.$loose>;
308
+ patterns: z.ZodObject<{
309
+ prTypeDistribution: z.ZodObject<{
310
+ docs: z.ZodNumber;
311
+ fixes: z.ZodNumber;
312
+ features: z.ZodNumber;
313
+ refactors: z.ZodNumber;
314
+ tests: z.ZodNumber;
315
+ other: z.ZodNumber;
316
+ }, z.core.$loose>;
317
+ trajectoryDirection: z.ZodEnum<{
318
+ growing: "growing";
319
+ steady: "steady";
320
+ declining: "declining";
321
+ }>;
322
+ averagePRSize: z.ZodNumber;
323
+ }, z.core.$loose>;
324
+ recommendations: z.ZodObject<{
325
+ languages: z.ZodArray<z.ZodString>;
326
+ repos: z.ZodArray<z.ZodString>;
327
+ issueTypes: z.ZodArray<z.ZodString>;
328
+ avoidPatterns: z.ZodArray<z.ZodString>;
329
+ }, z.core.$loose>;
330
+ }, z.core.$loose>>>;
278
331
  }, z.core.$strip>;
279
332
  export declare const CompactDailyOutputSchema: z.ZodObject<{
280
333
  digest: z.ZodObject<{
@@ -371,6 +424,50 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
371
424
  timestamp: z.ZodOptional<z.ZodString>;
372
425
  details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
373
426
  }, z.core.$strip>>;
427
+ strategySummary: z.ZodOptional<z.ZodNullable<z.ZodObject<{
428
+ profile: z.ZodObject<{
429
+ style: z.ZodEnum<{
430
+ maintainer: "maintainer";
431
+ explorer: "explorer";
432
+ specialist: "specialist";
433
+ generalist: "generalist";
434
+ }>;
435
+ totalPRs: z.ZodNumber;
436
+ mergedCount: z.ZodNumber;
437
+ mergeRate: z.ZodNumber;
438
+ primaryLanguages: z.ZodArray<z.ZodString>;
439
+ favoriteRepos: z.ZodArray<z.ZodString>;
440
+ }, z.core.$loose>;
441
+ capacity: z.ZodObject<{
442
+ openPRCount: z.ZodNumber;
443
+ dormantPRCount: z.ZodNumber;
444
+ dormantRepoCount: z.ZodNumber;
445
+ overExtended: z.ZodBoolean;
446
+ suggestedAction: z.ZodUnion<readonly [z.ZodLiteral<"open_more">, z.ZodLiteral<"follow_up_dormant">, z.ZodLiteral<"wait_on_maintainers">, z.ZodNull]>;
447
+ }, z.core.$loose>;
448
+ patterns: z.ZodObject<{
449
+ prTypeDistribution: z.ZodObject<{
450
+ docs: z.ZodNumber;
451
+ fixes: z.ZodNumber;
452
+ features: z.ZodNumber;
453
+ refactors: z.ZodNumber;
454
+ tests: z.ZodNumber;
455
+ other: z.ZodNumber;
456
+ }, z.core.$loose>;
457
+ trajectoryDirection: z.ZodEnum<{
458
+ growing: "growing";
459
+ steady: "steady";
460
+ declining: "declining";
461
+ }>;
462
+ averagePRSize: z.ZodNumber;
463
+ }, z.core.$loose>;
464
+ recommendations: z.ZodObject<{
465
+ languages: z.ZodArray<z.ZodString>;
466
+ repos: z.ZodArray<z.ZodString>;
467
+ issueTypes: z.ZodArray<z.ZodString>;
468
+ avoidPatterns: z.ZodArray<z.ZodString>;
469
+ }, z.core.$loose>;
470
+ }, z.core.$loose>>>;
374
471
  }, z.core.$strip>;
375
472
  export declare const SearchOutputSchema: z.ZodObject<{
376
473
  candidates: z.ZodArray<z.ZodObject<{
@@ -454,6 +551,14 @@ export declare const ListMoveTierOutputSchema: z.ZodObject<{
454
551
  count: z.ZodNumber;
455
552
  reason: z.ZodOptional<z.ZodString>;
456
553
  }, z.core.$strip>;
554
+ export declare const ListMarkDoneOutputSchema: z.ZodObject<{
555
+ marked: z.ZodBoolean;
556
+ filePath: z.ZodString;
557
+ url: z.ZodString;
558
+ repoHeadingStruck: z.ZodBoolean;
559
+ remainingUnderRepo: z.ZodNumber;
560
+ reason: z.ZodOptional<z.ZodString>;
561
+ }, z.core.$strip>;
457
562
  export declare const PostOutputSchema: z.ZodObject<{
458
563
  commentUrl: z.ZodString;
459
564
  url: z.ZodString;
@@ -30,6 +30,7 @@ export function toCompactDailyOutput(output) {
30
30
  commentedIssues: output.commentedIssues,
31
31
  failureCount: output.failures.length,
32
32
  warnings: output.warnings,
33
+ strategySummary: output.strategySummary,
33
34
  };
34
35
  }
35
36
  /**
@@ -198,6 +199,65 @@ const CompactRepoGroupSchema = z.object({
198
199
  });
199
200
  // DailyWarning schemas were hoisted above StatusOutputSchema (#1193) so the
200
201
  // status output can reference them without `z.lazy()`.
202
+ // Mirrors {@link StrategyResult} in core/strategy.ts. Kept passthrough on the
203
+ // inner objects so additive shape changes there don't break Zod validation
204
+ // before the schema catches up — drift on required keys still fails.
205
+ const StrategyResultSchema = z
206
+ .object({
207
+ profile: z
208
+ .object({
209
+ style: z.enum(['maintainer', 'explorer', 'specialist', 'generalist']),
210
+ totalPRs: z.number().int().nonnegative(),
211
+ mergedCount: z.number().int().nonnegative(),
212
+ mergeRate: z.number(),
213
+ primaryLanguages: z.array(z.string()),
214
+ favoriteRepos: z.array(z.string()),
215
+ })
216
+ .passthrough(),
217
+ capacity: z
218
+ .object({
219
+ openPRCount: z.number().int().nonnegative(),
220
+ dormantPRCount: z.number().int().nonnegative(),
221
+ dormantRepoCount: z.number().int().nonnegative(),
222
+ overExtended: z.boolean(),
223
+ suggestedAction: z.union([
224
+ z.literal('open_more'),
225
+ z.literal('follow_up_dormant'),
226
+ z.literal('wait_on_maintainers'),
227
+ z.null(),
228
+ ]),
229
+ })
230
+ .passthrough(),
231
+ patterns: z
232
+ .object({
233
+ // Closed set on the six required PR-type buckets — drift on any
234
+ // of these breaks the snapshot rendering. `.passthrough()` allows
235
+ // additive growth (e.g., a future 'security' bucket) without a
236
+ // schema bump, but a typo on `features` → `feauters` fails here.
237
+ prTypeDistribution: z
238
+ .object({
239
+ docs: z.number(),
240
+ fixes: z.number(),
241
+ features: z.number(),
242
+ refactors: z.number(),
243
+ tests: z.number(),
244
+ other: z.number(),
245
+ })
246
+ .passthrough(),
247
+ trajectoryDirection: z.enum(['growing', 'steady', 'declining']),
248
+ averagePRSize: z.number(),
249
+ })
250
+ .passthrough(),
251
+ recommendations: z
252
+ .object({
253
+ languages: z.array(z.string()),
254
+ repos: z.array(z.string()),
255
+ issueTypes: z.array(z.string()),
256
+ avoidPatterns: z.array(z.string()),
257
+ })
258
+ .passthrough(),
259
+ })
260
+ .passthrough();
201
261
  export const DailyOutputSchema = z.object({
202
262
  digest: DailyDigestCompactSchema,
203
263
  capacity: CapacityAssessmentSchema,
@@ -209,6 +269,7 @@ export const DailyOutputSchema = z.object({
209
269
  repoGroups: z.array(CompactRepoGroupSchema),
210
270
  failures: z.array(PRCheckFailurePassthroughSchema),
211
271
  warnings: z.array(DailyWarningSchema),
272
+ strategySummary: StrategyResultSchema.nullable().optional(),
212
273
  });
213
274
  export const CompactDailyOutputSchema = z.object({
214
275
  digest: DailyDigestCompactSchema,
@@ -219,6 +280,7 @@ export const CompactDailyOutputSchema = z.object({
219
280
  commentedIssues: z.array(CommentedIssuePassthroughSchema),
220
281
  failureCount: z.number().int().nonnegative(),
221
282
  warnings: z.array(DailyWarningSchema),
283
+ strategySummary: StrategyResultSchema.nullable().optional(),
222
284
  });
223
285
  // ── Search output schema (#1147) ─────────────────────────────────────
224
286
  const SearchPrioritySchema = z.enum(['merged_pr', 'preferred_org', 'starred', 'normal']);
@@ -287,6 +349,18 @@ export const ListMoveTierOutputSchema = z.object({
287
349
  count: z.number().int().nonnegative(),
288
350
  reason: z.string().optional(),
289
351
  });
352
+ // list-mark-done (#1299): mirrors {@link MarkDoneOutput} from the command
353
+ // module. Strict shape — additional keys must be added here AND in the
354
+ // command output, otherwise the validator's `parse()` rejects the response
355
+ // before it reaches consumers.
356
+ export const ListMarkDoneOutputSchema = z.object({
357
+ marked: z.boolean(),
358
+ filePath: z.string(),
359
+ url: z.string(),
360
+ repoHeadingStruck: z.boolean(),
361
+ remainingUnderRepo: z.number().int().nonnegative(),
362
+ reason: z.string().optional(),
363
+ });
290
364
  // ── #1155: Zod coverage for remaining CLI commands ───────────────────
291
365
  export const PostOutputSchema = z.object({
292
366
  commentUrl: z.string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {