@oss-autopilot/core 3.4.1 → 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.
Files changed (58) hide show
  1. package/dist/cli-registry.js +99 -0
  2. package/dist/cli.bundle.cjs +112 -105
  3. package/dist/commands/compliance-score.d.ts +21 -0
  4. package/dist/commands/compliance-score.js +156 -0
  5. package/dist/commands/daily.d.ts +8 -0
  6. package/dist/commands/daily.js +21 -0
  7. package/dist/commands/index.d.ts +6 -0
  8. package/dist/commands/index.js +6 -0
  9. package/dist/commands/list-mark-done.d.ts +48 -0
  10. package/dist/commands/list-mark-done.js +213 -0
  11. package/dist/commands/parse-list.js +86 -9
  12. package/dist/commands/repo-vet.d.ts +21 -0
  13. package/dist/commands/repo-vet.js +215 -0
  14. package/dist/commands/startup.js +41 -1
  15. package/dist/core/anti-llm-policy.d.ts +42 -13
  16. package/dist/core/anti-llm-policy.js +102 -13
  17. package/dist/core/ci-analysis.d.ts +32 -1
  18. package/dist/core/ci-analysis.js +92 -0
  19. package/dist/core/ci-enforced-tools.d.ts +35 -0
  20. package/dist/core/ci-enforced-tools.js +109 -0
  21. package/dist/core/comment-decision.d.ts +72 -0
  22. package/dist/core/comment-decision.js +74 -0
  23. package/dist/core/compliance-score.d.ts +127 -0
  24. package/dist/core/compliance-score.js +277 -0
  25. package/dist/core/config-registry.js +12 -0
  26. package/dist/core/contributing.d.ts +52 -0
  27. package/dist/core/contributing.js +139 -0
  28. package/dist/core/errors.d.ts +19 -0
  29. package/dist/core/errors.js +54 -0
  30. package/dist/core/extraction-categories.d.ts +55 -0
  31. package/dist/core/extraction-categories.js +108 -0
  32. package/dist/core/follow-up-history.d.ts +41 -0
  33. package/dist/core/follow-up-history.js +71 -0
  34. package/dist/core/gist-state-store.d.ts +30 -7
  35. package/dist/core/gist-state-store.js +87 -11
  36. package/dist/core/issue-conversation.js +1 -0
  37. package/dist/core/issue-effort.d.ts +29 -0
  38. package/dist/core/issue-effort.js +41 -0
  39. package/dist/core/maintainer-hints.d.ts +23 -0
  40. package/dist/core/maintainer-hints.js +36 -0
  41. package/dist/core/pr-monitor.d.ts +1 -1
  42. package/dist/core/pr-monitor.js +31 -11
  43. package/dist/core/pr-quality-rubric.d.ts +70 -0
  44. package/dist/core/pr-quality-rubric.js +121 -0
  45. package/dist/core/repo-vet.d.ts +90 -0
  46. package/dist/core/repo-vet.js +178 -0
  47. package/dist/core/state-schema.d.ts +77 -0
  48. package/dist/core/state-schema.js +84 -0
  49. package/dist/core/state.d.ts +7 -0
  50. package/dist/core/state.js +10 -0
  51. package/dist/core/strategy.d.ts +95 -0
  52. package/dist/core/strategy.js +270 -0
  53. package/dist/core/types.d.ts +51 -0
  54. package/dist/core/workflow-state.d.ts +56 -0
  55. package/dist/core/workflow-state.js +101 -0
  56. package/dist/formatters/json.d.ts +252 -0
  57. package/dist/formatters/json.js +153 -0
  58. package/package.json +1 -1
