@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,90 @@
1
+ /**
2
+ * Repo health scoring (#1242).
3
+ *
4
+ * Extracted from `agents/repo-evaluator.md`'s in-prompt assembly logic
5
+ * so the rubric weights, sub-factor thresholds, and verdict cutoffs
6
+ * are deterministic, unit-testable, and tunable without editing
7
+ * markdown. Same architectural shape as success-grade (#858),
8
+ * compliance-score (#1245), and strategy (#1243).
9
+ *
10
+ * The function is pure — callers (the MCP tool, the CLI command)
11
+ * supply pre-fetched repo signals so the score is reproducible
12
+ * against fixture data and offline replays.
13
+ *
14
+ * Rubric reference: docs/repo-rubric.md.
15
+ */
16
+ export interface RepoVetInput {
17
+ /** Star count from the GitHub repo metadata. */
18
+ stars: number;
19
+ forks: number;
20
+ openIssues: number;
21
+ watchers: number;
22
+ isArchived: boolean;
23
+ /** ISO-8601 timestamp of the last push. */
24
+ lastPushed: string;
25
+ /** ISO-8601 timestamp of repo creation. */
26
+ createdAt: string;
27
+ /** Number of commits to the default branch in the last 30 days. */
28
+ commitsLast30Days: number;
29
+ /** Per-PR `createdAt → mergedAt` durations in days, for the last 90
30
+ * days of merged PRs. Used to derive avg / median merge time. */
31
+ prMergeTimesDays: number[];
32
+ /** Count of merges in the last 90 days. */
33
+ mergedCount90Days: number;
34
+ /** Count of opened PRs in the last 90 days. Used for merge rate. */
35
+ openedCount90Days: number;
36
+ /** ISO-8601 timestamp of the most recent commit on the default branch. */
37
+ lastCommitISO: string | null;
38
+ /** Distinct authors who committed in the last 90 days. */
39
+ contributorsLast90d: number;
40
+ /** ISO-8601 timestamp of the most recent published release. */
41
+ lastReleaseISO: string | null;
42
+ hasContributing: boolean;
43
+ hasIssueTemplates: boolean;
44
+ hasPRTemplate: boolean;
45
+ hasCodeOfConduct: boolean;
46
+ }
47
+ export type RepoVetVerdict = 'recommended' | 'proceed_with_caution' | 'avoid';
48
+ export interface RepoVetResult {
49
+ repo: {
50
+ stars: number;
51
+ forks: number;
52
+ openIssues: number;
53
+ watchers: number;
54
+ isArchived: boolean;
55
+ lastPushed: string;
56
+ createdAt: string;
57
+ };
58
+ prMergeTime: {
59
+ avgDays: number | null;
60
+ medianDays: number | null;
61
+ sampleSize: number;
62
+ sourceWindowDays: 90;
63
+ };
64
+ mergeRate: {
65
+ merged: number;
66
+ opened: number;
67
+ percent: number | null;
68
+ windowDays: 90;
69
+ };
70
+ maintainerActivity: {
71
+ lastCommitISO: string | null;
72
+ contributorsLast90d: number;
73
+ lastReleaseISO: string | null;
74
+ };
75
+ communityHealth: {
76
+ contributing: boolean;
77
+ issueTemplates: boolean;
78
+ prTemplate: boolean;
79
+ codeOfConduct: boolean;
80
+ };
81
+ /** Weighted 1-10 score per docs/repo-rubric.md. */
82
+ rubricScore: number;
83
+ /** Top-line verdict derived from the score and red-flag overrides. */
84
+ rubricVerdict: RepoVetVerdict;
85
+ }
86
+ /**
87
+ * Compute repo-health metrics and an overall rubric score from
88
+ * pre-fetched signals (#1242). Pure function — no I/O, no global state.
89
+ */
90
+ export declare function computeRepoVet(input: RepoVetInput): RepoVetResult;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Repo health scoring (#1242).
3
+ *
4
+ * Extracted from `agents/repo-evaluator.md`'s in-prompt assembly logic
5
+ * so the rubric weights, sub-factor thresholds, and verdict cutoffs
6
+ * are deterministic, unit-testable, and tunable without editing
7
+ * markdown. Same architectural shape as success-grade (#858),
8
+ * compliance-score (#1245), and strategy (#1243).
9
+ *
10
+ * The function is pure — callers (the MCP tool, the CLI command)
11
+ * supply pre-fetched repo signals so the score is reproducible
12
+ * against fixture data and offline replays.
13
+ *
14
+ * Rubric reference: docs/repo-rubric.md.
15
+ */
16
+ const WEIGHTS = {
17
+ activity: 0.25,
18
+ prSpeed: 0.25,
19
+ mergeRate: 0.2,
20
+ guidelines: 0.1,
21
+ stability: 0.05,
22
+ /** Responsiveness (15%) is documented in the rubric but the input
23
+ * shape doesn't surface it — `gh pr list --json` doesn't expose
24
+ * "time to first review". Per the rubric note, omit it rather than
25
+ * fabricate it. The remaining weights total 0.85 and are normalized
26
+ * to a 10-point ceiling at the end. */
27
+ };
28
+ const SUM_OF_AVAILABLE_WEIGHTS = WEIGHTS.activity + WEIGHTS.prSpeed + WEIGHTS.mergeRate + WEIGHTS.guidelines + WEIGHTS.stability;
29
+ const VERDICT_CUTOFFS = {
30
+ recommended: 7.5,
31
+ cautious: 5,
32
+ };
33
+ function median(nums) {
34
+ if (nums.length === 0)
35
+ return 0;
36
+ const sorted = [...nums].sort((a, b) => a - b);
37
+ const mid = Math.floor(sorted.length / 2);
38
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
39
+ }
40
+ function activitySubScore(input) {
41
+ // 1.0 when commits/30d ≥ 30 (one per day), 0 when 0, linear in between.
42
+ if (input.isArchived)
43
+ return 0;
44
+ return Math.min(1, input.commitsLast30Days / 30);
45
+ }
46
+ function prSpeedSubScore(input) {
47
+ // Rubric: avg PR merge time < 7 days = healthy. Linear: 0d → 1.0,
48
+ // 14d+ → 0.0. Sample of zero merges = 0 score (no signal of speed).
49
+ if (input.prMergeTimesDays.length === 0)
50
+ return 0;
51
+ const avg = input.prMergeTimesDays.reduce((sum, d) => sum + d, 0) / input.prMergeTimesDays.length;
52
+ if (avg <= 0)
53
+ return 1;
54
+ if (avg >= 14)
55
+ return 0;
56
+ return Math.max(0, Math.min(1, (14 - avg) / 14));
57
+ }
58
+ function mergeRateSubScore(input) {
59
+ // Rubric: >70% merged in last 90 days. Linear: 100% → 1.0, 0% → 0.
60
+ if (input.openedCount90Days === 0)
61
+ return 0;
62
+ const rate = input.mergedCount90Days / input.openedCount90Days;
63
+ return Math.max(0, Math.min(1, rate));
64
+ }
65
+ function guidelinesSubScore(input) {
66
+ // Each of the four community-health flags contributes equally.
67
+ let score = 0;
68
+ if (input.hasContributing)
69
+ score += 0.4;
70
+ if (input.hasIssueTemplates)
71
+ score += 0.25;
72
+ if (input.hasPRTemplate)
73
+ score += 0.25;
74
+ if (input.hasCodeOfConduct)
75
+ score += 0.1;
76
+ return Math.min(1, score);
77
+ }
78
+ function stabilitySubScore(input) {
79
+ // Rubric: not archived, regular releases. Half-credit for not-archived,
80
+ // half for a release within the last 6 months.
81
+ if (input.isArchived)
82
+ return 0;
83
+ let score = 0.5;
84
+ if (input.lastReleaseISO) {
85
+ const since = Date.now() - new Date(input.lastReleaseISO).getTime();
86
+ const sixMonths = 1000 * 60 * 60 * 24 * 30 * 6;
87
+ if (since <= sixMonths)
88
+ score += 0.5;
89
+ }
90
+ return score;
91
+ }
92
+ /**
93
+ * Hard red-flag overrides — when one fires, verdict drops to `avoid`
94
+ * regardless of the weighted score. Sourced verbatim from the rubric.
95
+ */
96
+ function hasRedFlags(input) {
97
+ if (input.isArchived)
98
+ return true;
99
+ if (input.lastCommitISO) {
100
+ const sinceCommit = Date.now() - new Date(input.lastCommitISO).getTime();
101
+ const sixtyDays = 60 * 24 * 60 * 60 * 1000;
102
+ if (sinceCommit > sixtyDays)
103
+ return true;
104
+ }
105
+ if (input.openedCount90Days > 0 && input.mergedCount90Days === 0)
106
+ return true;
107
+ return false;
108
+ }
109
+ function deriveVerdict(score, redFlags) {
110
+ if (redFlags)
111
+ return 'avoid';
112
+ if (score >= VERDICT_CUTOFFS.recommended)
113
+ return 'recommended';
114
+ if (score >= VERDICT_CUTOFFS.cautious)
115
+ return 'proceed_with_caution';
116
+ return 'avoid';
117
+ }
118
+ /**
119
+ * Compute repo-health metrics and an overall rubric score from
120
+ * pre-fetched signals (#1242). Pure function — no I/O, no global state.
121
+ */
122
+ export function computeRepoVet(input) {
123
+ const subScores = {
124
+ activity: activitySubScore(input),
125
+ prSpeed: prSpeedSubScore(input),
126
+ mergeRate: mergeRateSubScore(input),
127
+ guidelines: guidelinesSubScore(input),
128
+ stability: stabilitySubScore(input),
129
+ };
130
+ // Weighted average normalized to the 10-point ceiling.
131
+ const weightedSum = subScores.activity * WEIGHTS.activity +
132
+ subScores.prSpeed * WEIGHTS.prSpeed +
133
+ subScores.mergeRate * WEIGHTS.mergeRate +
134
+ subScores.guidelines * WEIGHTS.guidelines +
135
+ subScores.stability * WEIGHTS.stability;
136
+ const rubricScore = Math.round((weightedSum / SUM_OF_AVAILABLE_WEIGHTS) * 100) / 10;
137
+ const sampleSize = input.prMergeTimesDays.length;
138
+ const avgDays = sampleSize === 0 ? null : input.prMergeTimesDays.reduce((s, d) => s + d, 0) / sampleSize;
139
+ const medianDays = sampleSize === 0 ? null : median(input.prMergeTimesDays);
140
+ // Cap at 100. A repo clearing a backlog can have mergedCount > openedCount
141
+ // because PRs opened before the window can merge inside it — without the
142
+ // cap, the surfaced text would read "Merge rate (90d): 160% (8/5)" which
143
+ // looks like a tool bug. The score itself is fine (mergeRateSubScore
144
+ // clamps separately); this just keeps the display sensible.
145
+ const mergeRatePercent = input.openedCount90Days === 0 ? null : Math.min(100, (input.mergedCount90Days / input.openedCount90Days) * 100);
146
+ const verdict = deriveVerdict(rubricScore, hasRedFlags(input));
147
+ return {
148
+ repo: {
149
+ stars: input.stars,
150
+ forks: input.forks,
151
+ openIssues: input.openIssues,
152
+ watchers: input.watchers,
153
+ isArchived: input.isArchived,
154
+ lastPushed: input.lastPushed,
155
+ createdAt: input.createdAt,
156
+ },
157
+ prMergeTime: { avgDays, medianDays, sampleSize, sourceWindowDays: 90 },
158
+ mergeRate: {
159
+ merged: input.mergedCount90Days,
160
+ opened: input.openedCount90Days,
161
+ percent: mergeRatePercent,
162
+ windowDays: 90,
163
+ },
164
+ maintainerActivity: {
165
+ lastCommitISO: input.lastCommitISO,
166
+ contributorsLast90d: input.contributorsLast90d,
167
+ lastReleaseISO: input.lastReleaseISO,
168
+ },
169
+ communityHealth: {
170
+ contributing: input.hasContributing,
171
+ issueTemplates: input.hasIssueTemplates,
172
+ prTemplate: input.hasPRTemplate,
173
+ codeOfConduct: input.hasCodeOfConduct,
174
+ },
175
+ rubricScore,
176
+ rubricVerdict: verdict,
177
+ };
178
+ }
@@ -78,6 +78,53 @@ export declare const AnalyzedIssueConversationSchema: z.ZodObject<{
78
78
  repo: z.ZodString;
79
79
  analyzedAt: z.ZodString;
80
80
  }, z.core.$strip>;
81
+ /**
82
+ * One entry in a PR's follow-up history (#1277). Tier matches the
83
+ * cadence labels in `skills/pr-etiquette/SKILL.md` (light_check_in
84
+ * for 7-13 days, direct_check_in for 14-29 days, final_check_in for
85
+ * 30+ days). The `draftPath` is optional so an entry recorded from a
86
+ * direct `gh pr comment` (no draft) still validates.
87
+ */
88
+ export declare const FollowUpEntrySchema: z.ZodObject<{
89
+ tier: z.ZodEnum<{
90
+ light_check_in: "light_check_in";
91
+ direct_check_in: "direct_check_in";
92
+ final_check_in: "final_check_in";
93
+ }>;
94
+ timestamp: z.ZodString;
95
+ draftPath: z.ZodOptional<z.ZodString>;
96
+ }, z.core.$strip>;
97
+ export type FollowUpEntry = z.infer<typeof FollowUpEntrySchema>;
98
+ /**
99
+ * Pause-point snapshot for resumable workflows (#1280). When the user
100
+ * picks "Done for now" inside `draft-first-workflow.md`,
101
+ * `work-through-issues.md`, or `pre-commit-review.md`, the workflow
102
+ * records its position here. The next `/oss` run consults this state
103
+ * and offers "Resume / Restart / Discard" instead of restarting from
104
+ * the beginning.
105
+ *
106
+ * `stepData` is intentionally typed as `Record<string, unknown>` so
107
+ * each workflow can persist whatever per-step context it needs to
108
+ * resume cleanly (compliance-gap skip list, last review pass count,
109
+ * etc.) without forcing a tagged-union schema in shared state.
110
+ */
111
+ export declare const WorkflowStateSchema: z.ZodObject<{
112
+ workflowName: z.ZodEnum<{
113
+ "draft-first": "draft-first";
114
+ "work-through-issues": "work-through-issues";
115
+ "pre-commit-review": "pre-commit-review";
116
+ }>;
117
+ currentStep: z.ZodString;
118
+ branchName: z.ZodOptional<z.ZodString>;
119
+ issueContext: z.ZodOptional<z.ZodObject<{
120
+ title: z.ZodString;
121
+ url: z.ZodString;
122
+ }, z.core.$strip>>;
123
+ completedSteps: z.ZodDefault<z.ZodArray<z.ZodString>>;
124
+ stepData: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
125
+ lastUpdatedAt: z.ZodString;
126
+ }, z.core.$strip>;
127
+ export type WorkflowState = z.infer<typeof WorkflowStateSchema>;
81
128
  export declare const ContributionGuidelinesSchema: z.ZodObject<{
82
129
  branchNamingConvention: z.ZodOptional<z.ZodString>;
83
130
  commitMessageFormat: z.ZodOptional<z.ZodString>;
@@ -243,6 +290,8 @@ export declare const AgentConfigSchema: z.ZodObject<{
243
290
  }>>;
244
291
  diffToolCustomCommand: z.ZodOptional<z.ZodString>;
245
292
  autoFormatBeforePush: z.ZodDefault<z.ZodBoolean>;
293
+ healthCheckFreshnessMinutes: z.ZodDefault<z.ZodNumber>;
294
+ reviewMaxPasses: z.ZodOptional<z.ZodNumber>;
246
295
  slmTriageModel: z.ZodDefault<z.ZodString>;
247
296
  slmTriageHost: z.ZodDefault<z.ZodString>;
248
297
  }, z.core.$strip>;
@@ -403,6 +452,8 @@ export declare const AgentStateSchema: z.ZodObject<{
403
452
  }>>;
404
453
  diffToolCustomCommand: z.ZodOptional<z.ZodString>;
405
454
  autoFormatBeforePush: z.ZodDefault<z.ZodBoolean>;
455
+ healthCheckFreshnessMinutes: z.ZodDefault<z.ZodNumber>;
456
+ reviewMaxPasses: z.ZodOptional<z.ZodNumber>;
406
457
  slmTriageModel: z.ZodDefault<z.ZodString>;
407
458
  slmTriageHost: z.ZodDefault<z.ZodString>;
408
459
  }, z.core.$strip>>;
@@ -532,6 +583,31 @@ export declare const AgentStateSchema: z.ZodObject<{
532
583
  notes: z.ZodArray<z.ZodString>;
533
584
  }, z.core.$strip>>;
534
585
  }, z.core.$strip>>>;
586
+ prFollowUpHistory: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
587
+ tier: z.ZodEnum<{
588
+ light_check_in: "light_check_in";
589
+ direct_check_in: "direct_check_in";
590
+ final_check_in: "final_check_in";
591
+ }>;
592
+ timestamp: z.ZodString;
593
+ draftPath: z.ZodOptional<z.ZodString>;
594
+ }, z.core.$strip>>>>;
595
+ workflowState: z.ZodOptional<z.ZodObject<{
596
+ workflowName: z.ZodEnum<{
597
+ "draft-first": "draft-first";
598
+ "work-through-issues": "work-through-issues";
599
+ "pre-commit-review": "pre-commit-review";
600
+ }>;
601
+ currentStep: z.ZodString;
602
+ branchName: z.ZodOptional<z.ZodString>;
603
+ issueContext: z.ZodOptional<z.ZodObject<{
604
+ title: z.ZodString;
605
+ url: z.ZodString;
606
+ }, z.core.$strip>>;
607
+ completedSteps: z.ZodDefault<z.ZodArray<z.ZodString>>;
608
+ stepData: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
609
+ lastUpdatedAt: z.ZodString;
610
+ }, z.core.$strip>>;
535
611
  }, z.core.$strip>;
