co-maintainer 0.4.9 → 0.4.11
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/package.json +1 -1
- package/dist/src/cli/args.js +21 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/src/github/collect.js +46 -6
- package/dist/src/knowledge/validate.d.ts +1 -1
- package/dist/src/knowledge/validate.js +7 -69
- package/dist/src/services/setup.js +5 -3
- package/dist/src/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
package/dist/src/cli/args.js
CHANGED
|
@@ -53,6 +53,7 @@ export async function parseArgs(args) {
|
|
|
53
53
|
console.log(" --include-commit-history --include-how-repo-works");
|
|
54
54
|
console.log(" --max-commits=N --max-pr-months=N --max-pull-request-change-lines=N --max-comment=N");
|
|
55
55
|
console.log(" --only-request-changed-pr");
|
|
56
|
+
console.log(" --pr-state=open,closed,merged");
|
|
56
57
|
console.log(" --auth=gh|pat --github-pat=... --ai=none|openrouter|hetzner --token=... --low-model=... --high-model=...");
|
|
57
58
|
process.exit(0);
|
|
58
59
|
}
|
|
@@ -111,6 +112,24 @@ export async function parseArgs(args) {
|
|
|
111
112
|
: undefined;
|
|
112
113
|
};
|
|
113
114
|
const text = (name) => rest.find((item) => item.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
115
|
+
const prStateOption = (args, saved) => {
|
|
116
|
+
const prefix = "--pr-state=";
|
|
117
|
+
const raw = args
|
|
118
|
+
.find((arg) => arg.startsWith(prefix))
|
|
119
|
+
?.slice(prefix.length);
|
|
120
|
+
if (raw === undefined)
|
|
121
|
+
return saved && saved.length > 0 ? saved : undefined;
|
|
122
|
+
const allowed = ["open", "closed", "merged"];
|
|
123
|
+
const parts = raw
|
|
124
|
+
.split(",")
|
|
125
|
+
.map((part) => part.trim())
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
if (parts.length === 0 ||
|
|
128
|
+
parts.some((part) => !allowed.includes(part))) {
|
|
129
|
+
die("pr-state must be a comma-separated list of: open, closed, merged");
|
|
130
|
+
}
|
|
131
|
+
return [...new Set(parts)];
|
|
132
|
+
};
|
|
114
133
|
const choice = (name, allowed, fallback) => {
|
|
115
134
|
const prefix = `--${name}=`;
|
|
116
135
|
const raw = rest
|
|
@@ -130,6 +149,7 @@ export async function parseArgs(args) {
|
|
|
130
149
|
"max-pr-months",
|
|
131
150
|
"max-pull-request-change-lines",
|
|
132
151
|
"max-comment",
|
|
152
|
+
"pr-state",
|
|
133
153
|
"improve-matrix",
|
|
134
154
|
"env",
|
|
135
155
|
"gh-concurrent",
|
|
@@ -245,6 +265,7 @@ export async function parseArgs(args) {
|
|
|
245
265
|
includeHowRepoWorks: enabled("include-how-repo-works"),
|
|
246
266
|
onlyRequestChangedPr: rest.includes("--only-request-changed-pr") ||
|
|
247
267
|
repoConfig.onlyRequestChangedPr === true,
|
|
268
|
+
prState: prStateOption(rest, repoConfig.prState),
|
|
248
269
|
maxCommits: value("max-commits") ?? repoConfig.maxCommits ?? configDefault.maxCommits,
|
|
249
270
|
maxPrMonths: value("max-pr-months") ??
|
|
250
271
|
repoConfig.maxPrMonths ??
|
package/dist/src/config.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type RepoConfig = {
|
|
|
18
18
|
includeCommitHistory?: boolean;
|
|
19
19
|
includeHowRepoWorks?: boolean;
|
|
20
20
|
onlyRequestChangedPr?: boolean;
|
|
21
|
+
prState?: ("open" | "closed" | "merged")[];
|
|
21
22
|
/** Five field cron in UTC, checked by `serve` to queue a remake. */
|
|
22
23
|
remakeCron?: string;
|
|
23
24
|
};
|
|
@@ -30,6 +30,36 @@ function listingCovers(cachedMonths, current) {
|
|
|
30
30
|
return false;
|
|
31
31
|
return cachedMonths >= current;
|
|
32
32
|
}
|
|
33
|
+
function stateKey(states) {
|
|
34
|
+
if (!states || states.length === 0)
|
|
35
|
+
return "";
|
|
36
|
+
return [...states].sort().join(",");
|
|
37
|
+
}
|
|
38
|
+
/** `closed` is closed and not merged. GitHub's list has no merged state. */
|
|
39
|
+
function matchesPrState(pr, states) {
|
|
40
|
+
if (!states || states.length === 0)
|
|
41
|
+
return true;
|
|
42
|
+
const merged = Boolean(pr.merged_at);
|
|
43
|
+
const state = String(pr.state ?? "");
|
|
44
|
+
return states.some((wanted) => {
|
|
45
|
+
if (wanted === "merged")
|
|
46
|
+
return merged;
|
|
47
|
+
if (wanted === "open")
|
|
48
|
+
return state === "open";
|
|
49
|
+
return state === "closed" && !merged;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function listQueryState(states) {
|
|
53
|
+
if (!states || states.length === 0)
|
|
54
|
+
return "all";
|
|
55
|
+
const wantsOpen = states.includes("open");
|
|
56
|
+
const wantsClosed = states.includes("closed") || states.includes("merged");
|
|
57
|
+
if (wantsOpen && wantsClosed)
|
|
58
|
+
return "all";
|
|
59
|
+
if (wantsOpen)
|
|
60
|
+
return "open";
|
|
61
|
+
return "closed";
|
|
62
|
+
}
|
|
33
63
|
async function loadListing(repo) {
|
|
34
64
|
const raw = await cacheGet("pr-listing", repo);
|
|
35
65
|
if (!raw)
|
|
@@ -39,20 +69,26 @@ async function loadListing(repo) {
|
|
|
39
69
|
return undefined;
|
|
40
70
|
return parsed;
|
|
41
71
|
}
|
|
42
|
-
async function saveListing(repo, maxPrMonths, items) {
|
|
43
|
-
await cacheSet("pr-listing", repo, JSON.stringify({
|
|
72
|
+
async function saveListing(repo, maxPrMonths, states, items) {
|
|
73
|
+
await cacheSet("pr-listing", repo, JSON.stringify({
|
|
74
|
+
maxPrMonths: maxPrMonths ?? 0,
|
|
75
|
+
states: stateKey(states),
|
|
76
|
+
items,
|
|
77
|
+
}));
|
|
44
78
|
}
|
|
45
79
|
async function listPullRequestPages(client, options, phase) {
|
|
46
80
|
const cached = await loadListing(options.repo);
|
|
47
81
|
const canCatchUp = cached !== undefined &&
|
|
48
|
-
listingCovers(cached.maxPrMonths, options.maxPrMonths)
|
|
82
|
+
listingCovers(cached.maxPrMonths, options.maxPrMonths) &&
|
|
83
|
+
(cached.states ?? "") === stateKey(options.prState);
|
|
49
84
|
const cachedByNumber = new Map((cached?.items ?? []).map((pr) => [Number(pr.number), pr]));
|
|
50
85
|
const selected = [];
|
|
51
86
|
const seen = new Set();
|
|
52
87
|
const concurrency = Math.max(1, options.ghConcurrent);
|
|
53
|
-
|
|
88
|
+
const states = stateKey(options.prState) || "all";
|
|
89
|
+
log("fetch", `pull request listing · ${states} · concurrency=${concurrency}`);
|
|
54
90
|
const fetchPage = async (page) => {
|
|
55
|
-
const pageItems = await client.request(`repos/${options.repo}/pulls?state
|
|
91
|
+
const pageItems = await client.request(`repos/${options.repo}/pulls?state=${listQueryState(options.prState)}&sort=updated&direction=desc&per_page=100&page=${page}`);
|
|
56
92
|
return Array.isArray(pageItems) ? pageItems : [];
|
|
57
93
|
};
|
|
58
94
|
const ingest = (page, pageItems) => {
|
|
@@ -72,6 +108,8 @@ async function listPullRequestPages(client, options, phase) {
|
|
|
72
108
|
reason = "window reached";
|
|
73
109
|
break;
|
|
74
110
|
}
|
|
111
|
+
if (!matchesPrState(pr, options.prState))
|
|
112
|
+
continue;
|
|
75
113
|
const number = Number(pr.number);
|
|
76
114
|
selected.push(pr);
|
|
77
115
|
seen.add(number);
|
|
@@ -111,6 +149,8 @@ async function listPullRequestPages(client, options, phase) {
|
|
|
111
149
|
if (!withinPrWindow(String(pr.updated_at ?? ""), options.maxPrMonths)) {
|
|
112
150
|
continue;
|
|
113
151
|
}
|
|
152
|
+
if (!matchesPrState(pr, options.prState))
|
|
153
|
+
continue;
|
|
114
154
|
selected.push(pr);
|
|
115
155
|
seen.add(number);
|
|
116
156
|
reused++;
|
|
@@ -119,7 +159,7 @@ async function listPullRequestPages(client, options, phase) {
|
|
|
119
159
|
log("fetch", `pull request listing · reused ${reused} cached PRs · ${selected.length} total`);
|
|
120
160
|
}
|
|
121
161
|
}
|
|
122
|
-
await saveListing(options.repo, options.maxPrMonths, selected);
|
|
162
|
+
await saveListing(options.repo, options.maxPrMonths, options.prState, selected);
|
|
123
163
|
return selected;
|
|
124
164
|
}
|
|
125
165
|
async function mapPool(items, concurrency, fn) {
|
|
@@ -4,4 +4,4 @@ export type ValidationResult = {
|
|
|
4
4
|
errors: string[];
|
|
5
5
|
warnings: string[];
|
|
6
6
|
};
|
|
7
|
-
export declare function validateSkill(markdown: string,
|
|
7
|
+
export declare function validateSkill(markdown: string, source?: Source, referenceIssues?: "error" | "warning"): Promise<ValidationResult>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export async function validateSkill(markdown, outputDirectory, source, referenceIssues = "error") {
|
|
1
|
+
export async function validateSkill(markdown, source, referenceIssues = "error") {
|
|
3
2
|
const errors = [];
|
|
4
3
|
const warnings = [];
|
|
5
4
|
const referenceProblems = referenceIssues === "error" ? errors : warnings;
|
|
@@ -33,54 +32,6 @@ export async function validateSkill(markdown, outputDirectory, source, reference
|
|
|
33
32
|
if (source) {
|
|
34
33
|
const paths = new Set([...source.tree, ...Object.keys(source.files)]);
|
|
35
34
|
const repositoryName = String(source.repo.full_name ?? "");
|
|
36
|
-
const commands = new Set();
|
|
37
|
-
for (const [path, content] of Object.entries(source.files)) {
|
|
38
|
-
if (path.endsWith("package.json")) {
|
|
39
|
-
try {
|
|
40
|
-
const scripts = JSON.parse(content)
|
|
41
|
-
.scripts ?? {};
|
|
42
|
-
const prefix = source.tree.some((item) => /pnpm-lock\.yaml$/.test(item))
|
|
43
|
-
? "pnpm"
|
|
44
|
-
: source.tree.some((item) => /yarn\.lock$/.test(item))
|
|
45
|
-
? "yarn"
|
|
46
|
-
: "npm";
|
|
47
|
-
for (const name of Object.keys(scripts)) {
|
|
48
|
-
commands.add(`${prefix} ${name}`);
|
|
49
|
-
commands.add(`${prefix} run ${name}`);
|
|
50
|
-
commands.add(String(scripts[name]));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Ignore malformed manifests; syntax validation is outside this gate.
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (/deno\.jsonc?$/.test(path)) {
|
|
58
|
-
try {
|
|
59
|
-
const tasks = JSON.parse(content).tasks ??
|
|
60
|
-
{};
|
|
61
|
-
for (const [name, task] of Object.entries(tasks)) {
|
|
62
|
-
commands.add(`deno task ${name}`);
|
|
63
|
-
commands.add(String(task));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// Ignore malformed manifests; syntax validation is outside this gate.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
for (const match of content.matchAll(/^\s*run:\s*([^\s#].*?)\s*$/gm)) {
|
|
71
|
-
commands.add(match[1].replace(/^['"]|['"]$/g, ""));
|
|
72
|
-
}
|
|
73
|
-
for (const match of content.matchAll(/`((?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s+[^`\n]+)`/g)) {
|
|
74
|
-
commands.add(match[1]);
|
|
75
|
-
}
|
|
76
|
-
for (const match of content.matchAll(/^\s*((?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s+[^\n`]+)$/gm)) {
|
|
77
|
-
commands.add(match[1].trim());
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (source.tree.some((path) => /Cargo\.toml$/.test(path))) {
|
|
81
|
-
commands.add("cargo test");
|
|
82
|
-
commands.add("cargo build");
|
|
83
|
-
}
|
|
84
35
|
const references = [...markdown.matchAll(/`([^`\n]+)`/g)].map((match) => match[1]);
|
|
85
36
|
for (const reference of references) {
|
|
86
37
|
const isQualifier = /^[a-z][a-z\d_-]*:/i.test(reference);
|
|
@@ -109,27 +60,14 @@ export async function validateSkill(markdown, outputDirectory, source, reference
|
|
|
109
60
|
referenceProblems.push(`referenced path is absent from source: ${reference}`);
|
|
110
61
|
}
|
|
111
62
|
}
|
|
112
|
-
if (/^(?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s/.test(reference) &&
|
|
113
|
-
![...commands].some((command) => reference === command ||
|
|
114
|
-
reference.startsWith(`${command} `) ||
|
|
115
|
-
command.startsWith(`${reference} `))) {
|
|
116
|
-
referenceProblems.push(`referenced command is absent from source: ${reference}`);
|
|
117
|
-
}
|
|
118
63
|
}
|
|
119
64
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
if (/^[a-z][a-z\d+.-]*:/i.test(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
errors.push(`link is not relative: ${
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
await stat(`${outputDirectory}/${link}`);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
errors.push(`linked file does not exist: ${link}`);
|
|
65
|
+
for (const link of markdown.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
|
|
66
|
+
const target = link[1];
|
|
67
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(target) ||
|
|
68
|
+
target.startsWith("/") ||
|
|
69
|
+
target.includes("..")) {
|
|
70
|
+
errors.push(`link is not relative: ${target}`);
|
|
133
71
|
}
|
|
134
72
|
}
|
|
135
73
|
return { valid: errors.length === 0, errors, warnings };
|
|
@@ -204,19 +204,19 @@ export async function runInitOrRemake(options) {
|
|
|
204
204
|
let result = await timed("assemble skill", options.logTime, () => assembleSkill(options.repo, facts, previousMarkdown, previous?.sectionHashes ?? {}, overrides));
|
|
205
205
|
const reviewDocuments = buildReviewDocuments(facts);
|
|
206
206
|
result.markdown = addReviewLink(result.markdown, reviewDocuments);
|
|
207
|
-
const validation = await timed("validate skill", options.logTime, () => validateSkill(result.markdown,
|
|
207
|
+
const validation = await timed("validate skill", options.logTime, () => validateSkill(result.markdown, source));
|
|
208
208
|
if (!validation.valid && Object.keys(overrides).length) {
|
|
209
209
|
log("validate", `AI output rejected: ${validation.errors.join("; ")}`);
|
|
210
210
|
overrides = {};
|
|
211
211
|
result = await timed("reassemble valid skill", options.logTime, () => assembleSkill(options.repo, facts, previousMarkdown, previous?.sectionHashes ?? {}, overrides));
|
|
212
212
|
result.markdown = addReviewLink(result.markdown, reviewDocuments);
|
|
213
213
|
}
|
|
214
|
-
const finalValidation = await timed("final skill validation", options.logTime, () => validateSkill(result.markdown,
|
|
214
|
+
const finalValidation = await timed("final skill validation", options.logTime, () => validateSkill(result.markdown, source, "warning"));
|
|
215
215
|
if (finalValidation.warnings.length) {
|
|
216
216
|
log("validate", `stale references kept: ${finalValidation.warnings.join(", ")}`);
|
|
217
217
|
}
|
|
218
218
|
if (!finalValidation.valid) {
|
|
219
|
-
|
|
219
|
+
log("validate", `skill problems kept: ${finalValidation.errors.join("; ")}`);
|
|
220
220
|
}
|
|
221
221
|
await timed("write skill and state", options.logTime, async () => {
|
|
222
222
|
const { withKnowledgeLock } = await import("../review/guides.js");
|
|
@@ -260,6 +260,7 @@ export async function runInitOrRemake(options) {
|
|
|
260
260
|
includeCommitHistory: options.includeCommitHistory,
|
|
261
261
|
includeHowRepoWorks: options.includeHowRepoWorks,
|
|
262
262
|
onlyRequestChangedPr: options.onlyRequestChangedPr,
|
|
263
|
+
prState: options.prState,
|
|
263
264
|
});
|
|
264
265
|
});
|
|
265
266
|
log("write", `${path} · ${result.changed.length
|
|
@@ -308,6 +309,7 @@ export function optionsFromConfig(repo, command) {
|
|
|
308
309
|
includeCommitHistory: repoConfig.includeCommitHistory ?? true,
|
|
309
310
|
includeHowRepoWorks: repoConfig.includeHowRepoWorks ?? true,
|
|
310
311
|
onlyRequestChangedPr: repoConfig.onlyRequestChangedPr === true,
|
|
312
|
+
prState: repoConfig.prState,
|
|
311
313
|
maxCommits: repoConfig.maxCommits ?? config.defaults?.maxCommits,
|
|
312
314
|
maxPrMonths: repoConfig.maxPrMonths ?? config.defaults?.maxPrMonths,
|
|
313
315
|
maxPullRequestChangeLines: repoConfig.maxPullRequestChangeLines ??
|
package/dist/src/types.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type Options = {
|
|
|
37
37
|
/** Keep only pull requests in the window that have at least one
|
|
38
38
|
* `CHANGES_REQUESTED` review. Off by default, so the window is unchanged. */
|
|
39
39
|
onlyRequestChangedPr: boolean;
|
|
40
|
+
/** Empty or omitted means every state. `closed` is closed and not merged. */
|
|
41
|
+
prState?: ("open" | "closed" | "merged")[];
|
|
40
42
|
maxCommits?: number;
|
|
41
43
|
maxPrMonths?: number;
|
|
42
44
|
maxPullRequestChangeLines?: number;
|