@@ -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,
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Canonical PR quality rubric (#1252).
3
+ *
4
+ * Single source of truth for the PR-quality checks shared between
5
+ * `skills/pr-etiquette/SKILL.md` (human-facing checklist) and
6
+ * `agents/pr-compliance-checker.md` (agent-facing scoring via
7
+ * `computeComplianceScore` in compliance-score.ts).
8
+ *
9
+ * Both surfaces had the same checks listed independently, with the
10
+ * "< 10 files, < 400 lines ideal" threshold duplicated. Drift was
11
+ * inevitable as either surface evolved. This module exports the
12
+ * rubric as structured data so the two consumers cannot drift
13
+ * silently — `compliance-score.ts` re-exports its WEIGHTS /
14
+ * FOCUSED_CHANGES from here, and the skill prose references this
15
+ * file as the canonical definition.
16
+ *
17
+ * Same architectural shape as the typed-core extractions in
18
+ * #858 / #910 / #911 / #1245 / #1242 / #1243 — pull the rubric out
19
+ * of prose into typed code so it's testable and tunable.
20
+ */
21
+ export type PRQualityTier = 'required' | 'conditional' | 'optional';
22
+ export interface PRQualityCheck {
23
+ /** Stable identifier referenced by the agent's scoring code. */
24
+ id: 'issueReference' | 'description' | 'title' | 'focusedChanges' | 'minimalDiff' | 'tests' | 'docs' | 'branch' | 'screenshots';
25
+ /** Human-facing label as it appears in the skill checklist. */
26
+ label: string;
27
+ /** One-line description for both the skill checklist and the
28
+ * agent's recommendation prose. */
29
+ description: string;
30
+ tier: PRQualityTier;
31
+ /**
32
+ * Percent weight in the agent's compliance score, when the check
33
+ * is part of the scored set. `null` for checks the skill lists but
34
+ * the agent doesn't currently score (Minimal Diff, Docs, Screenshots).
35
+ */
36
+ weight: number | null;
37
+ }
38
+ /**
39
+ * Canonical "focused changes" thresholds. Mirrored by
40
+ * compliance-score.ts's `FOCUSED_CHANGES` constant — that file
41
+ * imports these values.
42
+ */
43
+ export declare const FOCUSED_CHANGES_THRESHOLDS: {
44
+ readonly passFiles: 10;
45
+ readonly passLines: 400;
46
+ readonly warnFiles: 20;
47
+ readonly warnLines: 800;
48
+ };
49
+ /**
50
+ * Title byte budget. Mirrored by compliance-score.ts's
51
+ * `TITLE_LENGTH_BUDGET` — that file imports this value.
52
+ */
53
+ export declare const TITLE_LENGTH_BUDGET = 72;
54
+ /**
55
+ * The full rubric. The order is the order the skill renders the
56
+ * checklist in; the weights total 100 across the scored entries.
57
+ */
58
+ export declare const PR_QUALITY_RUBRIC: readonly PRQualityCheck[];
59
+ /**
60
+ * Look up a check by id. Used by `compliance-score.ts` to wire the
61
+ * scored checks to their canonical weight without hardcoding the
62
+ * value in two places.
63
+ */
64
+ export declare function getRubricCheck(id: PRQualityCheck['id']): PRQualityCheck | undefined;
65
+ /**
66
+ * Sum of the weights for scored checks. Should equal 100. Tests pin
67
+ * this so a future edit that fails to balance the weights surfaces
68
+ * at CI time rather than silently shipping a < 100 rubric.
69
+ */
70
+ export declare function totalScoredWeight(): number;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Canonical PR quality rubric (#1252).
3
+ *
4
+ * Single source of truth for the PR-quality checks shared between
5
+ * `skills/pr-etiquette/SKILL.md` (human-facing checklist) and
6
+ * `agents/pr-compliance-checker.md` (agent-facing scoring via
7
+ * `computeComplianceScore` in compliance-score.ts).
8
+ *
9
+ * Both surfaces had the same checks listed independently, with the
10
+ * "< 10 files, < 400 lines ideal" threshold duplicated. Drift was
11
+ * inevitable as either surface evolved. This module exports the
12
+ * rubric as structured data so the two consumers cannot drift
13
+ * silently — `compliance-score.ts` re-exports its WEIGHTS /
14
+ * FOCUSED_CHANGES from here, and the skill prose references this
15
+ * file as the canonical definition.
16
+ *
17
+ * Same architectural shape as the typed-core extractions in
18
+ * #858 / #910 / #911 / #1245 / #1242 / #1243 — pull the rubric out
19
+ * of prose into typed code so it's testable and tunable.
20
+ */
21
+ /**
22
+ * Canonical "focused changes" thresholds. Mirrored by
23
+ * compliance-score.ts's `FOCUSED_CHANGES` constant — that file
24
+ * imports these values.
25
+ */
26
+ export const FOCUSED_CHANGES_THRESHOLDS = {
27
+ passFiles: 10,
28
+ passLines: 400,
29
+ warnFiles: 20,
30
+ warnLines: 800,
31
+ };
32
+ /**
33
+ * Title byte budget. Mirrored by compliance-score.ts's
34
+ * `TITLE_LENGTH_BUDGET` — that file imports this value.
35
+ */
36
+ export const TITLE_LENGTH_BUDGET = 72;
37
+ /**
38
+ * The full rubric. The order is the order the skill renders the
39
+ * checklist in; the weights total 100 across the scored entries.
40
+ */
41
+ export const PR_QUALITY_RUBRIC = [
42
+ {
43
+ id: 'issueReference',
44
+ label: 'Issue Reference',
45
+ description: 'PR links to issue (`Closes #X` or `Fixes #X`)',
46
+ tier: 'required',
47
+ weight: 25,
48
+ },
49
+ {
50
+ id: 'description',
51
+ label: 'Description Quality',
52
+ description: 'Explains what changed and why',
53
+ tier: 'required',
54
+ weight: 25,
55
+ },
56
+ {
57
+ id: 'title',
58
+ label: 'Title Quality',
59
+ description: 'Descriptive, properly formatted (e.g., `fix: resolve login timeout`)',
60
+ tier: 'required',
61
+ weight: 10,
62
+ },
63
+ {
64
+ id: 'focusedChanges',
65
+ label: 'Focused Changes',
66
+ description: `One logical change per PR (< ${FOCUSED_CHANGES_THRESHOLDS.passFiles} files, < ${FOCUSED_CHANGES_THRESHOLDS.passLines} lines ideal)`,
67
+ tier: 'required',
68
+ weight: 20,
69
+ },
70
+ {
71
+ id: 'minimalDiff',
72
+ label: 'Minimal Diff',
73
+ description: 'No unrelated formatting changes (whitespace, quotes, imports, trailing commas)',
74
+ tier: 'required',
75
+ weight: null,
76
+ },
77
+ {
78
+ id: 'tests',
79
+ label: 'Tests Included',
80
+ description: 'If project requires tests, add them',
81
+ tier: 'conditional',
82
+ weight: 15,
83
+ },
84
+ {
85
+ id: 'docs',
86
+ label: 'Docs Updated',
87
+ description: 'If behavior changed, update docs',
88
+ tier: 'conditional',
89
+ weight: null,
90
+ },
91
+ {
92
+ id: 'branch',
93
+ label: 'Branch Naming',
94
+ description: 'Follows convention (`feature/`, `fix/`, `docs/`)',
95
+ tier: 'optional',
96
+ weight: 5,
97
+ },
98
+ {
99
+ id: 'screenshots',
100
+ label: 'Screenshots',
101
+ description: 'Included for UI changes',
102
+ tier: 'optional',
103
+ weight: null,
104
+ },
105
+ ];
106
+ /**
107
+ * Look up a check by id. Used by `compliance-score.ts` to wire the
108
+ * scored checks to their canonical weight without hardcoding the
109
+ * value in two places.
110
+ */
111
+ export function getRubricCheck(id) {
112
+ return PR_QUALITY_RUBRIC.find((c) => c.id === id);
113
+ }
114
+ /**
115
+ * Sum of the weights for scored checks. Should equal 100. Tests pin
116
+ * this so a future edit that fails to balance the weights surfaces
117
+ * at CI time rather than silently shipping a < 100 rubric.
118
+ */
119
+ export function totalScoredWeight() {
120
+ return PR_QUALITY_RUBRIC.reduce((sum, c) => sum + (c.weight ?? 0), 0);
121
+ }
@@ -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,11 +452,14 @@ 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>>;
409
460
  lastRunAt: z.ZodDefault<z.ZodString>;
410
461
  lastDigestAt: z.ZodOptional<z.ZodString>;
462
+ lastStrategyAt: z.ZodOptional<z.ZodString>;
411
463
  lastDigest: z.ZodOptional<z.ZodObject<{
412
464
  generatedAt: z.ZodString;
413
465
  openPRs: z.ZodArray<z.ZodAny>;
@@ -532,6 +584,31 @@ export declare const AgentStateSchema: z.ZodObject<{
532
584
  notes: z.ZodArray<z.ZodString>;
533
585
  }, z.core.$strip>>;
534
586
  }, z.core.$strip>>>;
587
+ prFollowUpHistory: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
588
+ tier: z.ZodEnum<{
589
+ light_check_in: "light_check_in";
590
+ direct_check_in: "direct_check_in";
591
+ final_check_in: "final_check_in";
592
+ }>;
593
+ timestamp: z.ZodString;
594
+ draftPath: z.ZodOptional<z.ZodString>;
595
+ }, z.core.$strip>>>>;
596
+ workflowState: z.ZodOptional<z.ZodObject<{
597
+ workflowName: z.ZodEnum<{
598
+ "draft-first": "draft-first";
599
+ "work-through-issues": "work-through-issues";
600
+ "pre-commit-review": "pre-commit-review";
601
+ }>;
602
+ currentStep: z.ZodString;
603
+ branchName: z.ZodOptional<z.ZodString>;
604
+ issueContext: z.ZodOptional<z.ZodObject<{
605
+ title: z.ZodString;
606
+ url: z.ZodString;
607
+ }, z.core.$strip>>;
608
+ completedSteps: z.ZodDefault<z.ZodArray<z.ZodString>>;
609
+ stepData: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
610
+ lastUpdatedAt: z.ZodString;
611
+ }, z.core.$strip>>;
535
612
  }, z.core.$strip>;
536
613
  export type IssueStatus = z.infer<typeof IssueStatusSchema>;
537
614
  export type FetchedPRStatus = z.infer<typeof FetchedPRStatusSchema>;