536
612
  export type IssueStatus = z.infer<typeof IssueStatusSchema>;
537
613
  export type FetchedPRStatus = z.infer<typeof FetchedPRStatusSchema>;
@@ -67,6 +67,45 @@ export const AnalyzedIssueConversationSchema = z.object({
67
67
  repo: z.string(),
68
68
  analyzedAt: z.string(),
69
69
  });
70
+ /**
71
+ * One entry in a PR's follow-up history (#1277). Tier matches the
72
+ * cadence labels in `skills/pr-etiquette/SKILL.md` (light_check_in
73
+ * for 7-13 days, direct_check_in for 14-29 days, final_check_in for
74
+ * 30+ days). The `draftPath` is optional so an entry recorded from a
75
+ * direct `gh pr comment` (no draft) still validates.
76
+ */
77
+ export const FollowUpEntrySchema = z.object({
78
+ tier: z.enum(['light_check_in', 'direct_check_in', 'final_check_in']),
79
+ timestamp: z.string().datetime(),
80
+ draftPath: z.string().optional(),
81
+ });
82
+ /**
83
+ * Pause-point snapshot for resumable workflows (#1280). When the user
84
+ * picks "Done for now" inside `draft-first-workflow.md`,
85
+ * `work-through-issues.md`, or `pre-commit-review.md`, the workflow
86
+ * records its position here. The next `/oss` run consults this state
87
+ * and offers "Resume / Restart / Discard" instead of restarting from
88
+ * the beginning.
89
+ *
90
+ * `stepData` is intentionally typed as `Record<string, unknown>` so
91
+ * each workflow can persist whatever per-step context it needs to
92
+ * resume cleanly (compliance-gap skip list, last review pass count,
93
+ * etc.) without forcing a tagged-union schema in shared state.
94
+ */
95
+ export const WorkflowStateSchema = z.object({
96
+ workflowName: z.enum(['draft-first', 'work-through-issues', 'pre-commit-review']),
97
+ currentStep: z.string(),
98
+ branchName: z.string().optional(),
99
+ issueContext: z
100
+ .object({
101
+ title: z.string(),
102
+ url: z.string(),
103
+ })
104
+ .optional(),
105
+ completedSteps: z.array(z.string()).default([]),
106
+ stepData: z.record(z.string(), z.unknown()).default({}),
107
+ lastUpdatedAt: z.string().datetime(),
108
+ });
70
109
  // ── 3. Contribution schemas ──────────────────────────────────────────
