@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.
- package/dist/cli-registry.js +99 -0
- package/dist/cli.bundle.cjs +112 -105
- package/dist/commands/compliance-score.d.ts +21 -0
- package/dist/commands/compliance-score.js +156 -0
- package/dist/commands/daily.d.ts +8 -0
- package/dist/commands/daily.js +21 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/list-mark-done.d.ts +48 -0
- package/dist/commands/list-mark-done.js +213 -0
- package/dist/commands/parse-list.js +86 -9
- package/dist/commands/repo-vet.d.ts +21 -0
- package/dist/commands/repo-vet.js +215 -0
- package/dist/commands/startup.js +41 -1
- package/dist/core/anti-llm-policy.d.ts +42 -13
- package/dist/core/anti-llm-policy.js +102 -13
- package/dist/core/ci-analysis.d.ts +32 -1
- package/dist/core/ci-analysis.js +92 -0
- package/dist/core/ci-enforced-tools.d.ts +35 -0
- package/dist/core/ci-enforced-tools.js +109 -0
- package/dist/core/comment-decision.d.ts +72 -0
- package/dist/core/comment-decision.js +74 -0
- package/dist/core/compliance-score.d.ts +127 -0
- package/dist/core/compliance-score.js +277 -0
- package/dist/core/config-registry.js +12 -0
- package/dist/core/contributing.d.ts +52 -0
- package/dist/core/contributing.js +139 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.js +54 -0
- package/dist/core/extraction-categories.d.ts +55 -0
- package/dist/core/extraction-categories.js +108 -0
- package/dist/core/follow-up-history.d.ts +41 -0
- package/dist/core/follow-up-history.js +71 -0
- package/dist/core/gist-state-store.d.ts +30 -7
- package/dist/core/gist-state-store.js +87 -11
- package/dist/core/issue-conversation.js +1 -0
- package/dist/core/issue-effort.d.ts +29 -0
- package/dist/core/issue-effort.js +41 -0
- package/dist/core/maintainer-hints.d.ts +23 -0
- package/dist/core/maintainer-hints.js +36 -0
- package/dist/core/pr-monitor.d.ts +1 -1
- package/dist/core/pr-monitor.js +31 -11
- package/dist/core/pr-quality-rubric.d.ts +70 -0
- package/dist/core/pr-quality-rubric.js +121 -0
- package/dist/core/repo-vet.d.ts +90 -0
- package/dist/core/repo-vet.js +178 -0
- package/dist/core/state-schema.d.ts +77 -0
- package/dist/core/state-schema.js +84 -0
- package/dist/core/state.d.ts +7 -0
- package/dist/core/state.js +10 -0
- package/dist/core/strategy.d.ts +95 -0
- package/dist/core/strategy.js +270 -0
- package/dist/core/types.d.ts +51 -0
- package/dist/core/workflow-state.d.ts +56 -0
- package/dist/core/workflow-state.js +101 -0
- package/dist/formatters/json.d.ts +252 -0
- package/dist/formatters/json.js +153 -0
- package/package.json +1 -1
package/dist/core/pr-monitor.js
CHANGED
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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>;
|