@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repo-vet command (#1271, follow-up to #1242).
|
|
3
|
+
*
|
|
4
|
+
* Fetches repo health signals via the GitHub API, runs the typed
|
|
5
|
+
* `computeRepoVet` core function, and returns the structured result.
|
|
6
|
+
* Used by the `repo-evaluator` agent (and the future `repo-vet` MCP tool)
|
|
7
|
+
* to replace per-prompt rubric assembly with a deterministic, testable
|
|
8
|
+
* evaluation.
|
|
9
|
+
*
|
|
10
|
+
* Same architectural shape as `compliance-score`: read-only API calls,
|
|
11
|
+
* no state mutation, runs against a public `owner/repo` slug.
|
|
12
|
+
*/
|
|
13
|
+
import type { RepoVetOutput } from '../formatters/json.js';
|
|
14
|
+
/**
|
|
15
|
+
* Run the repo-vet evaluation against an `owner/repo` slug.
|
|
16
|
+
*
|
|
17
|
+
* @throws {ValidationError} If the repo identifier is malformed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runRepoVet(options: {
|
|
20
|
+
repo: string;
|
|
21
|
+
}): Promise<RepoVetOutput>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repo-vet command (#1271, follow-up to #1242).
|
|
3
|
+
*
|
|
4
|
+
* Fetches repo health signals via the GitHub API, runs the typed
|
|
5
|
+
* `computeRepoVet` core function, and returns the structured result.
|
|
6
|
+
* Used by the `repo-evaluator` agent (and the future `repo-vet` MCP tool)
|
|
7
|
+
* to replace per-prompt rubric assembly with a deterministic, testable
|
|
8
|
+
* evaluation.
|
|
9
|
+
*
|
|
10
|
+
* Same architectural shape as `compliance-score`: read-only API calls,
|
|
11
|
+
* no state mutation, runs against a public `owner/repo` slug.
|
|
12
|
+
*/
|
|
13
|
+
import { getOctokit, requireGitHubToken } from '../core/index.js';
|
|
14
|
+
import { errorMessage } from '../core/errors.js';
|
|
15
|
+
import { warn } from '../core/logger.js';
|
|
16
|
+
import { validateRepoIdentifier } from './validation.js';
|
|
17
|
+
import { computeRepoVet } from '../core/repo-vet.js';
|
|
18
|
+
const MODULE = 'repo-vet';
|
|
19
|
+
const DAY_MS = 86400000;
|
|
20
|
+
const COMMUNITY_HEALTH_PATHS = [
|
|
21
|
+
// Each entry can live at the repo root OR under .github/. We probe both
|
|
22
|
+
// because some repos prefer the visible-at-root convention and some
|
|
23
|
+
// prefer the .github/ folder convention.
|
|
24
|
+
{ key: 'hasContributing', candidates: ['CONTRIBUTING.md', '.github/CONTRIBUTING.md'] },
|
|
25
|
+
{ key: 'hasIssueTemplates', candidates: ['.github/ISSUE_TEMPLATE'] },
|
|
26
|
+
{
|
|
27
|
+
key: 'hasPRTemplate',
|
|
28
|
+
candidates: ['.github/pull_request_template.md', 'pull_request_template.md', 'PULL_REQUEST_TEMPLATE.md'],
|
|
29
|
+
},
|
|
30
|
+
{ key: 'hasCodeOfConduct', candidates: ['CODE_OF_CONDUCT.md', '.github/CODE_OF_CONDUCT.md'] },
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Returns true when the path exists. Treats 404 as "absent" (the only
|
|
34
|
+
* shape we care about for community-health flags). Re-throws everything
|
|
35
|
+
* else — auth/rate-limit failures must not silently scrub the
|
|
36
|
+
* `hasContributing` etc. flags to `false`, which would understate a
|
|
37
|
+
* repo's community health on a token that lacks scope or has been
|
|
38
|
+
* throttled mid-sequence.
|
|
39
|
+
*/
|
|
40
|
+
async function probePath(octokit, owner, repo, path) {
|
|
41
|
+
try {
|
|
42
|
+
await octokit.repos.getContent({ owner, repo, path });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const status = err.status;
|
|
47
|
+
if (status === 404)
|
|
48
|
+
return false;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Probe each community-health path. Treats 404 as "absent" (a definitive
|
|
54
|
+
* signal). On any other error (auth, rate-limit, 5xx) the per-key result
|
|
55
|
+
* stays `false` AND the whole-function `incomplete` flag is set so the
|
|
56
|
+
* caller knows community-health was unverified rather than confirmed-absent.
|
|
57
|
+
*
|
|
58
|
+
* This is the per-key analogue of the call-level catch: instead of either
|
|
59
|
+
* silently misclassifying 403 → absent OR aborting the entire repo-vet
|
|
60
|
+
* call, we degrade gracefully and surface the gap.
|
|
61
|
+
*/
|
|
62
|
+
async function checkCommunityHealth(octokit, owner, repo) {
|
|
63
|
+
const out = {
|
|
64
|
+
hasContributing: false,
|
|
65
|
+
hasIssueTemplates: false,
|
|
66
|
+
hasPRTemplate: false,
|
|
67
|
+
hasCodeOfConduct: false,
|
|
68
|
+
};
|
|
69
|
+
let incomplete = false;
|
|
70
|
+
await Promise.all(COMMUNITY_HEALTH_PATHS.map(async ({ key, candidates }) => {
|
|
71
|
+
let sawError = false;
|
|
72
|
+
for (const path of candidates) {
|
|
73
|
+
try {
|
|
74
|
+
if (await probePath(octokit, owner, repo, path)) {
|
|
75
|
+
out[key] = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// probePath only throws on non-404 errors. Don't abandon
|
|
81
|
+
// remaining candidates — `CONTRIBUTING.md` failing with 403
|
|
82
|
+
// does NOT imply `.github/CONTRIBUTING.md` will fail too. Mark
|
|
83
|
+
// the per-key probe as incomplete only if every candidate
|
|
84
|
+
// either errored or returned absent.
|
|
85
|
+
warn(MODULE, `community-health probe for ${owner}/${repo}/${path} failed: ${errorMessage(err)}`);
|
|
86
|
+
sawError = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Reached only when no candidate hit. If any candidate errored,
|
|
91
|
+
// the absent flag is unreliable — surface that to the caller so
|
|
92
|
+
// the agent prompt distinguishes "probed all candidates and
|
|
93
|
+
// absent" from "couldn't tell".
|
|
94
|
+
if (sawError)
|
|
95
|
+
incomplete = true;
|
|
96
|
+
}));
|
|
97
|
+
return {
|
|
98
|
+
hasContributing: out.hasContributing,
|
|
99
|
+
hasIssueTemplates: out.hasIssueTemplates,
|
|
100
|
+
hasPRTemplate: out.hasPRTemplate,
|
|
101
|
+
hasCodeOfConduct: out.hasCodeOfConduct,
|
|
102
|
+
incomplete,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function summarizePRMerges(prs, windowDays, now) {
|
|
106
|
+
const cutoff = now.getTime() - windowDays * DAY_MS;
|
|
107
|
+
const prMergeTimesDays = [];
|
|
108
|
+
let mergedCount = 0;
|
|
109
|
+
let openedCount = 0;
|
|
110
|
+
for (const pr of prs) {
|
|
111
|
+
const createdAt = new Date(pr.created_at).getTime();
|
|
112
|
+
if (createdAt >= cutoff)
|
|
113
|
+
openedCount += 1;
|
|
114
|
+
if (pr.merged_at) {
|
|
115
|
+
const mergedAt = new Date(pr.merged_at).getTime();
|
|
116
|
+
if (mergedAt >= cutoff) {
|
|
117
|
+
mergedCount += 1;
|
|
118
|
+
const days = (mergedAt - createdAt) / DAY_MS;
|
|
119
|
+
if (Number.isFinite(days) && days >= 0)
|
|
120
|
+
prMergeTimesDays.push(days);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { prMergeTimesDays, mergedCount, openedCount };
|
|
125
|
+
}
|
|
126
|
+
function summarizeCommits(commits, now) {
|
|
127
|
+
const cutoff30 = now.getTime() - 30 * DAY_MS;
|
|
128
|
+
const cutoff90 = now.getTime() - 90 * DAY_MS;
|
|
129
|
+
const contributors90 = new Set();
|
|
130
|
+
let commitsLast30Days = 0;
|
|
131
|
+
let lastCommitMs = null;
|
|
132
|
+
for (const c of commits) {
|
|
133
|
+
const dateStr = c.commit.author?.date;
|
|
134
|
+
if (!dateStr)
|
|
135
|
+
continue;
|
|
136
|
+
const ts = new Date(dateStr).getTime();
|
|
137
|
+
if (Number.isNaN(ts))
|
|
138
|
+
continue;
|
|
139
|
+
if (lastCommitMs === null || ts > lastCommitMs)
|
|
140
|
+
lastCommitMs = ts;
|
|
141
|
+
if (ts >= cutoff30)
|
|
142
|
+
commitsLast30Days += 1;
|
|
143
|
+
if (ts >= cutoff90 && c.author?.login)
|
|
144
|
+
contributors90.add(c.author.login);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
commitsLast30Days,
|
|
148
|
+
contributorsLast90d: contributors90.size,
|
|
149
|
+
lastCommitISO: lastCommitMs ? new Date(lastCommitMs).toISOString() : null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Run the repo-vet evaluation against an `owner/repo` slug.
|
|
154
|
+
*
|
|
155
|
+
* @throws {ValidationError} If the repo identifier is malformed.
|
|
156
|
+
*/
|
|
157
|
+
export async function runRepoVet(options) {
|
|
158
|
+
validateRepoIdentifier(options.repo);
|
|
159
|
+
const [owner, repo] = options.repo.split('/');
|
|
160
|
+
const token = requireGitHubToken();
|
|
161
|
+
const octokit = getOctokit(token);
|
|
162
|
+
const now = new Date();
|
|
163
|
+
const [repoMetaResp, closedPRsResp, commitsResp, releasesResp, communityHealthSummary] = await Promise.all([
|
|
164
|
+
octokit.repos.get({ owner, repo }),
|
|
165
|
+
octokit.pulls.list({ owner, repo, state: 'closed', sort: 'updated', direction: 'desc', per_page: 100 }),
|
|
166
|
+
octokit.repos.listCommits({ owner, repo, per_page: 100 }),
|
|
167
|
+
octokit.repos
|
|
168
|
+
.listReleases({ owner, repo, per_page: 1 })
|
|
169
|
+
.catch(() => ({ data: [] })),
|
|
170
|
+
checkCommunityHealth(octokit, owner, repo),
|
|
171
|
+
]);
|
|
172
|
+
const prs = closedPRsResp.data.map((p) => ({
|
|
173
|
+
created_at: p.created_at,
|
|
174
|
+
merged_at: p.merged_at,
|
|
175
|
+
updated_at: p.updated_at,
|
|
176
|
+
}));
|
|
177
|
+
const { prMergeTimesDays, mergedCount, openedCount } = summarizePRMerges(prs, 90, now);
|
|
178
|
+
const commitSummary = summarizeCommits(commitsResp.data, now);
|
|
179
|
+
const releases = releasesResp.data;
|
|
180
|
+
const lastReleaseISO = releases.length > 0 ? (releases[0].published_at ?? releases[0].created_at ?? null) : null;
|
|
181
|
+
const input = {
|
|
182
|
+
stars: repoMetaResp.data.stargazers_count ?? 0,
|
|
183
|
+
forks: repoMetaResp.data.forks_count ?? 0,
|
|
184
|
+
openIssues: repoMetaResp.data.open_issues_count ?? 0,
|
|
185
|
+
watchers: repoMetaResp.data.subscribers_count ?? 0,
|
|
186
|
+
isArchived: repoMetaResp.data.archived ?? false,
|
|
187
|
+
lastPushed: repoMetaResp.data.pushed_at ?? new Date(0).toISOString(),
|
|
188
|
+
createdAt: repoMetaResp.data.created_at ?? new Date(0).toISOString(),
|
|
189
|
+
commitsLast30Days: commitSummary.commitsLast30Days,
|
|
190
|
+
prMergeTimesDays,
|
|
191
|
+
mergedCount90Days: mergedCount,
|
|
192
|
+
openedCount90Days: openedCount,
|
|
193
|
+
lastCommitISO: commitSummary.lastCommitISO,
|
|
194
|
+
contributorsLast90d: commitSummary.contributorsLast90d,
|
|
195
|
+
lastReleaseISO,
|
|
196
|
+
hasContributing: communityHealthSummary.hasContributing,
|
|
197
|
+
hasIssueTemplates: communityHealthSummary.hasIssueTemplates,
|
|
198
|
+
hasPRTemplate: communityHealthSummary.hasPRTemplate,
|
|
199
|
+
hasCodeOfConduct: communityHealthSummary.hasCodeOfConduct,
|
|
200
|
+
};
|
|
201
|
+
const result = computeRepoVet(input);
|
|
202
|
+
// The core function names its metadata object `repo`. Rename to `repoMeta`
|
|
203
|
+
// at the CLI boundary so the top-level slug doesn't collide with it.
|
|
204
|
+
// Also overlay the community-health `incomplete` flag the wrapper
|
|
205
|
+
// tracks (the core type doesn't carry it because computeRepoVet is
|
|
206
|
+
// pure — only the wrapper makes the API calls that can fail mid-probe).
|
|
207
|
+
const { repo: repoMeta, communityHealth, ...rest } = result;
|
|
208
|
+
return {
|
|
209
|
+
repoSlug: options.repo,
|
|
210
|
+
fetchedAt: now.toISOString(),
|
|
211
|
+
repoMeta,
|
|
212
|
+
communityHealth: { ...communityHealth, incomplete: communityHealthSummary.incomplete },
|
|
213
|
+
...rest,
|
|
214
|
+
};
|
|
215
|
+
}
|
package/dist/commands/startup.js
CHANGED
|
@@ -115,11 +115,33 @@ export function detectIssueList() {
|
|
|
115
115
|
// Matches the sibling warn at line 64 for `issueListPath` read failures.
|
|
116
116
|
warn('startup', `Could not read skippedIssuesPath from state: ${errorMessage(err)}`);
|
|
117
117
|
}
|
|
118
|
-
// Probe default path: same directory as issue list, named skipped-issues.md
|
|
118
|
+
// Probe default path: same directory as issue list, named skipped-issues.md.
|
|
119
|
+
// When found, also persist to state.config so downstream commands
|
|
120
|
+
// (`skip-add`, `scout search`'s skip-list filter) read the same path
|
|
121
|
+
// instead of silently no-opping with "No skipped-issues path configured"
|
|
122
|
+
// (#1330). Without persistence, the auto-detect printed the path on
|
|
123
|
+
// every startup but nothing else honored it — search would re-surface
|
|
124
|
+
// already-skipped candidates round after round.
|
|
119
125
|
if (!skippedIssuesPath && issueListPath) {
|
|
120
126
|
const defaultSkipPath = path.join(path.dirname(issueListPath), 'skipped-issues.md');
|
|
121
127
|
if (fs.existsSync(defaultSkipPath)) {
|
|
122
128
|
skippedIssuesPath = defaultSkipPath;
|
|
129
|
+
try {
|
|
130
|
+
const stateManager = getStateManager();
|
|
131
|
+
// Only write when config actually doesn't have one — re-running
|
|
132
|
+
// startup shouldn't trigger an autoSave on every run if the
|
|
133
|
+
// value is already there.
|
|
134
|
+
const current = stateManager.getState().config.skippedIssuesPath;
|
|
135
|
+
if (!current) {
|
|
136
|
+
stateManager.updateConfig({ skippedIssuesPath: defaultSkipPath });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
// Persistence is best-effort — startup still surfaces the path
|
|
141
|
+
// in its return value so the current run benefits, but the next
|
|
142
|
+
// run won't. Log so the failure is debuggable.
|
|
143
|
+
warn('startup', `Could not persist auto-detected skippedIssuesPath: ${errorMessage(err)}`);
|
|
144
|
+
}
|
|
123
145
|
}
|
|
124
146
|
}
|
|
125
147
|
return { path: issueListPath, source, availableCount, completedCount, skippedIssuesPath };
|
|
@@ -277,6 +299,22 @@ export async function runStartup() {
|
|
|
277
299
|
}
|
|
278
300
|
// 5. Detect issue list
|
|
279
301
|
const issueList = detectIssueList();
|
|
302
|
+
// 6. Pass through dashboard-build status set by the workflow shell (#1293).
|
|
303
|
+
// The workflow runs the SPA build BEFORE invoking startup, so the status
|
|
304
|
+
// arrives here via env vars rather than being something runStartup itself
|
|
305
|
+
// computes. Validate the env value so a malformed export can't sneak an
|
|
306
|
+
// arbitrary string into a typed enum.
|
|
307
|
+
const rawBuildStatus = process.env.OSS_DASHBOARD_BUILD_STATUS;
|
|
308
|
+
const dashboardBuildStatus = rawBuildStatus === 'fresh' ||
|
|
309
|
+
rawBuildStatus === 'rebuilt' ||
|
|
310
|
+
rawBuildStatus === 'failed' ||
|
|
311
|
+
rawBuildStatus === 'missing-pnpm'
|
|
312
|
+
? rawBuildStatus
|
|
313
|
+
: undefined;
|
|
314
|
+
const rawBuildErrorTail = process.env.OSS_DASHBOARD_BUILD_ERROR_TAIL;
|
|
315
|
+
const dashboardBuildErrorTail = (dashboardBuildStatus === 'failed' || dashboardBuildStatus === 'missing-pnpm') && rawBuildErrorTail
|
|
316
|
+
? rawBuildErrorTail
|
|
317
|
+
: undefined;
|
|
280
318
|
return {
|
|
281
319
|
version,
|
|
282
320
|
setupComplete: true,
|
|
@@ -284,6 +322,8 @@ export async function runStartup() {
|
|
|
284
322
|
daily,
|
|
285
323
|
dashboardUrl,
|
|
286
324
|
dashboardError,
|
|
325
|
+
dashboardBuildStatus,
|
|
326
|
+
dashboardBuildErrorTail,
|
|
287
327
|
issueList,
|
|
288
328
|
};
|
|
289
329
|
}
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AI/LLM policy scans (#108, #911, #979, #1269).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two complementary scanners over the same input (concatenated repo docs
|
|
5
|
+
* — CONTRIBUTING.md, CODE_OF_CONDUCT.md, README, etc.):
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link scanForAntiLLMPolicy} — detects language indicating the
|
|
8
|
+
* project does NOT accept AI/LLM-generated contributions. Used by
|
|
9
|
+
* issue-scout / repo-evaluator to filter out repos where landing a
|
|
10
|
+
* PR is impossible.
|
|
11
|
+
*
|
|
12
|
+
* 2. {@link scanAIDisclosureRequirement} — detects the opposite:
|
|
13
|
+
* language requiring or inviting AI disclosure ("must disclose AI
|
|
14
|
+
* use", "credit AI tools"). Used by pr-compliance-checker to decide
|
|
15
|
+
* whether AI attribution should be flagged as a violation, encouraged,
|
|
16
|
+
* or required (#1269 Improvement C).
|
|
8
17
|
*
|
|
9
18
|
* The long-term home for this logic is `@oss-scout/core`, where the
|
|
10
19
|
* relevant files are already fetched during vetting. Keeping it here
|
|
11
20
|
* for now lets the agent invoke it directly and gives scout a
|
|
12
21
|
* reference implementation + test fixtures to adopt. See #979.
|
|
13
22
|
*
|
|
14
|
-
* Precision matters more than recall
|
|
15
|
-
*
|
|
16
|
-
* contribution surface
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Precision matters more than recall in both directions. A false
|
|
24
|
+
* positive on the anti-LLM side silently shrinks the user's
|
|
25
|
+
* contribution surface; a false positive on the disclosure side tells
|
|
26
|
+
* the user to add attribution that the maintainer didn't actually ask
|
|
27
|
+
* for. Patterns require an explicit verb-phrase + AI/LLM-noun pairing.
|
|
19
28
|
*
|
|
20
29
|
* **User-facing reference:** `docs/anti-llm-policy.md` — explains the
|
|
21
|
-
* three categories, example phrases per category, and the
|
|
22
|
-
* resistance design (why "AI division will be closed at
|
|
23
|
-
* does NOT match).
|
|
30
|
+
* three anti-LLM categories, example phrases per category, and the
|
|
31
|
+
* false-positive-resistance design (why "AI division will be closed at
|
|
32
|
+
* end of Q4" does NOT match).
|
|
24
33
|
*/
|
|
25
34
|
export type AntiLLMCategory = 'explicit_ban' | 'tool_ban' | 'reject_framing';
|
|
26
35
|
export interface AntiLLMMatch {
|
|
@@ -35,3 +44,23 @@ export interface AntiLLMScanResult {
|
|
|
35
44
|
matches: AntiLLMMatch[];
|
|
36
45
|
}
|
|
37
46
|
export declare function scanForAntiLLMPolicy(text: string): AntiLLMScanResult;
|
|
47
|
+
export type AIDisclosureCategory = 'mandatory' | 'recommended' | 'invited';
|
|
48
|
+
export interface AIDisclosureMatch {
|
|
49
|
+
category: AIDisclosureCategory;
|
|
50
|
+
phrase: string;
|
|
51
|
+
excerpt: string;
|
|
52
|
+
}
|
|
53
|
+
export interface AIDisclosureScanResult {
|
|
54
|
+
matched: boolean;
|
|
55
|
+
matches: AIDisclosureMatch[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Scan repo docs for language requiring or inviting AI disclosure (#1269).
|
|
59
|
+
*
|
|
60
|
+
* Mirrors {@link scanForAntiLLMPolicy}'s shape: same input contract,
|
|
61
|
+
* same false-positive-resistance discipline, same per-match excerpt for
|
|
62
|
+
* surfacing to the user. Categories are ordered by binding strength —
|
|
63
|
+
* callers that want to avoid false-positive flagging on weak invitations
|
|
64
|
+
* can filter to 'mandatory' / 'recommended' only.
|
|
65
|
+
*/
|
|
66
|
+
export declare function scanAIDisclosureRequirement(text: string): AIDisclosureScanResult;
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AI/LLM policy scans (#108, #911, #979, #1269).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two complementary scanners over the same input (concatenated repo docs
|
|
5
|
+
* — CONTRIBUTING.md, CODE_OF_CONDUCT.md, README, etc.):
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link scanForAntiLLMPolicy} — detects language indicating the
|
|
8
|
+
* project does NOT accept AI/LLM-generated contributions. Used by
|
|
9
|
+
* issue-scout / repo-evaluator to filter out repos where landing a
|
|
10
|
+
* PR is impossible.
|
|
11
|
+
*
|
|
12
|
+
* 2. {@link scanAIDisclosureRequirement} — detects the opposite:
|
|
13
|
+
* language requiring or inviting AI disclosure ("must disclose AI
|
|
14
|
+
* use", "credit AI tools"). Used by pr-compliance-checker to decide
|
|
15
|
+
* whether AI attribution should be flagged as a violation, encouraged,
|
|
16
|
+
* or required (#1269 Improvement C).
|
|
8
17
|
*
|
|
9
18
|
* The long-term home for this logic is `@oss-scout/core`, where the
|
|
10
19
|
* relevant files are already fetched during vetting. Keeping it here
|
|
11
20
|
* for now lets the agent invoke it directly and gives scout a
|
|
12
21
|
* reference implementation + test fixtures to adopt. See #979.
|
|
13
22
|
*
|
|
14
|
-
* Precision matters more than recall
|
|
15
|
-
*
|
|
16
|
-
* contribution surface
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Precision matters more than recall in both directions. A false
|
|
24
|
+
* positive on the anti-LLM side silently shrinks the user's
|
|
25
|
+
* contribution surface; a false positive on the disclosure side tells
|
|
26
|
+
* the user to add attribution that the maintainer didn't actually ask
|
|
27
|
+
* for. Patterns require an explicit verb-phrase + AI/LLM-noun pairing.
|
|
19
28
|
*
|
|
20
29
|
* **User-facing reference:** `docs/anti-llm-policy.md` — explains the
|
|
21
|
-
* three categories, example phrases per category, and the
|
|
22
|
-
* resistance design (why "AI division will be closed at
|
|
23
|
-
* does NOT match).
|
|
30
|
+
* three anti-LLM categories, example phrases per category, and the
|
|
31
|
+
* false-positive-resistance design (why "AI division will be closed at
|
|
32
|
+
* end of Q4" does NOT match).
|
|
24
33
|
*/
|
|
25
34
|
const PATTERNS = [
|
|
26
35
|
// Explicit "no X" bans against AI/LLM nouns.
|
|
@@ -104,3 +113,83 @@ export function scanForAntiLLMPolicy(text) {
|
|
|
104
113
|
}
|
|
105
114
|
return { matched: matches.length > 0, matches };
|
|
106
115
|
}
|
|
116
|
+
const DISCLOSURE_PATTERNS = [
|
|
117
|
+
// Mandatory: imperative verbs binding to an AI/LLM disclosure noun.
|
|
118
|
+
// Match shape: <verb-phrase> <AI noun> <disclosure noun>?
|
|
119
|
+
// Verb phrases: "must disclose", "required to disclose", "are required
|
|
120
|
+
// to indicate", "you must indicate". The optional disclosure-action
|
|
121
|
+
// noun catches "must disclose use of AI" without requiring a separate
|
|
122
|
+
// pattern. The AI noun can be plain "ai/llm" or a tool name.
|
|
123
|
+
{
|
|
124
|
+
category: 'mandatory',
|
|
125
|
+
regex: /\b(must|required\s+to|are\s+required\s+to)\s+(disclose|indicate|declare|note|state|mention|label|tag|credit|acknowledge)\s+(?:[a-z\s'-]{0,40}?\b)?(ai|llm|generative\s+ai|copilot|chatgpt|claude|cursor)\b/i,
|
|
126
|
+
},
|
|
127
|
+
// "PRs using AI must be labeled / tagged / disclosed"
|
|
128
|
+
{
|
|
129
|
+
category: 'mandatory',
|
|
130
|
+
regex: /\b(prs?|contributions?|commits?|code)\s+(using|generated\s+by|written\s+by|made\s+with)\s+(ai|llm|copilot|chatgpt|claude|cursor)\s+(?:tools?\s+)?(must\s+be|need\s+to\s+be|are\s+to\s+be)\s+(labeled|tagged|disclosed|marked|flagged|noted)\b/i,
|
|
131
|
+
},
|
|
132
|
+
// "Disclosure of AI assistance is required" — passive form
|
|
133
|
+
{
|
|
134
|
+
category: 'mandatory',
|
|
135
|
+
regex: /\b(disclosure|disclosing|labeling|labelling|tagging|crediting)\s+(of\s+)?(ai|llm|copilot|chatgpt|claude|cursor)(\s+(use|usage|assistance|tools?|contributions?|generated\s+code))?\s+(is\s+required|is\s+mandatory|must\s+be\s+included)\b/i,
|
|
136
|
+
},
|
|
137
|
+
// Recommended: softer "should" or "we ask" framing. Same noun anchors.
|
|
138
|
+
// Verb forms allow optional gerund (-ing) endings since "we strongly
|
|
139
|
+
// encourage acknowledging AI" reads naturally even though "acknowledge"
|
|
140
|
+
// is the bare verb in the imperative list.
|
|
141
|
+
{
|
|
142
|
+
category: 'recommended',
|
|
143
|
+
regex: /\b(should|we\s+ask\s+you\s+to|we\s+ask\s+that\s+you|we\s+(?:strongly\s+)?(?:encourage|recommend))\s+(disclose|disclosing|indicate|indicating|declare|declaring|note|noting|mention|mentioning|label|labeling|labelling|tag|tagging|credit|crediting|acknowledge|acknowledging)\s+(?:[a-z\s'-]{0,40}?\b)?(ai|llm|generative\s+ai|copilot|chatgpt|claude|cursor)\b/i,
|
|
144
|
+
},
|
|
145
|
+
// "credit AI tools you used" — direct imperative without "must/should"
|
|
146
|
+
{
|
|
147
|
+
category: 'recommended',
|
|
148
|
+
regex: /\b(credit|acknowledge|attribute)\s+(ai|llm|generative\s+ai)\s+(tools?|assistants?|use|usage|assistance)\b/i,
|
|
149
|
+
},
|
|
150
|
+
// Invited: permissive framing. Lower confidence.
|
|
151
|
+
// The "please" branch is dropped intentionally — bare "please mention X"
|
|
152
|
+
// doesn't carry enough policy weight on its own, and including it as
|
|
153
|
+
// `please\s+(?:feel\s+free\s+to)?\s+` introduces super-linear backtracking
|
|
154
|
+
// (regexp/no-super-linear-backtracking) because of the ambiguous \s+
|
|
155
|
+
// boundary on either side of the optional group.
|
|
156
|
+
{
|
|
157
|
+
category: 'invited',
|
|
158
|
+
regex: /\b(feel\s+free\s+to|please\s+feel\s+free\s+to|you('re|\s+are)\s+welcome\s+to|welcome\s+to)\s+(disclose|mention|note|indicate|label|tag|credit)\s+(?:[a-z\s'-]{0,40}?\b)?(ai|llm|generative\s+ai|copilot|chatgpt|claude|cursor)\b/i,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
/**
|
|
162
|
+
* Scan repo docs for language requiring or inviting AI disclosure (#1269).
|
|
163
|
+
*
|
|
164
|
+
* Mirrors {@link scanForAntiLLMPolicy}'s shape: same input contract,
|
|
165
|
+
* same false-positive-resistance discipline, same per-match excerpt for
|
|
166
|
+
* surfacing to the user. Categories are ordered by binding strength —
|
|
167
|
+
* callers that want to avoid false-positive flagging on weak invitations
|
|
168
|
+
* can filter to 'mandatory' / 'recommended' only.
|
|
169
|
+
*/
|
|
170
|
+
export function scanAIDisclosureRequirement(text) {
|
|
171
|
+
if (typeof text !== 'string') {
|
|
172
|
+
throw new TypeError(`scanAIDisclosureRequirement: expected string, received ${typeof text}`);
|
|
173
|
+
}
|
|
174
|
+
if (text === '')
|
|
175
|
+
return { matched: false, matches: [] };
|
|
176
|
+
const normalized = normalizeText(text);
|
|
177
|
+
const seenLabels = new Set();
|
|
178
|
+
const matches = [];
|
|
179
|
+
for (const pattern of DISCLOSURE_PATTERNS) {
|
|
180
|
+
const hit = normalized.match(pattern.regex);
|
|
181
|
+
if (!hit || hit.index === undefined)
|
|
182
|
+
continue;
|
|
183
|
+
const phrase = hit[0];
|
|
184
|
+
const key = `${pattern.category}:${phrase.toLowerCase()}`;
|
|
185
|
+
if (seenLabels.has(key))
|
|
186
|
+
continue;
|
|
187
|
+
seenLabels.add(key);
|
|
188
|
+
matches.push({
|
|
189
|
+
category: pattern.category,
|
|
190
|
+
phrase,
|
|
191
|
+
excerpt: makeExcerpt(normalized, hit.index, phrase.length),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return { matched: matches.length > 0, matches };
|
|
195
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from PRMonitor to isolate CI-related logic (#263).
|
|
4
4
|
*/
|
|
5
5
|
import type { Octokit } from '@octokit/rest';
|
|
6
|
-
import { CIFailureCategory, ClassifiedCheck, CIStatusResult } from './types.js';
|
|
6
|
+
import { CIFailureCategory, ClassifiedCheck, CIStatusResult, CIStatus, CIStatusCategorization } from './types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145, #743).
|
|
9
9
|
* Default is 'actionable' — only known patterns get reclassified.
|
|
@@ -16,6 +16,37 @@ export declare function classifyCICheck(name: string, description?: string, conc
|
|
|
16
16
|
* Accepts optional conclusion data to detect infrastructure failures and auth gates.
|
|
17
17
|
*/
|
|
18
18
|
export declare function classifyFailingChecks(failingCheckNames: string[], conclusions?: Map<string, string>): ClassifiedCheck[];
|
|
19
|
+
/**
|
|
20
|
+
* Map an aggregate `ciStatus + failingCheckNames + classifiedChecks` triple
|
|
21
|
+
* into one of five mutually exclusive overall states (#1272). The 5-row
|
|
22
|
+
* truth table previously lived as prose in `agents/pr-health-checker.md`;
|
|
23
|
+
* extracting it lets that agent (and any future consumer — dashboard,
|
|
24
|
+
* MCP, sibling agents that adopt the field) read a single typed value
|
|
25
|
+
* instead of re-deriving from `ciStatus + failingCheckNames + classifiedChecks`.
|
|
26
|
+
*
|
|
27
|
+
* Decision order (each branch is exclusive):
|
|
28
|
+
* 1. `passing` → `all_passing`
|
|
29
|
+
* 2. `pending` → `blocked` (awaiting trigger / completion)
|
|
30
|
+
* 3. `failing` + actionable → `failing` (real test/lint/build issue)
|
|
31
|
+
* 4. `failing` + only infrastructure → `blocked` (cancelled/timed-out runner — needs rerun)
|
|
32
|
+
* 5. `failing` + only fork/auth → `fork_limitation` (informational)
|
|
33
|
+
* 6. `failing` + zero classified → `failing` w/ "details unavailable" summary
|
|
34
|
+
* 7. `unknown` → `not_running`
|
|
35
|
+
*
|
|
36
|
+
* Why infrastructure routes to `blocked` and not `fork_limitation`:
|
|
37
|
+
* a cancelled or timed-out runner is genuinely worth re-running; calling
|
|
38
|
+
* it "informational" would tell the agent to ignore something the user
|
|
39
|
+
* can fix with a rerun-request.
|
|
40
|
+
*
|
|
41
|
+
* The `summary` is short (≤180 char even for 10+ failing checks) and
|
|
42
|
+
* suitable for inline display. `action` is a hint, not enforcement —
|
|
43
|
+
* agents may still escalate based on other PR context.
|
|
44
|
+
*/
|
|
45
|
+
export declare function categorizeCIStatus(input: {
|
|
46
|
+
ciStatus: CIStatus;
|
|
47
|
+
failingCheckNames: string[];
|
|
48
|
+
classifiedChecks: ClassifiedCheck[];
|
|
49
|
+
}): CIStatusCategorization;
|
|
19
50
|
/**
|
|
20
51
|
* Analyze check runs (GitHub Actions, etc.) and categorize them.
|
|
21
52
|
* Returns flags for failing/pending/success and lists of failing check names + conclusions.
|
package/dist/core/ci-analysis.js
CHANGED
|
@@ -92,6 +92,98 @@ export function classifyFailingChecks(failingCheckNames, conclusions) {
|
|
|
92
92
|
};
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Map an aggregate `ciStatus + failingCheckNames + classifiedChecks` triple
|
|
97
|
+
* into one of five mutually exclusive overall states (#1272). The 5-row
|
|
98
|
+
* truth table previously lived as prose in `agents/pr-health-checker.md`;
|
|
99
|
+
* extracting it lets that agent (and any future consumer — dashboard,
|
|
100
|
+
* MCP, sibling agents that adopt the field) read a single typed value
|
|
101
|
+
* instead of re-deriving from `ciStatus + failingCheckNames + classifiedChecks`.
|
|
102
|
+
*
|
|
103
|
+
* Decision order (each branch is exclusive):
|
|
104
|
+
* 1. `passing` → `all_passing`
|
|
105
|
+
* 2. `pending` → `blocked` (awaiting trigger / completion)
|
|
106
|
+
* 3. `failing` + actionable → `failing` (real test/lint/build issue)
|
|
107
|
+
* 4. `failing` + only infrastructure → `blocked` (cancelled/timed-out runner — needs rerun)
|
|
108
|
+
* 5. `failing` + only fork/auth → `fork_limitation` (informational)
|
|
109
|
+
* 6. `failing` + zero classified → `failing` w/ "details unavailable" summary
|
|
110
|
+
* 7. `unknown` → `not_running`
|
|
111
|
+
*
|
|
112
|
+
* Why infrastructure routes to `blocked` and not `fork_limitation`:
|
|
113
|
+
* a cancelled or timed-out runner is genuinely worth re-running; calling
|
|
114
|
+
* it "informational" would tell the agent to ignore something the user
|
|
115
|
+
* can fix with a rerun-request.
|
|
116
|
+
*
|
|
117
|
+
* The `summary` is short (≤180 char even for 10+ failing checks) and
|
|
118
|
+
* suitable for inline display. `action` is a hint, not enforcement —
|
|
119
|
+
* agents may still escalate based on other PR context.
|
|
120
|
+
*/
|
|
121
|
+
export function categorizeCIStatus(input) {
|
|
122
|
+
const { ciStatus, failingCheckNames, classifiedChecks } = input;
|
|
123
|
+
if (ciStatus === 'passing') {
|
|
124
|
+
return { category: 'all_passing', summary: 'All checks passing', action: 'none' };
|
|
125
|
+
}
|
|
126
|
+
if (ciStatus === 'pending') {
|
|
127
|
+
// `mergeStatuses` currently sets `failingCheckNames: []` on pending,
|
|
128
|
+
// so the count branch is defensive — kept so a future caller that
|
|
129
|
+
// forwards pending names doesn't silently drop them.
|
|
130
|
+
const count = failingCheckNames.length;
|
|
131
|
+
const summary = count > 0 ? `${count} pending check(s); CI run incomplete` : 'CI checks pending';
|
|
132
|
+
return { category: 'blocked', summary, action: 'request_rerun' };
|
|
133
|
+
}
|
|
134
|
+
if (ciStatus === 'failing') {
|
|
135
|
+
const actionable = classifiedChecks.filter((c) => c.category === 'actionable');
|
|
136
|
+
if (actionable.length > 0) {
|
|
137
|
+
const preview = actionable
|
|
138
|
+
.slice(0, 3)
|
|
139
|
+
.map((c) => c.name)
|
|
140
|
+
.join(', ');
|
|
141
|
+
const more = actionable.length > 3 ? ` (+${actionable.length - 3} more)` : '';
|
|
142
|
+
return {
|
|
143
|
+
category: 'failing',
|
|
144
|
+
summary: `${actionable.length} actionable failure(s): ${preview}${more}`,
|
|
145
|
+
action: 'investigate',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// No actionable failures. Distinguish three sub-cases:
|
|
149
|
+
// (a) failing with zero classified checks: status came from the
|
|
150
|
+
// legacy combined-status endpoint without per-check detail. Be
|
|
151
|
+
// honest about the missing detail rather than asserting "fork
|
|
152
|
+
// limitations" the caller can't verify.
|
|
153
|
+
if (classifiedChecks.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
category: 'failing',
|
|
156
|
+
summary: 'CI reported failure but no check details available',
|
|
157
|
+
action: 'investigate',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// (b) at least one infrastructure failure (cancelled / timed-out /
|
|
161
|
+
// dependency-install). Re-running often fixes the issue, so
|
|
162
|
+
// surface as `blocked` with `request_rerun` rather than
|
|
163
|
+
// mislabeling as "fork limits / auth gates."
|
|
164
|
+
const hasInfrastructure = classifiedChecks.some((c) => c.category === 'infrastructure');
|
|
165
|
+
if (hasInfrastructure) {
|
|
166
|
+
const total = classifiedChecks.length;
|
|
167
|
+
return {
|
|
168
|
+
category: 'blocked',
|
|
169
|
+
summary: `${total} non-actionable failure(s) including infrastructure issues; rerun may resolve`,
|
|
170
|
+
action: 'request_rerun',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// (c) only fork-limitation / auth-gate failures — purely informational.
|
|
174
|
+
const total = classifiedChecks.length;
|
|
175
|
+
return {
|
|
176
|
+
category: 'fork_limitation',
|
|
177
|
+
summary: `${total} non-actionable failure(s) (fork limits / auth gates)`,
|
|
178
|
+
action: 'informational',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ciStatus === 'unknown' — no checks reported, or status couldn't be
|
|
182
|
+
// determined. Treat both as not_running so callers don't have to
|
|
183
|
+
// distinguish the rare indeterminate case from the common "no CI
|
|
184
|
+
// configured" case.
|
|
185
|
+
return { category: 'not_running', summary: 'No CI checks reported', action: 'check_workflows' };
|
|
186
|
+
}
|
|
95
187
|
/**
|
|
96
188
|
* Analyze check runs (GitHub Actions, etc.) and categorize them.
|
|
97
189
|
* Returns flags for failing/pending/success and lists of failing check names + conclusions.
|