71
110
  export const ContributionGuidelinesSchema = z.object({
72
111
  branchNamingConvention: z.string().optional(),
@@ -161,6 +200,25 @@ export const AgentConfigSchema = z.object({
161
200
  * the hook does nothing on every push unless the user explicitly enables it.
162
201
  */
163
202
  autoFormatBeforePush: z.boolean().default(false),
203
+ /**
204
+ * Threshold (in minutes) for the SessionStart PR health one-liner (#1255).
205
+ * The cached digest only refreshes when the user runs `/oss`; SessionStart
206
+ * fires every session. Without a freshness gate the line drifts arbitrarily
207
+ * stale between runs. When the cache is older than this many minutes (and
208
+ * not yet 7 days old, which keeps the existing catch-up nudge), the line is
209
+ * suppressed entirely. Default 30 minutes.
210
+ */
211
+ healthCheckFreshnessMinutes: z.number().int().positive().default(30),
212
+ /**
213
+ * Convergence cap for the multi-agent review loop in
214
+ * `workflows/dispatch-review.md` (#1275). When unset, the workflow
215
+ * falls back to per-mode defaults (5 for diff, 3 for plan). Lower
216
+ * values shorten the loop at the cost of skipping later iterations
217
+ * if findings persist; higher values give the loop more chances to
218
+ * converge before bailing. Optional — leave unset to use the
219
+ * defaults.
220
+ */
221
+ reviewMaxPasses: z.number().int().positive().optional(),
164
222
  /**
165
223
  * Optional Ollama model for SLM pre-triage during issue vetting (#1122).
166
224
  * Empty disables the feature. Recommended: `gemma4:e4b` (default for
@@ -236,4 +294,21 @@ export const AgentStateSchema = z.object({
236
294
  closedPRs: z.array(StoredClosedPRSchema).optional(),
237
295
  analyzedIssueConversations: z.array(AnalyzedIssueConversationSchema).optional(),
238
296
  activeIssues: z.array(TrackedIssueSchema).default([]),
297
+ /**
298
+ * Per-PR follow-up history (#1277). Keyed by PR URL. Each entry
299
+ * records a tier-bucketed follow-up that the user drafted (and
300
+ * presumably posted via `draft-review-post`). The dormant-pr
301
+ * workflow reads this before drafting to enforce the
302
+ * one-follow-up-per-timeframe rule documented in
303
+ * `skills/pr-etiquette/SKILL.md`.
304
+ */
305
+ prFollowUpHistory: z.record(z.string(), z.array(FollowUpEntrySchema)).optional(),
306
+ /**
307
+ * Pause-point snapshot for resumable workflows (#1280). Set when
308
+ * the user picks "Done for now" mid-workflow; cleared when the
309
+ * workflow completes or the user explicitly discards. The router
310
+ * reads this on every `/oss` invocation and offers Resume /
311
+ * Restart / Discard when present.
312
+ */
313
+ workflowState: WorkflowStateSchema.optional(),
239
314
  });
@@ -0,0 +1,75 @@
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
+ import type { AgentState } from './state-schema.js';
17
+ export type ContributorStyle = 'maintainer' | 'explorer' | 'specialist' | 'generalist';
18
+ export type TrajectoryDirection = 'growing' | 'steady' | 'declining';
19
+ export type CapacityAction = 'open_more' | 'follow_up_dormant' | 'wait_on_maintainers' | null;
20
+ export interface StrategyProfile {
21
+ style: ContributorStyle;
22
+ totalPRs: number;
23
+ mergedCount: number;
24
+ /** Merge rate as a 0-1 fraction. `0` when no PRs are tracked. */
25
+ mergeRate: number;
26
+ /** Top languages by repo PR count, descending. Empty when language data
27
+ * is not available in repoScores. */
28
+ primaryLanguages: string[];
29
+ /** Top repos by merged PR count, descending. */
30
+ favoriteRepos: string[];
31
+ }
32
+ export interface StrategyCapacity {
33
+ openPRCount: number;
34
+ dormantPRCount: number;
35
+ /** True when dormant PRs span >=2 distinct repos (a signal that the
36
+ * contributor is awaiting reviews from multiple maintainers, not just
37
+ * one slow project). */
38
+ overExtended: boolean;
39
+ /** The single highest-priority next action — null when state is too
40
+ * thin to recommend anything. */
41
+ suggestedAction: CapacityAction;
42
+ }
43
+ export interface StrategyPatterns {
44
+ prTypeDistribution: {
45
+ docs: number;
46
+ fixes: number;
47
+ features: number;
48
+ refactors: number;
49
+ tests: number;
50
+ other: number;
51
+ };
52
+ trajectoryDirection: TrajectoryDirection;
53
+ averagePRSize: number;
54
+ }
55
+ export interface StrategyRecommendations {
56
+ languages: string[];
57
+ repos: string[];
58
+ issueTypes: string[];
59
+ avoidPatterns: string[];
60
+ }
61
+ export interface StrategyResult {
62
+ profile: StrategyProfile;
63
+ capacity: StrategyCapacity;
64
+ patterns: StrategyPatterns;
65
+ recommendations: StrategyRecommendations;
66
+ }
67
+ /** Minimum tracked PR history before strategy output is meaningful (#1243). */
68
+ export declare const STRATEGY_MIN_PRS = 10;
69
+ /**
70
+ * Compute the deterministic strategy signal from agent state. Returns
71
+ * null when state is thinner than {@link STRATEGY_MIN_PRS} merged PRs —
72
+ * the auto-display surface uses this null sentinel as the
73
+ * minimum-data gate (#1243).
74
+ */
75
+ export declare function computeStrategy(state: AgentState): StrategyResult | null;