@oss-autopilot/core 3.4.1 → 3.5.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 (46) hide show
  1. package/dist/cli-registry.js +50 -0
  2. package/dist/cli.bundle.cjs +81 -78
  3. package/dist/commands/compliance-score.d.ts +21 -0
  4. package/dist/commands/compliance-score.js +156 -0
  5. package/dist/commands/index.d.ts +4 -0
  6. package/dist/commands/index.js +4 -0
  7. package/dist/commands/list-mark-done.d.ts +48 -0
  8. package/dist/commands/list-mark-done.js +213 -0
  9. package/dist/commands/parse-list.js +86 -9
  10. package/dist/commands/repo-vet.d.ts +21 -0
  11. package/dist/commands/repo-vet.js +215 -0
  12. package/dist/commands/startup.js +18 -0
  13. package/dist/core/ci-enforced-tools.d.ts +35 -0
  14. package/dist/core/ci-enforced-tools.js +109 -0
  15. package/dist/core/comment-decision.d.ts +72 -0
  16. package/dist/core/comment-decision.js +74 -0
  17. package/dist/core/compliance-score.d.ts +127 -0
  18. package/dist/core/compliance-score.js +277 -0
  19. package/dist/core/config-registry.js +12 -0
  20. package/dist/core/contributing.d.ts +52 -0
  21. package/dist/core/contributing.js +139 -0
  22. package/dist/core/extraction-categories.d.ts +55 -0
  23. package/dist/core/extraction-categories.js +108 -0
  24. package/dist/core/follow-up-history.d.ts +41 -0
  25. package/dist/core/follow-up-history.js +71 -0
  26. package/dist/core/gist-state-store.d.ts +30 -7
  27. package/dist/core/gist-state-store.js +87 -11
  28. package/dist/core/issue-conversation.js +1 -0
  29. package/dist/core/issue-effort.d.ts +29 -0
  30. package/dist/core/issue-effort.js +41 -0
  31. package/dist/core/maintainer-hints.d.ts +23 -0
  32. package/dist/core/maintainer-hints.js +36 -0
  33. package/dist/core/pr-quality-rubric.d.ts +70 -0
  34. package/dist/core/pr-quality-rubric.js +121 -0
  35. package/dist/core/repo-vet.d.ts +90 -0
  36. package/dist/core/repo-vet.js +178 -0
  37. package/dist/core/state-schema.d.ts +76 -0
  38. package/dist/core/state-schema.js +75 -0
  39. package/dist/core/strategy.d.ts +75 -0
  40. package/dist/core/strategy.js +226 -0
  41. package/dist/core/types.d.ts +2 -0
  42. package/dist/core/workflow-state.d.ts +56 -0
  43. package/dist/core/workflow-state.js +101 -0
  44. package/dist/formatters/json.d.ts +147 -0
  45. package/dist/formatters/json.js +79 -0
  46. package/package.json +1 -1
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Contribution strategy compute (#1243).
3
+ *
4
+ * Extracts the deterministic compute layer from
5
+ * `agents/contribution-strategist.md` so the categorization,
6
+ * trajectory detection, and capacity overextension rules are typed,
7
+ * unit-testable, and consumable both by the agent (synthesis layer)
8
+ * and a future auto-display in `/oss`.
9
+ *
10
+ * Same architectural shape as success-grade (#858), linked-PR
11
+ * classifier (#910), and compliance-score (#1245). Pure function — no
12
+ * I/O, no global state, no LLM. The interpretive narrative ("you're
13
+ * doing X well, here's a growth opportunity") stays in the agent
14
+ * prompt.
15
+ */
16
+ /** Minimum tracked PR history before strategy output is meaningful (#1243). */
17
+ export const STRATEGY_MIN_PRS = 10;
18
+ /** How many top-N items each "primary" / "favorite" list returns. */
19
+ const TOP_N = 5;
20
+ /** Days since `mergedAt` after which a PR no longer counts as recent for
21
+ * the active-now PR-type distribution. 90 days matches the issue's
22
+ * proposed trajectory window. */
23
+ const RECENT_WINDOW_DAYS = 90;
24
+ /** Match repo from a GitHub URL: `https://github.com/owner/repo/pull/N`. */
25
+ const REPO_FROM_URL = /https:\/\/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/\d+/i;
26
+ function parseRepo(url) {
27
+ const m = url.match(REPO_FROM_URL);
28
+ return m ? `${m[1]}/${m[2]}` : null;
29
+ }
30
+ function classifyTitle(title) {
31
+ const t = title.trim().toLowerCase();
32
+ // Match Conventional Commits prefix first; fall back to keyword search
33
+ // in the title body.
34
+ const prefix = t.match(/^(feat|fix|docs|refactor|test|perf|chore|build|ci|style|revert)(?:\([^)]+\))?!?:/);
35
+ const tag = prefix?.[1];
36
+ if (tag === 'docs')
37
+ return 'docs';
38
+ if (tag === 'fix' || tag === 'perf')
39
+ return 'fixes';
40
+ if (tag === 'feat')
41
+ return 'features';
42
+ if (tag === 'refactor')
43
+ return 'refactors';
44
+ if (tag === 'test')
45
+ return 'tests';
46
+ // Heuristic fallbacks for non-conventional titles.
47
+ if (/\b(?:doc|readme|comment|typo)\b/i.test(t))
48
+ return 'docs';
49
+ if (/\b(?:fix|bug|crash|regression|broken)\b/i.test(t))
50
+ return 'fixes';
51
+ if (/\b(?:add|implement|introduce|support|new)\b/i.test(t))
52
+ return 'features';
53
+ if (/\b(?:refactor|cleanup|extract|simplify|rename)\b/i.test(t))
54
+ return 'refactors';
55
+ if (/\btest|spec\b/i.test(t))
56
+ return 'tests';
57
+ return 'other';
58
+ }
59
+ function topNByCount(counts, n) {
60
+ return Object.entries(counts)
61
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
62
+ .slice(0, n)
63
+ .map(([name]) => name);
64
+ }
65
+ function determineStyle(_totalPRs, primaryLanguages, favoriteRepos) {
66
+ // `totalPRs` is reserved for future "explorer" classification (low-volume
67
+ // contributors). Today the function is only reachable from `computeStrategy`
68
+ // which gates at `merged.length >= STRATEGY_MIN_PRS` (10), so a `totalPRs < 5`
69
+ // branch could never fire — removed to avoid misleading future readers.
70
+ // Heavy concentration in 1-2 repos → maintainer; spread across many → generalist.
71
+ if (favoriteRepos.length === 1)
72
+ return 'maintainer';
73
+ if (primaryLanguages.length === 1 && favoriteRepos.length >= 2 && favoriteRepos.length <= 3) {
74
+ return 'specialist';
75
+ }
76
+ if (favoriteRepos.length >= 4)
77
+ return 'generalist';
78
+ return 'specialist';
79
+ }
80
+ function determineTrajectory(state) {
81
+ // Compare the last three months' merged PR counts against the prior
82
+ // three months. If each window has at least one merge, ratio >1.2 is
83
+ // growing, <0.8 is declining, otherwise steady.
84
+ const counts = state.monthlyMergedCounts ?? {};
85
+ const months = Object.keys(counts).sort();
86
+ if (months.length < 6)
87
+ return 'steady';
88
+ const recent = months.slice(-3).reduce((sum, m) => sum + (counts[m] ?? 0), 0);
89
+ const prior = months.slice(-6, -3).reduce((sum, m) => sum + (counts[m] ?? 0), 0);
90
+ if (prior === 0)
91
+ return recent > 0 ? 'growing' : 'steady';
92
+ const ratio = recent / prior;
93
+ if (ratio >= 1.2)
94
+ return 'growing';
95
+ if (ratio <= 0.8)
96
+ return 'declining';
97
+ return 'steady';
98
+ }
99
+ function recommendForOverExtension(openPRCount, dormantPRCount, overExtended) {
100
+ if (overExtended)
101
+ return 'follow_up_dormant';
102
+ if (dormantPRCount > 0 && openPRCount > 5)
103
+ return 'wait_on_maintainers';
104
+ if (openPRCount === 0)
105
+ return 'open_more';
106
+ return null;
107
+ }
108
+ /**
109
+ * Compute the deterministic strategy signal from agent state. Returns
110
+ * null when state is thinner than {@link STRATEGY_MIN_PRS} merged PRs —
111
+ * the auto-display surface uses this null sentinel as the
112
+ * minimum-data gate (#1243).
113
+ */
114
+ export function computeStrategy(state) {
115
+ const merged = state.mergedPRs ?? [];
116
+ const closed = state.closedPRs ?? [];
117
+ const totalPRs = merged.length + closed.length;
118
+ if (merged.length < STRATEGY_MIN_PRS)
119
+ return null;
120
+ // Per-repo PR counts (merged). Used both for "favorite repos" and to
121
+ // back-fill primary languages from `repoScores` entries that overlap.
122
+ const mergedByRepo = {};
123
+ for (const pr of merged) {
124
+ const repo = parseRepo(pr.url);
125
+ if (!repo)
126
+ continue;
127
+ mergedByRepo[repo] = (mergedByRepo[repo] ?? 0) + 1;
128
+ }
129
+ const favoriteRepos = topNByCount(mergedByRepo, TOP_N);
130
+ // Languages: weight a repo's language by that repo's merged count so
131
+ // `vercel/next.js` (TypeScript) counts more than a one-off PR to a
132
+ // Lua project even if both repos are in `repoScores`.
133
+ const languageCounts = {};
134
+ for (const [repo, count] of Object.entries(mergedByRepo)) {
135
+ const score = state.repoScores[repo];
136
+ const lang = score?.language;
137
+ if (!lang)
138
+ continue;
139
+ languageCounts[lang] = (languageCounts[lang] ?? 0) + count;
140
+ }
141
+ const primaryLanguages = topNByCount(languageCounts, TOP_N);
142
+ // PR-type distribution over the recent window.
143
+ const distribution = {
144
+ docs: 0,
145
+ fixes: 0,
146
+ features: 0,
147
+ refactors: 0,
148
+ tests: 0,
149
+ other: 0,
150
+ };
151
+ const cutoff = Date.now() - RECENT_WINDOW_DAYS * 86400000;
152
+ for (const pr of merged) {
153
+ const mergedAt = Date.parse(pr.mergedAt);
154
+ if (Number.isNaN(mergedAt) || mergedAt < cutoff)
155
+ continue;
156
+ distribution[classifyTitle(pr.title)] += 1;
157
+ }
158
+ // Capacity: read from the last digest. When no digest exists yet,
159
+ // default to zero (the agent has nothing to recommend without a
160
+ // digest). The DailyDigest schema does not store a `dormantCount`
161
+ // directly — derive it from the `waitingOnMaintainerPRs` array,
162
+ // which is the "PR flagged as awaiting maintainer review" bucket
163
+ // produced by `pr-monitor`.
164
+ const summary = state.lastDigest?.summary;
165
+ const openPRCount = summary?.totalActivePRs ?? 0;
166
+ const waiting = state.lastDigest?.waitingOnMaintainerPRs ?? [];
167
+ const dormantPRCount = waiting.length;
168
+ // Overextended: dormant PRs spread across 2+ repos. Same definition
169
+ // the issue body uses.
170
+ const dormantRepos = new Set(waiting
171
+ .map((pr) => (typeof pr.url === 'string' ? parseRepo(pr.url) : null))
172
+ .filter((r) => r !== null));
173
+ const overExtended = dormantPRCount >= 2 && dormantRepos.size >= 2;
174
+ const profile = {
175
+ style: determineStyle(totalPRs, primaryLanguages, favoriteRepos),
176
+ totalPRs,
177
+ mergedCount: merged.length,
178
+ mergeRate: totalPRs === 0 ? 0 : merged.length / totalPRs,
179
+ primaryLanguages,
180
+ favoriteRepos,
181
+ };
182
+ const capacity = {
183
+ openPRCount,
184
+ dormantPRCount,
185
+ overExtended,
186
+ suggestedAction: recommendForOverExtension(openPRCount, dormantPRCount, overExtended),
187
+ };
188
+ const patterns = {
189
+ prTypeDistribution: distribution,
190
+ trajectoryDirection: determineTrajectory(state),
191
+ // Without per-PR diff size in StoredMergedPR, a true "average PR
192
+ // size" lookup is out of scope until that data is captured (#1243
193
+ // explicitly defers this). Surface 0 so downstream callers know the
194
+ // signal is unavailable rather than fabricating a value.
195
+ averagePRSize: 0,
196
+ };
197
+ const recommendations = {
198
+ // The deterministic recommendations layer reuses the profile
199
+ // signals — the agent's coaching prose layers on top of these.
200
+ languages: primaryLanguages,
201
+ repos: favoriteRepos,
202
+ issueTypes: deriveIssueTypePreferences(distribution),
203
+ avoidPatterns: deriveAvoidPatterns(capacity, patterns),
204
+ };
205
+ return { profile, capacity, patterns, recommendations };
206
+ }
207
+ function deriveIssueTypePreferences(distribution) {
208
+ // Recommend the user's two strongest PR types — they have a track
209
+ // record there, so issues in those buckets are higher-yield.
210
+ const ranked = Object.entries(distribution)
211
+ .filter(([type]) => type !== 'other')
212
+ .sort((a, b) => b[1] - a[1])
213
+ .slice(0, 2)
214
+ .map(([type]) => type);
215
+ return ranked;
216
+ }
217
+ function deriveAvoidPatterns(capacity, patterns) {
218
+ const out = [];
219
+ if (capacity.overExtended) {
220
+ out.push('opening new PRs while dormant ones await review across multiple repos');
221
+ }
222
+ if (patterns.trajectoryDirection === 'declining') {
223
+ out.push('drifting away from a previously productive cadence — capacity may be saturated');
224
+ }
225
+ return out;
226
+ }
@@ -207,6 +207,8 @@ interface CommentedIssueBase {
207
207
  title: string;
208
208
  url: string;
209
209
  userLastCommentedAt: string;
210
+ /** User's most recent comment body, truncated to 200 chars (+ "..." suffix when truncated). #1290 */
211
+ userLastCommentBody: string;
210
212
  labels: string[];
211
213
  daysSinceUserComment: number;
212
214
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Workflow-state helpers (#1280).
3
+ *
4
+ * Centralizes the read/write logic for `state.workflowState`. Used
5
+ * by `draft-first-workflow.md`, `work-through-issues.md`, and
6
+ * `pre-commit-review.md` to record pause points and offer
7
+ * Resume / Restart / Discard at the next `/oss` invocation.
8
+ *
9
+ * Pure functions — callers manage state I/O. Same architectural
10
+ * shape as the recent #1277 (follow-up-history) helpers.
11
+ */
12
+ import type { AgentState, WorkflowState } from './state-schema.js';
13
+ export type WorkflowName = WorkflowState['workflowName'];
14
+ /**
15
+ * Snapshot the user's current workflow position. Returns the
16
+ * patched state; callers persist via `StateManager.save()` (or the
17
+ * gist `checkpoint()` path).
18
+ */
19
+ export declare function recordWorkflowPause(state: AgentState, next: Omit<WorkflowState, 'lastUpdatedAt'>, now?: Date): AgentState;
20
+ /**
21
+ * Read the current workflow snapshot, or null when no pause is
22
+ * recorded.
23
+ */
24
+ export declare function getWorkflowState(state: AgentState): WorkflowState | null;
25
+ /**
26
+ * Discard the current pause snapshot. Used by:
27
+ * - the workflow completing normally,
28
+ * - the user picking "Discard and start fresh" at the resume
29
+ * prompt,
30
+ * - any consumer that detects the snapshot has gone stale (branch
31
+ * deleted, repo no longer reachable).
32
+ */
33
+ export declare function clearWorkflowState(state: AgentState): AgentState;
34
+ /**
35
+ * Append a step name to `completedSteps` and update `currentStep`
36
+ * to the supplied next step. Convenience wrapper around the immer-
37
+ * style read/replace pattern. Returns the patched state.
38
+ */
39
+ export declare function advanceWorkflowStep(state: AgentState, nextStep: string, now?: Date): AgentState;
40
+ /**
41
+ * Merge per-step data into `stepData` without overwriting other
42
+ * keys. Useful when a workflow's resume needs to restore non-trivial
43
+ * context (skipped compliance items, last review pass count, etc.).
44
+ */
45
+ export declare function setStepData(state: AgentState, step: string, data: unknown, now?: Date): AgentState;
46
+ /**
47
+ * Read step data for a specific step, or undefined when none was
48
+ * stored. Callers typically narrow the unknown via a type guard.
49
+ */
50
+ export declare function getStepData(state: AgentState, step: string): unknown;
51
+ /**
52
+ * Whether the recorded pause is on the supplied workflow. Useful
53
+ * for the router to decide whether to offer Resume vs ignore the
54
+ * snapshot when the current `/oss` invocation is unrelated.
55
+ */
56
+ export declare function isPausedIn(state: AgentState, workflowName: WorkflowName): boolean;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Workflow-state helpers (#1280).
3
+ *
4
+ * Centralizes the read/write logic for `state.workflowState`. Used
5
+ * by `draft-first-workflow.md`, `work-through-issues.md`, and
6
+ * `pre-commit-review.md` to record pause points and offer
7
+ * Resume / Restart / Discard at the next `/oss` invocation.
8
+ *
9
+ * Pure functions — callers manage state I/O. Same architectural
10
+ * shape as the recent #1277 (follow-up-history) helpers.
11
+ */
12
+ /**
13
+ * Snapshot the user's current workflow position. Returns the
14
+ * patched state; callers persist via `StateManager.save()` (or the
15
+ * gist `checkpoint()` path).
16
+ */
17
+ export function recordWorkflowPause(state, next, now = new Date()) {
18
+ const incoming = {
19
+ ...next,
20
+ lastUpdatedAt: now.toISOString(),
21
+ };
22
+ return { ...state, workflowState: incoming };
23
+ }
24
+ /**
25
+ * Read the current workflow snapshot, or null when no pause is
26
+ * recorded.
27
+ */
28
+ export function getWorkflowState(state) {
29
+ return state.workflowState ?? null;
30
+ }
31
+ /**
32
+ * Discard the current pause snapshot. Used by:
33
+ * - the workflow completing normally,
34
+ * - the user picking "Discard and start fresh" at the resume
35
+ * prompt,
36
+ * - any consumer that detects the snapshot has gone stale (branch
37
+ * deleted, repo no longer reachable).
38
+ */
39
+ export function clearWorkflowState(state) {
40
+ if (!state.workflowState)
41
+ return state;
42
+ const { workflowState: _drop, ...rest } = state;
43
+ return rest;
44
+ }
45
+ /**
46
+ * Append a step name to `completedSteps` and update `currentStep`
47
+ * to the supplied next step. Convenience wrapper around the immer-
48
+ * style read/replace pattern. Returns the patched state.
49
+ */
50
+ export function advanceWorkflowStep(state, nextStep, now = new Date()) {
51
+ const ws = state.workflowState;
52
+ if (!ws)
53
+ return state;
54
+ if (ws.currentStep === nextStep)
55
+ return state;
56
+ const completed = [...ws.completedSteps];
57
+ if (!completed.includes(ws.currentStep))
58
+ completed.push(ws.currentStep);
59
+ return {
60
+ ...state,
61
+ workflowState: {
62
+ ...ws,
63
+ currentStep: nextStep,
64
+ completedSteps: completed,
65
+ lastUpdatedAt: now.toISOString(),
66
+ },
67
+ };
68
+ }
69
+ /**
70
+ * Merge per-step data into `stepData` without overwriting other
71
+ * keys. Useful when a workflow's resume needs to restore non-trivial
72
+ * context (skipped compliance items, last review pass count, etc.).
73
+ */
74
+ export function setStepData(state, step, data, now = new Date()) {
75
+ const ws = state.workflowState;
76
+ if (!ws)
77
+ return state;
78
+ return {
79
+ ...state,
80
+ workflowState: {
81
+ ...ws,
82
+ stepData: { ...ws.stepData, [step]: data },
83
+ lastUpdatedAt: now.toISOString(),
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * Read step data for a specific step, or undefined when none was
89
+ * stored. Callers typically narrow the unknown via a type guard.
90
+ */
91
+ export function getStepData(state, step) {
92
+ return state.workflowState?.stepData[step];
93
+ }
94
+ /**
95
+ * Whether the recorded pause is on the supplied workflow. Useful
96
+ * for the router to decide whether to offer Resume vs ignore the
97
+ * snapshot when the current `/oss` invocation is unrelated.
98
+ */
99
+ export function isPausedIn(state, workflowName) {
100
+ return state.workflowState?.workflowName === workflowName;
101
+ }
@@ -534,6 +534,134 @@ export declare const PRTemplateOutputSchema: z.ZodObject<{
534
534
  source: z.ZodNullable<z.ZodString>;
535
535
  error: z.ZodOptional<z.ZodString>;
536
536
  }, z.core.$strip>;
537
+ export declare const RepoVetOutputSchema: z.ZodObject<{
538
+ repoSlug: z.ZodString;
539
+ fetchedAt: z.ZodString;
540
+ repoMeta: z.ZodObject<{
541
+ stars: z.ZodNumber;
542
+ forks: z.ZodNumber;
543
+ openIssues: z.ZodNumber;
544
+ watchers: z.ZodNumber;
545
+ isArchived: z.ZodBoolean;
546
+ lastPushed: z.ZodString;
547
+ createdAt: z.ZodString;
548
+ }, z.core.$strip>;
549
+ prMergeTime: z.ZodObject<{
550
+ avgDays: z.ZodNullable<z.ZodNumber>;
551
+ medianDays: z.ZodNullable<z.ZodNumber>;
552
+ sampleSize: z.ZodNumber;
553
+ sourceWindowDays: z.ZodLiteral<90>;
554
+ }, z.core.$strip>;
555
+ mergeRate: z.ZodObject<{
556
+ merged: z.ZodNumber;
557
+ opened: z.ZodNumber;
558
+ percent: z.ZodNullable<z.ZodNumber>;
559
+ windowDays: z.ZodLiteral<90>;
560
+ }, z.core.$strip>;
561
+ maintainerActivity: z.ZodObject<{
562
+ lastCommitISO: z.ZodNullable<z.ZodString>;
563
+ contributorsLast90d: z.ZodNumber;
564
+ lastReleaseISO: z.ZodNullable<z.ZodString>;
565
+ }, z.core.$strip>;
566
+ communityHealth: z.ZodObject<{
567
+ contributing: z.ZodBoolean;
568
+ issueTemplates: z.ZodBoolean;
569
+ prTemplate: z.ZodBoolean;
570
+ codeOfConduct: z.ZodBoolean;
571
+ incomplete: z.ZodOptional<z.ZodBoolean>;
572
+ }, z.core.$strip>;
573
+ rubricScore: z.ZodNumber;
574
+ rubricVerdict: z.ZodEnum<{
575
+ recommended: "recommended";
576
+ proceed_with_caution: "proceed_with_caution";
577
+ avoid: "avoid";
578
+ }>;
579
+ }, z.core.$strip>;
580
+ /**
581
+ * The CLI wrapper renames the core function's `repo` metadata object to
582
+ * `repoMeta` so the top-level slug doesn't collide with it. The TS type
583
+ * is derived from the Zod schema so any drift between schema and type
584
+ * fails at compile time.
585
+ */
586
+ export type RepoVetOutput = z.infer<typeof RepoVetOutputSchema>;
587
+ export declare const ComplianceScoreOutputSchema: z.ZodObject<{
588
+ pr: z.ZodObject<{
589
+ repo: z.ZodString;
590
+ number: z.ZodNumber;
591
+ title: z.ZodString;
592
+ url: z.ZodString;
593
+ }, z.core.$strip>;
594
+ score: z.ZodNumber;
595
+ rating: z.ZodEnum<{
596
+ ready: "ready";
597
+ minor: "minor";
598
+ fix_first: "fix_first";
599
+ significant_work: "significant_work";
600
+ }>;
601
+ emoji: z.ZodEnum<{
602
+ "\uD83C\uDF1F": "🌟";
603
+ "\u2705": "✅";
604
+ "\u26A0\uFE0F": "⚠️";
605
+ "\u274C": "❌";
606
+ }>;
607
+ checks: z.ZodObject<{
608
+ issueReference: z.ZodObject<{
609
+ status: z.ZodEnum<{
610
+ fail: "fail";
611
+ pass: "pass";
612
+ warn: "warn";
613
+ }>;
614
+ weight: z.ZodNumber;
615
+ detail: z.ZodString;
616
+ }, z.core.$strip>;
617
+ description: z.ZodObject<{
618
+ status: z.ZodEnum<{
619
+ fail: "fail";
620
+ pass: "pass";
621
+ warn: "warn";
622
+ }>;
623
+ weight: z.ZodNumber;
624
+ detail: z.ZodString;
625
+ }, z.core.$strip>;
626
+ focusedChanges: z.ZodObject<{
627
+ status: z.ZodEnum<{
628
+ fail: "fail";
629
+ pass: "pass";
630
+ warn: "warn";
631
+ }>;
632
+ weight: z.ZodNumber;
633
+ detail: z.ZodString;
634
+ }, z.core.$strip>;
635
+ tests: z.ZodObject<{
636
+ status: z.ZodEnum<{
637
+ fail: "fail";
638
+ pass: "pass";
639
+ warn: "warn";
640
+ }>;
641
+ weight: z.ZodNumber;
642
+ detail: z.ZodString;
643
+ }, z.core.$strip>;
644
+ title: z.ZodObject<{
645
+ status: z.ZodEnum<{
646
+ fail: "fail";
647
+ pass: "pass";
648
+ warn: "warn";
649
+ }>;
650
+ weight: z.ZodNumber;
651
+ detail: z.ZodString;
652
+ }, z.core.$strip>;
653
+ branch: z.ZodObject<{
654
+ status: z.ZodEnum<{
655
+ fail: "fail";
656
+ pass: "pass";
657
+ warn: "warn";
658
+ }>;
659
+ weight: z.ZodNumber;
660
+ detail: z.ZodString;
661
+ }, z.core.$strip>;
662
+ }, z.core.$strip>;
663
+ }, z.core.$strip>;
664
+ export type ComplianceScoreOutput = z.infer<typeof ComplianceScoreOutputSchema>;
537
665
  export declare const ParseIssueListOutputSchema: z.ZodObject<{
538
666
  available: z.ZodArray<z.ZodObject<{
539
667
  repo: z.ZodString;
@@ -696,6 +824,25 @@ export interface StartupOutput {
696
824
  * a structured signal to surface or recover from the failure.
697
825
  */
698
826
  dashboardError?: string;
827
+ /**
828
+ * Status of the dashboard SPA build that ran (or didn't) before this
829
+ * startup invocation (#1293). Populated by the workflow shell via
830
+ * `OSS_DASHBOARD_BUILD_STATUS`; absent when the CLI is invoked outside
831
+ * the plugin workflow:
832
+ * - `'fresh'` — built artifact was up-to-date, no rebuild attempted.
833
+ * - `'rebuilt'` — rebuild ran and succeeded.
834
+ * - `'failed'` — rebuild ran and failed; `dashboardUrl` may serve stale
835
+ * or missing assets until the build is fixed.
836
+ * - `'missing-pnpm'` — rebuild was needed but pnpm (required for the
837
+ * workspace dependency) was unavailable.
838
+ */
839
+ dashboardBuildStatus?: 'fresh' | 'rebuilt' | 'failed' | 'missing-pnpm';
840
+ /**
841
+ * Last few lines of the dashboard build log when `dashboardBuildStatus`
842
+ * is `'failed'` or `'missing-pnpm'`. Surfaced by the workflow as a
843
+ * one-line warning so the user sees what broke without leaving `/oss`.
844
+ */
845
+ dashboardBuildErrorTail?: string;
699
846
  issueList?: IssueListInfo;
700
847
  }
701
848
  /**
@@ -376,6 +376,85 @@ export const PRTemplateOutputSchema = z.object({
376
376
  source: z.string().nullable(),
377
377
  error: z.string().optional(),
378
378
  });
379
+ /**
380
+ * Output of the `repo-vet` CLI command (#1271, follow-up to #1242).
381
+ *
382
+ * Validates the full nested shape so a refactor that drops or
383
+ * mis-types one of these fields trips `outputJsonValidated` rather
384
+ * than silently shipping a broken envelope (#1245 contract pattern,
385
+ * matching `ComplianceScoreOutputSchema` below).
386
+ */
387
+ const RepoVetMetaSchema = z.object({
388
+ stars: z.number(),
389
+ forks: z.number(),
390
+ openIssues: z.number(),
391
+ watchers: z.number(),
392
+ isArchived: z.boolean(),
393
+ lastPushed: z.string(),
394
+ createdAt: z.string(),
395
+ });
396
+ export const RepoVetOutputSchema = z.object({
397
+ repoSlug: z.string(),
398
+ fetchedAt: z.string(),
399
+ repoMeta: RepoVetMetaSchema,
400
+ prMergeTime: z.object({
401
+ avgDays: z.number().nullable(),
402
+ medianDays: z.number().nullable(),
403
+ sampleSize: z.number().int().nonnegative(),
404
+ sourceWindowDays: z.literal(90),
405
+ }),
406
+ mergeRate: z.object({
407
+ merged: z.number().int().nonnegative(),
408
+ opened: z.number().int().nonnegative(),
409
+ percent: z.number().nullable(),
410
+ windowDays: z.literal(90),
411
+ }),
412
+ maintainerActivity: z.object({
413
+ lastCommitISO: z.string().nullable(),
414
+ contributorsLast90d: z.number().int().nonnegative(),
415
+ lastReleaseISO: z.string().nullable(),
416
+ }),
417
+ communityHealth: z.object({
418
+ contributing: z.boolean(),
419
+ issueTemplates: z.boolean(),
420
+ prTemplate: z.boolean(),
421
+ codeOfConduct: z.boolean(),
422
+ /**
423
+ * True when at least one community-health probe failed for a non-404
424
+ * reason (auth, rate-limit, 5xx). The boolean flags above stay
425
+ * `false` for unprobed paths in that case, so consumers should treat
426
+ * the false flags as "could not determine" rather than "confirmed
427
+ * absent" when this is set.
428
+ */
429
+ incomplete: z.boolean().optional(),
430
+ }),
431
+ rubricScore: z.number(),
432
+ rubricVerdict: z.enum(['recommended', 'proceed_with_caution', 'avoid']),
433
+ });
434
+ const ComplianceCheckSchema = z.object({
435
+ status: z.enum(['pass', 'warn', 'fail']),
436
+ weight: z.number(),
437
+ detail: z.string(),
438
+ });
439
+ export const ComplianceScoreOutputSchema = z.object({
440
+ pr: z.object({
441
+ repo: z.string(),
442
+ number: z.number().int().nonnegative(),
443
+ title: z.string(),
444
+ url: z.string(),
445
+ }),
446
+ score: z.number().int().min(0).max(100),
447
+ rating: z.enum(['ready', 'minor', 'fix_first', 'significant_work']),
448
+ emoji: z.enum(['🌟', '✅', '⚠️', '❌']),
449
+ checks: z.object({
450
+ issueReference: ComplianceCheckSchema,
451
+ description: ComplianceCheckSchema,
452
+ focusedChanges: ComplianceCheckSchema,
453
+ tests: ComplianceCheckSchema,
454
+ title: ComplianceCheckSchema,
455
+ branch: ComplianceCheckSchema,
456
+ }),
457
+ });
379
458
  const ParsedIssueItemSchema = z.object({
380
459
  repo: z.string(),
381
460
  number: z.number(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {