co-maintainer 0.4.13 → 0.5.0-beta.1
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/LICENSE +21 -21
- package/README.md +57 -38
- package/dist/main.js +2 -3
- package/dist/package.json +21 -15
- package/dist/src/ai/batch.d.ts +24 -1
- package/dist/src/ai/batch.js +75 -23
- package/dist/src/ai/estimate.d.ts +40 -0
- package/dist/src/ai/estimate.js +113 -0
- package/dist/src/ai/fake.d.ts +1 -1
- package/dist/src/ai/fake.js +1 -1
- package/dist/src/ai/hetzner.js +1 -1
- package/dist/src/ai/openrouter.d.ts +13 -0
- package/dist/src/ai/openrouter.js +66 -12
- package/dist/src/ai/pricing.d.ts +14 -0
- package/dist/src/ai/pricing.js +77 -0
- package/dist/src/ai/provider.js +8 -0
- package/dist/src/ai/verify.d.ts +25 -0
- package/dist/src/ai/verify.js +78 -0
- package/dist/src/cli/args.js +25 -29
- package/dist/src/cli/commands/config.d.ts +44 -0
- package/dist/src/cli/commands/config.js +220 -0
- package/dist/src/cli/commands/probe.js +70 -103
- package/dist/src/cli/commands/registry.d.ts +57 -0
- package/dist/src/cli/commands/registry.js +713 -0
- package/dist/src/cli/commands/review.js +66 -43
- package/dist/src/cli/commands/serve.d.ts +4 -2
- package/dist/src/cli/commands/serve.js +14 -10
- package/dist/src/cli/commands/set.d.ts +2 -1
- package/dist/src/cli/commands/set.js +63 -17
- package/dist/src/cli/commands/view.d.ts +1 -0
- package/dist/src/cli/commands/view.js +180 -0
- package/dist/src/cli/error.d.ts +39 -0
- package/dist/src/cli/error.js +64 -0
- package/dist/src/cli/main.d.ts +7 -0
- package/dist/src/cli/main.js +82 -1
- package/dist/src/cli/prompt.js +21 -7
- package/dist/src/cli/review_args.d.ts +4 -0
- package/dist/src/cli/review_args.js +22 -9
- package/dist/src/cli/review_output.d.ts +2 -2
- package/dist/src/cli/review_output.js +7 -6
- package/dist/src/cli/review_result.d.ts +59 -8
- package/dist/src/cli/review_result.js +145 -103
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +31 -0
- package/dist/src/github/app.d.ts +13 -0
- package/dist/src/github/app.js +23 -0
- package/dist/src/github/app_manifest.d.ts +50 -0
- package/dist/src/github/app_manifest.js +138 -0
- package/dist/src/github/client.js +7 -1
- package/dist/src/github/collect.js +1 -1
- package/dist/src/github/gh.js +75 -18
- package/dist/src/knowledge/facts.js +8 -3
- package/dist/src/knowledge/guide.d.ts +8 -0
- package/dist/src/knowledge/guide.js +32 -8
- package/dist/src/knowledge/probe.js +13 -3
- package/dist/src/knowledge/sections.d.ts +9 -0
- package/dist/src/knowledge/sections.js +16 -0
- package/dist/src/knowledge/skill.d.ts +5 -0
- package/dist/src/knowledge/skill.js +68 -21
- package/dist/src/knowledge/synthesis.d.ts +17 -1
- package/dist/src/knowledge/synthesis.js +70 -15
- package/dist/src/knowledge/types.d.ts +5 -0
- package/dist/src/local/codegraph_prepare.d.ts +3 -2
- package/dist/src/local/codegraph_prepare.js +4 -2
- package/dist/src/local/git_ops.d.ts +5 -4
- package/dist/src/local/git_ops.js +8 -9
- package/dist/src/local/review_local.js +36 -14
- package/dist/src/pr/checkout.js +14 -3
- package/dist/src/pr/codegraph_tools.js +3 -3
- package/dist/src/pr/diff_summary.js +3 -3
- package/dist/src/pr/findings.js +16 -5
- package/dist/src/pr/findings_json.d.ts +26 -0
- package/dist/src/pr/findings_json.js +187 -0
- package/dist/src/pr/review_copy.d.ts +20 -0
- package/dist/src/pr/review_copy.js +40 -0
- package/dist/src/pr/reviewer.d.ts +2 -1
- package/dist/src/pr/reviewer.js +84 -91
- package/dist/src/remote/client.js +42 -42
- package/dist/src/remote/http.d.ts +5 -0
- package/dist/src/remote/http.js +49 -0
- package/dist/src/remote/server/guides.d.ts +22 -0
- package/dist/src/remote/server/guides.js +81 -0
- package/dist/src/remote/server/routes.js +24 -0
- package/dist/src/review/blocking.d.ts +35 -0
- package/dist/src/review/blocking.js +45 -0
- package/dist/src/review/carry_over.d.ts +12 -1
- package/dist/src/review/carry_over.js +35 -9
- package/dist/src/review/engine.d.ts +1 -0
- package/dist/src/review/guides.js +28 -1
- package/dist/src/server/api/installations.js +1 -1
- package/dist/src/server/api/repos.js +86 -3
- package/dist/src/server/api/settings.js +3 -3
- package/dist/src/server/app.d.ts +3 -0
- package/dist/src/server/app.js +12 -5
- package/dist/src/server/pages/activity.d.ts +1 -0
- package/dist/src/server/pages/activity.js +2 -2
- package/dist/src/server/pages/add_repo.js +73 -5
- package/dist/src/server/pages/client.d.ts +1 -1
- package/dist/src/server/pages/client.js +1 -1
- package/dist/src/server/pages/home.d.ts +6 -1
- package/dist/src/server/pages/home.js +25 -2
- package/dist/src/server/pages/knowledge.js +1 -1
- package/dist/src/server/pages/layout.js +2 -2
- package/dist/src/server/pages/repo.d.ts +1 -1
- package/dist/src/server/pages/repo.js +21 -2
- package/dist/src/server/pages/repo_prs.d.ts +5 -1
- package/dist/src/server/pages/repo_prs.js +36 -1
- package/dist/src/server/pages/repo_remote.js +2 -2
- package/dist/src/server/pages/repo_settings.js +2 -2
- package/dist/src/server/pages/router.d.ts +6 -0
- package/dist/src/server/pages/router.js +129 -9
- package/dist/src/server/pages/settings.d.ts +1 -1
- package/dist/src/server/pages/settings.js +87 -9
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/probe.d.ts +28 -0
- package/dist/src/services/probe.js +133 -0
- package/dist/src/services/remake_cron.d.ts +1 -1
- package/dist/src/services/remake_cron.js +3 -3
- package/dist/src/services/remote_review.js +14 -4
- package/dist/src/services/review.d.ts +11 -0
- package/dist/src/services/review.js +39 -11
- package/dist/src/services/setup.js +37 -19
- package/dist/src/services/setup_checklist.d.ts +10 -0
- package/dist/src/services/setup_checklist.js +87 -0
- package/dist/src/store/app_db.js +5 -2
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +27 -4
- package/dist/src/store/deliveries.d.ts +7 -0
- package/dist/src/store/deliveries.js +15 -0
- package/dist/src/tools/codegraph.d.ts +30 -0
- package/dist/src/tools/codegraph.js +49 -12
- package/dist/src/types.d.ts +10 -0
- package/dist/src/util/log.d.ts +3 -0
- package/dist/src/util/log.js +12 -0
- package/dist/src/util/run_summary.d.ts +33 -0
- package/dist/src/util/run_summary.js +47 -0
- package/dist/src/util/webhook_reachability.d.ts +11 -0
- package/dist/src/util/webhook_reachability.js +64 -0
- package/package.json +21 -15
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** The reading half of `co-maintainer probe`, shared by the CLI and the
|
|
2
|
+
* dashboard's Add repository preview (CORE-73).
|
|
3
|
+
*
|
|
4
|
+
* It only reads: no guide is written, nothing is cached here. The CLI adds
|
|
5
|
+
* the estimate, the printing and `--run`; `/api/repos/preview` adds the
|
|
6
|
+
* access check and returns the plan for confirmation. Keeping one
|
|
7
|
+
* implementation means the number an operator confirms is the number `init`
|
|
8
|
+
* would be given. */
|
|
9
|
+
import { analyzeProbe } from "../knowledge/probe.js";
|
|
10
|
+
import { timed } from "../util/log.js";
|
|
11
|
+
/** Samples this many pull requests for diff sizes, on top of a few per year. */
|
|
12
|
+
const SAMPLE_HEAD = 30;
|
|
13
|
+
const SAMPLE_PER_YEAR = 3;
|
|
14
|
+
export async function probePlan(client, repo, opts) {
|
|
15
|
+
const say = opts.log ?? (() => { });
|
|
16
|
+
const timedRun = (label, fn) => timed(label, opts.logTime ?? false, fn);
|
|
17
|
+
say(`reading ${repo} metadata`);
|
|
18
|
+
const meta = await timedRun("probe repository metadata", () => client.request(`repos/${repo}`));
|
|
19
|
+
let latestReleaseAt = "";
|
|
20
|
+
try {
|
|
21
|
+
const releases = await timedRun("probe release metadata", () => client.pages(`repos/${repo}/releases?per_page=1`, 1));
|
|
22
|
+
latestReleaseAt = String(releases[0]?.published_at ?? releases[0]?.created_at ?? "");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Release metadata is an optional probe signal.
|
|
26
|
+
}
|
|
27
|
+
say("reading pull request list");
|
|
28
|
+
const pulls = await timedRun("probe pull request listing", () => client.pages(`repos/${repo}/pulls?state=all&sort=updated&direction=desc`, undefined, (page, fetched) => say(`pull request listing · page ${page} · fetched ${fetched}`)));
|
|
29
|
+
const yearBuckets = new Map();
|
|
30
|
+
for (const pull of pulls) {
|
|
31
|
+
const pullYear = new Date(String(pull.updated_at)).getFullYear();
|
|
32
|
+
yearBuckets.set(pullYear, [...(yearBuckets.get(pullYear) ?? []), pull]);
|
|
33
|
+
}
|
|
34
|
+
const sampleTargets = new Map();
|
|
35
|
+
for (const pull of pulls.slice(0, SAMPLE_HEAD)) {
|
|
36
|
+
sampleTargets.set(Number(pull.number), pull);
|
|
37
|
+
}
|
|
38
|
+
for (const yearPulls of yearBuckets.values()) {
|
|
39
|
+
for (const pull of yearPulls.slice(0, SAMPLE_PER_YEAR)) {
|
|
40
|
+
sampleTargets.set(Number(pull.number), pull);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const samplePulls = [...sampleTargets.values()];
|
|
44
|
+
const detailSamples = [];
|
|
45
|
+
say(`sampling ${samplePulls.length} pull request details · concurrency=${opts.ghConcurrent}`);
|
|
46
|
+
await timedRun("probe PR detail sampling", async () => {
|
|
47
|
+
const details = new Array(samplePulls.length);
|
|
48
|
+
let cursor = 0;
|
|
49
|
+
let completed = 0;
|
|
50
|
+
const worker = async () => {
|
|
51
|
+
while (cursor < samplePulls.length) {
|
|
52
|
+
const index = cursor++;
|
|
53
|
+
const pull = samplePulls[index];
|
|
54
|
+
try {
|
|
55
|
+
details[index] = await client.request(`repos/${repo}/pulls/${Number(pull.number)}`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A missing detail should not invalidate the rest of the probe.
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
completed++;
|
|
62
|
+
say(`PR detail sampling · ${completed}/${samplePulls.length}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
await Promise.all(Array.from({ length: Math.min(opts.ghConcurrent, samplePulls.length) }, worker));
|
|
67
|
+
detailSamples.push(...details.filter((detail) => !!detail));
|
|
68
|
+
});
|
|
69
|
+
const branch = String(meta.default_branch ?? "main");
|
|
70
|
+
say(`sampled ${detailSamples.length} PR details · reading ${branch} commits`);
|
|
71
|
+
const commits = await timedRun("probe commit history", () => client.pages(`repos/${repo}/commits?sha=${encodeURIComponent(branch)}`, undefined, (page, fetched) => say(`commit history · page ${page} · fetched ${fetched}`)));
|
|
72
|
+
const analysis = await timedRun("probe analysis", async () => analyzeProbe({ ...meta, latest_release_at: latestReleaseAt }, pulls, detailSamples, commits));
|
|
73
|
+
const recommendation = ["--include-codebase"];
|
|
74
|
+
if (analysis.includePullRequests)
|
|
75
|
+
recommendation.push("--include-pull-requests");
|
|
76
|
+
if (analysis.includePullRequestChanges) {
|
|
77
|
+
recommendation.push("--include-pull-request-changes");
|
|
78
|
+
}
|
|
79
|
+
if (analysis.includeCommitHistory) {
|
|
80
|
+
recommendation.push("--include-commit-history");
|
|
81
|
+
}
|
|
82
|
+
if (pulls.length || Boolean(meta.has_issues)) {
|
|
83
|
+
recommendation.push("--include-how-repo-works");
|
|
84
|
+
}
|
|
85
|
+
if (analysis.maxPullRequestChangeLines) {
|
|
86
|
+
recommendation.push(`--max-pull-request-change-lines=${analysis.maxPullRequestChangeLines}`);
|
|
87
|
+
}
|
|
88
|
+
if (analysis.maxPrMonths) {
|
|
89
|
+
recommendation.push(`--max-pr-months=${analysis.maxPrMonths}`);
|
|
90
|
+
}
|
|
91
|
+
if (analysis.maxCommits) {
|
|
92
|
+
recommendation.push(`--max-commits=${analysis.maxCommits}`);
|
|
93
|
+
}
|
|
94
|
+
const command = ["co-maintainer", "init", repo, ...recommendation].join(" ");
|
|
95
|
+
const report = {
|
|
96
|
+
repo,
|
|
97
|
+
...analysis.report,
|
|
98
|
+
recommendations: {
|
|
99
|
+
includePullRequests: analysis.includePullRequests,
|
|
100
|
+
includePullRequestChanges: analysis.includePullRequestChanges,
|
|
101
|
+
includeCommitHistory: analysis.includeCommitHistory,
|
|
102
|
+
maxPrMonths: analysis.maxPrMonths ?? "all",
|
|
103
|
+
maxCommits: analysis.maxCommits ?? "all",
|
|
104
|
+
maxPullRequestChangeLines: analysis.maxPullRequestChangeLines ?? "all",
|
|
105
|
+
},
|
|
106
|
+
reasons: analysis.reasons,
|
|
107
|
+
recommendedCommand: command,
|
|
108
|
+
createdAt: new Date().toISOString(),
|
|
109
|
+
};
|
|
110
|
+
return { analysis, recommendation, command, report };
|
|
111
|
+
}
|
|
112
|
+
/** The `RepoConfig` patch that matches a recommendation, so a confirmed
|
|
113
|
+
* preview stores the same limits the printed command carries. */
|
|
114
|
+
export function recommendationPatch(recommendation) {
|
|
115
|
+
const has = (flag) => recommendation.includes(flag);
|
|
116
|
+
const value = (name) => {
|
|
117
|
+
const match = recommendation.find((flag) => flag.startsWith(`${name}=`));
|
|
118
|
+
if (!match)
|
|
119
|
+
return undefined;
|
|
120
|
+
const number = Number(match.slice(name.length + 1));
|
|
121
|
+
return Number.isFinite(number) ? number : undefined;
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
includeCodebase: has("--include-codebase"),
|
|
125
|
+
includePullRequests: has("--include-pull-requests"),
|
|
126
|
+
includePullRequestChanges: has("--include-pull-request-changes"),
|
|
127
|
+
includeCommitHistory: has("--include-commit-history"),
|
|
128
|
+
includeHowRepoWorks: has("--include-how-repo-works"),
|
|
129
|
+
maxPrMonths: value("--max-pr-months"),
|
|
130
|
+
maxCommits: value("--max-commits"),
|
|
131
|
+
maxPullRequestChangeLines: value("--max-pull-request-change-lines"),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Cron } from "../util/cron.ts";
|
|
2
|
-
/** A
|
|
2
|
+
/** A sync costs model tokens, so a schedule may fire at most once an hour. */
|
|
3
3
|
export declare function parseRemakeCron(expression: string): Cron;
|
|
4
4
|
/** Each minute is handled once however often the ticker is called. A minute
|
|
5
5
|
* the process slept through is not made up. */
|
|
@@ -3,11 +3,11 @@ import { getQueuedJobByKey, getRunningJobByKey } from "../store/jobs.js";
|
|
|
3
3
|
import { listActiveRepos } from "../store/repos.js";
|
|
4
4
|
import { cronMatches, parseCron } from "../util/cron.js";
|
|
5
5
|
import { enqueueSetup } from "./setup.js";
|
|
6
|
-
/** A
|
|
6
|
+
/** A sync costs model tokens, so a schedule may fire at most once an hour. */
|
|
7
7
|
export function parseRemakeCron(expression) {
|
|
8
8
|
const cron = parseCron(expression);
|
|
9
9
|
if (cron.minutes.size !== 1) {
|
|
10
|
-
throw new Error("the minute field must be a single value, so a
|
|
10
|
+
throw new Error("the minute field must be a single value, so a sync runs at most once an hour");
|
|
11
11
|
}
|
|
12
12
|
return cron;
|
|
13
13
|
}
|
|
@@ -51,7 +51,7 @@ export function startRemakeScheduler(intervalMs = 15_000) {
|
|
|
51
51
|
const ticker = createRemakeCronTicker();
|
|
52
52
|
timer = setInterval(() => {
|
|
53
53
|
for (const repo of ticker(new Date())) {
|
|
54
|
-
console.log(`[cron] queued a scheduled
|
|
54
|
+
console.log(`[cron] queued a scheduled sync for ${repo}`);
|
|
55
55
|
}
|
|
56
56
|
}, intervalMs);
|
|
57
57
|
}
|
|
@@ -3,11 +3,13 @@ import { resolvedFromFirstReview, revisionStats, sortResolvedFindings, summaryCo
|
|
|
3
3
|
import { parseFindings } from "../pr/findings.js";
|
|
4
4
|
import { codegraphToolHandlersForBridge } from "../pr/codegraph_tools.js";
|
|
5
5
|
import { reviewWorkspaceRevision } from "../pr/reviewer.js";
|
|
6
|
-
import { buildCarryPromptSection, classifyCarryItems, incrementalDiffPaths, } from "../review/carry_over.js";
|
|
6
|
+
import { buildCarryPromptSection, classifyCarryItems, guideRebuiltSince, incrementalDiffPaths, } from "../review/carry_over.js";
|
|
7
7
|
import { remoteBridgeToolHandlers } from "../remote/tool_bridge.js";
|
|
8
8
|
import { revisionFromSubmitJson } from "../remote/revision_from_submit.js";
|
|
9
9
|
import { closeRemoteSession, openRemoteSession, setRemoteSyncResult, } from "../remote/server/sessions.js";
|
|
10
10
|
import { readConfig } from "../config.js";
|
|
11
|
+
import { reviewBlockingFrom } from "../review/blocking.js";
|
|
12
|
+
import { loadGuides } from "../review/guides.js";
|
|
11
13
|
import { withLogSink } from "../util/log.js";
|
|
12
14
|
import { cancel, registerHandler } from "./jobs.js";
|
|
13
15
|
import { recordAiCost } from "./setup.js";
|
|
@@ -45,6 +47,7 @@ function reviewOptionsForRepo(repo, useCodegraph) {
|
|
|
45
47
|
includeCommitHistory: true,
|
|
46
48
|
includeHowRepoWorks: true,
|
|
47
49
|
onlyRequestChangedPr: false,
|
|
50
|
+
reviewBlocking: reviewBlockingFrom(config.reviewBlocking),
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
53
|
function supersedeActiveRemoteJobs(queueKey, supersededBy) {
|
|
@@ -158,7 +161,12 @@ export function registerRemoteReviewHandler() {
|
|
|
158
161
|
const extras = {};
|
|
159
162
|
if (subjectId) {
|
|
160
163
|
const subjectRevision = getSubjectRevision(subjectId);
|
|
161
|
-
|
|
164
|
+
// A guide rebuilt after the previous review invalidates its verdicts
|
|
165
|
+
// and its "unchanged" suppression (CORE-42 / F03): start fresh so a
|
|
166
|
+
// stale finding cannot mask a new one.
|
|
167
|
+
const guideRebuilt = subjectRevision !== undefined &&
|
|
168
|
+
guideRebuiltSince(subjectRevision.guide_built_at, (await loadGuides(job.repo)).guideBuiltAt);
|
|
169
|
+
if (subjectRevision && !guideRebuilt) {
|
|
162
170
|
const prevFiles = parseRevisionFiles(subjectRevision);
|
|
163
171
|
const { unchanged } = incrementalDiffPaths(revision, prevFiles);
|
|
164
172
|
extras.unchangedPaths = unchanged;
|
|
@@ -207,6 +215,7 @@ export function registerRemoteReviewHandler() {
|
|
|
207
215
|
const filesByPath = new Map(revision.files.map((file) => [file.path, { patch: file.patch }]));
|
|
208
216
|
const findings = sortResolvedFindings(resolvedFromFirstReview(parsed, filesByPath));
|
|
209
217
|
const durationMs = Math.round(performance.now() - started);
|
|
218
|
+
const blockingMode = reviewBlockingFrom(options.reviewBlocking);
|
|
210
219
|
setRemoteSyncResult(job.id, {
|
|
211
220
|
subject: {
|
|
212
221
|
repo: job.repo,
|
|
@@ -215,8 +224,9 @@ export function registerRemoteReviewHandler() {
|
|
|
215
224
|
},
|
|
216
225
|
revision: revisionStats(revision),
|
|
217
226
|
guide: { builtAt: result.guideBuiltAt },
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
codegraph: { state: result.codegraphState },
|
|
228
|
+
summary: summaryCounts(findings, blockingMode),
|
|
229
|
+
findings: findings.map((row) => toJsonFinding(row, blockingMode)),
|
|
220
230
|
usage: {
|
|
221
231
|
tokensIn,
|
|
222
232
|
tokensOut,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ParsedFinding } from "../pr/findings.ts";
|
|
2
2
|
import { type Anchor } from "../pr/hunks.ts";
|
|
3
|
+
import { type ReviewBlocking } from "../review/blocking.ts";
|
|
3
4
|
import { type LogFn } from "./jobs.ts";
|
|
4
5
|
import type { AiProvider, GitHubClient, Json, Options } from "../types.ts";
|
|
5
6
|
import type { JobRow } from "../store/rows.ts";
|
|
@@ -13,6 +14,16 @@ export declare function splitInline(findings: ParsedFinding[], files: Json[]): {
|
|
|
13
14
|
leftover: ParsedFinding[];
|
|
14
15
|
};
|
|
15
16
|
export declare function reviewEvent(findingsCount: number): "REQUEST_CHANGES" | "COMMENT";
|
|
17
|
+
/** The GitHub review event for a set of stored findings (CORE-41).
|
|
18
|
+
*
|
|
19
|
+
* `model` keeps the 0.4.13 behavior: any finding requests changes, because the
|
|
20
|
+
* model's label already decided. `severity` ignores that label and requests
|
|
21
|
+
* changes only for a P0 or P1, so the same finding set always yields the same
|
|
22
|
+
* event. */
|
|
23
|
+
export declare function appReviewEvent(findings: {
|
|
24
|
+
severity: string;
|
|
25
|
+
title: string;
|
|
26
|
+
}[], mode?: ReviewBlocking): "REQUEST_CHANGES" | "COMMENT";
|
|
16
27
|
export interface ReviewMetadata {
|
|
17
28
|
jobId: string;
|
|
18
29
|
model: string;
|
|
@@ -8,11 +8,12 @@ import { parseFindings } from "../pr/findings.js";
|
|
|
8
8
|
import { anchorFor } from "../pr/hunks.js";
|
|
9
9
|
import { stripSuggestion, suggestionAnchor, withoutSuggestionLine, } from "../pr/suggestion.js";
|
|
10
10
|
import { githubPullRequestToRevision } from "../pr/github_revision.js";
|
|
11
|
-
import { anchorTextFromPatch, buildCarryPromptSection, classifyCarryItems, incrementalDiffPaths, parsePreviousVerdicts, resolveCarryOutcomes, } from "../review/carry_over.js";
|
|
11
|
+
import { anchorTextFromPatch, buildCarryPromptSection, classifyCarryItems, guideRebuiltSince, incrementalDiffPaths, parsePreviousVerdicts, resolveCarryOutcomes, } from "../review/carry_over.js";
|
|
12
12
|
import { runReviewEngine } from "../review/engine.js";
|
|
13
13
|
import { loadGuides } from "../review/guides.js";
|
|
14
|
+
import { reviewBlockingFrom, isBlocking, DEFAULT_REVIEW_BLOCKING, } from "../review/blocking.js";
|
|
14
15
|
import { matchRepeat } from "../pr/rounds.js";
|
|
15
|
-
import { readConfig } from "../config.js";
|
|
16
|
+
import { readConfig, resolveAppPrivateKey } from "../config.js";
|
|
16
17
|
import { outsideCode, redact } from "../util/redact.js";
|
|
17
18
|
import { getRepo, setInstallationId } from "../store/repos.js";
|
|
18
19
|
import { getReviewByJobId, insertReview, latestPostedReview, setReviewScope, setReviewStatus, } from "../store/reviews.js";
|
|
@@ -22,6 +23,10 @@ import { enqueue, registerHandler } from "./jobs.js";
|
|
|
22
23
|
import { getEnv } from "../util/runtime.js";
|
|
23
24
|
const ACCESS_DENIED_BODY = "The App does not have access to review this pull request.";
|
|
24
25
|
export function humanCopy(text) {
|
|
26
|
+
// The semicolon is prose and always goes. The em dash is left alone here:
|
|
27
|
+
// this function also sanitizes a finding title, and a title posted before
|
|
28
|
+
// 0.5.0 uses ` — ` as its path/symbol separator, which the legacy heading
|
|
29
|
+
// reader still needs (CORE-83 keeps that one allowlisted exception).
|
|
25
30
|
return outsideCode(redact(text), (prose) => prose.replaceAll(";", "."));
|
|
26
31
|
}
|
|
27
32
|
function hasForbiddenCopy(text) {
|
|
@@ -61,6 +66,20 @@ export function splitInline(findings, files) {
|
|
|
61
66
|
export function reviewEvent(findingsCount) {
|
|
62
67
|
return findingsCount > 0 ? "REQUEST_CHANGES" : "COMMENT";
|
|
63
68
|
}
|
|
69
|
+
/** The GitHub review event for a set of stored findings (CORE-41).
|
|
70
|
+
*
|
|
71
|
+
* `model` keeps the 0.4.13 behavior: any finding requests changes, because the
|
|
72
|
+
* model's label already decided. `severity` ignores that label and requests
|
|
73
|
+
* changes only for a P0 or P1, so the same finding set always yields the same
|
|
74
|
+
* event. */
|
|
75
|
+
export function appReviewEvent(findings, mode = DEFAULT_REVIEW_BLOCKING) {
|
|
76
|
+
if (findings.length === 0)
|
|
77
|
+
return "COMMENT";
|
|
78
|
+
if (mode === "model")
|
|
79
|
+
return "REQUEST_CHANGES";
|
|
80
|
+
const blocking = findings.some((row) => isBlocking(mode, { severity: row.severity, text: row.title }));
|
|
81
|
+
return reviewEvent(blocking ? 1 : 0);
|
|
82
|
+
}
|
|
64
83
|
export function formatDuration(ms) {
|
|
65
84
|
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
66
85
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -132,6 +151,7 @@ export function reviewOptions(repo, prNumber) {
|
|
|
132
151
|
includeHowRepoWorks: true,
|
|
133
152
|
onlyRequestChangedPr: false,
|
|
134
153
|
useCodegraph: getRepo(repo)?.use_codegraph === 1,
|
|
154
|
+
reviewBlocking: reviewBlockingFrom(config.reviewBlocking),
|
|
135
155
|
};
|
|
136
156
|
}
|
|
137
157
|
export function aiFor(options) {
|
|
@@ -145,19 +165,21 @@ export function aiFor(options) {
|
|
|
145
165
|
}
|
|
146
166
|
export function clientFor(installationId) {
|
|
147
167
|
const config = readConfig();
|
|
148
|
-
|
|
168
|
+
const privateKeyPem = resolveAppPrivateKey(config);
|
|
169
|
+
if (!config.githubAppId || !privateKeyPem) {
|
|
149
170
|
throw new Error("review jobs need the GitHub App configured");
|
|
150
171
|
}
|
|
151
|
-
return new AppClient(config.githubAppId,
|
|
172
|
+
return new AppClient(config.githubAppId, privateKeyPem, installationId);
|
|
152
173
|
}
|
|
153
174
|
export async function resolveInstallationId(fullName) {
|
|
154
175
|
const current = getRepo(fullName)?.installation_id;
|
|
155
176
|
if (current)
|
|
156
177
|
return current;
|
|
157
178
|
const config = readConfig();
|
|
158
|
-
|
|
179
|
+
const privateKeyPem = resolveAppPrivateKey(config);
|
|
180
|
+
if (!config.githubAppId || !privateKeyPem)
|
|
159
181
|
return undefined;
|
|
160
|
-
const installationId = await findInstallationForRepo(config.githubAppId,
|
|
182
|
+
const installationId = await findInstallationForRepo(config.githubAppId, privateKeyPem, fullName);
|
|
161
183
|
if (installationId !== undefined)
|
|
162
184
|
setInstallationId(fullName, installationId);
|
|
163
185
|
return installationId;
|
|
@@ -244,7 +266,7 @@ async function finishCheck(client, job, checkRunId, conclusion, title, findings,
|
|
|
244
266
|
},
|
|
245
267
|
}, log);
|
|
246
268
|
}
|
|
247
|
-
async function publish(client, job, reviewId, headSha, files, log, metadata) {
|
|
269
|
+
async function publish(client, job, reviewId, headSha, files, log, metadata, mode = DEFAULT_REVIEW_BLOCKING) {
|
|
248
270
|
const stored = listFindingsForReview(reviewId);
|
|
249
271
|
const replies = stored.filter((row) => row.thread_comment_id);
|
|
250
272
|
const freshRows = stored.filter((row) => row.state === "new" && !row.thread_comment_id);
|
|
@@ -314,7 +336,7 @@ async function publish(client, job, reviewId, headSha, files, log, metadata) {
|
|
|
314
336
|
const payload = {
|
|
315
337
|
commit_id: headSha,
|
|
316
338
|
body,
|
|
317
|
-
event:
|
|
339
|
+
event: appReviewEvent(stored, mode),
|
|
318
340
|
comments,
|
|
319
341
|
};
|
|
320
342
|
setReviewStatus(reviewId, "posting");
|
|
@@ -523,7 +545,13 @@ async function runReviewJobCore(job, log, client, ai) {
|
|
|
523
545
|
let carryItems = [];
|
|
524
546
|
let carryPrevious = null;
|
|
525
547
|
const extras = {};
|
|
526
|
-
|
|
548
|
+
// A guide rebuilt after the previous review invalidates that review's
|
|
549
|
+
// verdicts and its "unchanged" suppression (CORE-42 / F03). Treat the run as
|
|
550
|
+
// if it had started fresh: carry-over off, every changed file back in scope,
|
|
551
|
+
// so a stale finding can never mask a new one.
|
|
552
|
+
const guideRebuilt = subjectRevision !== undefined &&
|
|
553
|
+
guideRebuiltSince(subjectRevision.guide_built_at, guidesBefore.guideBuiltAt);
|
|
554
|
+
if (subjectRevision && !guideRebuilt) {
|
|
527
555
|
const prevFiles = parseRevisionFiles(subjectRevision);
|
|
528
556
|
const { unchanged } = incrementalDiffPaths(revision, prevFiles);
|
|
529
557
|
extras.unchangedPaths = scope === "incremental" ? unchanged : undefined;
|
|
@@ -564,7 +592,7 @@ async function runReviewJobCore(job, log, client, ai) {
|
|
|
564
592
|
row.posted_comment_id,
|
|
565
593
|
]));
|
|
566
594
|
const verdicts = parsePreviousVerdicts(response.text);
|
|
567
|
-
const resolved = resolveCarryOutcomes(carryItems, revision, visiblePaths, verdicts, parsed
|
|
595
|
+
const resolved = resolveCarryOutcomes(carryItems, revision, visiblePaths, verdicts, parsed);
|
|
568
596
|
totalFindings = resolved.length;
|
|
569
597
|
for (const row of resolved) {
|
|
570
598
|
if (row.state === "open")
|
|
@@ -636,7 +664,7 @@ async function runReviewJobCore(job, log, client, ai) {
|
|
|
636
664
|
jobId: job.id,
|
|
637
665
|
model: options.highModel ?? "unknown",
|
|
638
666
|
durationMs: Date.now() - startedAt,
|
|
639
|
-
});
|
|
667
|
+
}, options.reviewBlocking);
|
|
640
668
|
await finishCheck(github, job, checkRunId, totalFindings === 0 ? "success" : "neutral", totalFindings === 0 ? "Review completed" : "Review findings", listFindingsForReview(reviewId), log);
|
|
641
669
|
}
|
|
642
670
|
export function registerReviewJobHandler() {
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
* directly; it used to re-spawn the CLI as a subprocess, a second
|
|
3
3
|
* execution path with its own failure modes. */
|
|
4
4
|
import { createAiProvider } from "../ai/provider.js";
|
|
5
|
-
import {
|
|
5
|
+
import { enrichFactsWithReport, synthesizeSections, } from "../knowledge/synthesis.js";
|
|
6
6
|
import { collectSource } from "../github/collect.js";
|
|
7
7
|
import { GhClient } from "../github/gh.js";
|
|
8
8
|
import { PatClient } from "../github/pat.js";
|
|
9
9
|
import { extractFacts } from "../knowledge/facts.js";
|
|
10
|
-
import { buildReviewDocuments } from "../knowledge/guide.js";
|
|
11
|
-
import { assembleSkill,
|
|
10
|
+
import { buildReviewDocuments, reviewSignalCount } from "../knowledge/guide.js";
|
|
11
|
+
import { assembleSkill, codebaseBody, factSectionHashes, } from "../knowledge/skill.js";
|
|
12
12
|
import { validateSkill } from "../knowledge/validate.js";
|
|
13
13
|
import { readConfig, reposDir, writeRepoConfig } from "../config.js";
|
|
14
14
|
import { readState, writeState } from "../store/skill_state.js";
|
|
15
15
|
import { cacheSet } from "../store/cache_db.js";
|
|
16
16
|
import { log, timed, withLogSink } from "../util/log.js";
|
|
17
|
+
import { printRunSummary, summaryFromMetrics } from "../util/run_summary.js";
|
|
17
18
|
import { enqueue, registerHandler } from "./jobs.js";
|
|
18
19
|
import { closeAppDb, isAppDbOpen, openAppDb } from "../store/app_db.js";
|
|
19
20
|
import { getRepo, markKnowledgeBuilt } from "../store/repos.js";
|
|
@@ -72,9 +73,12 @@ async function skillPath(repo) {
|
|
|
72
73
|
await mkdir(`${reposDir()}/${repo}`, { recursive: true });
|
|
73
74
|
return path;
|
|
74
75
|
}
|
|
75
|
-
async function writeReviewDocuments(repo, documents) {
|
|
76
|
+
async function writeReviewDocuments(repo, documents, signalCount) {
|
|
76
77
|
const directory = `${reposDir()}/${repo}`;
|
|
77
78
|
if (!documents) {
|
|
79
|
+
// F05: this used to delete the files without a word. The plan's rule is
|
|
80
|
+
// "says why the third file is missing": keep it to one line.
|
|
81
|
+
log("write", `PR_REVIEW_GUIDE.md skipped: found ${signalCount} review signals, needs at least 3`);
|
|
78
82
|
await Promise.all([
|
|
79
83
|
remove(`${directory}/PR_REVIEW_GUIDE.md`).catch(() => { }),
|
|
80
84
|
remove(`${directory}/PR_REVIEW_DETAILED_GUIDE.md`).catch(() => { }),
|
|
@@ -93,14 +97,12 @@ async function writeReviewDocuments(repo, documents) {
|
|
|
93
97
|
/** Splits the codebase-description sections (layout/style/tests/devloop) out
|
|
94
98
|
* of the assembled skill into their own file, so `review` can check a pull
|
|
95
99
|
* request against how this repository's code actually looks, not just the
|
|
96
|
-
* review-bar checklist mined from past PR comments.
|
|
97
|
-
|
|
100
|
+
* review-bar checklist mined from past PR comments. The skill links to this
|
|
101
|
+
* file rather than repeating it (CORE-32 / F26c), so its body is built from
|
|
102
|
+
* the facts instead of being scraped back out of the skill. */
|
|
103
|
+
async function writeCodebaseDocument(repo, facts, overrides = {}) {
|
|
98
104
|
const path = `${reposDir()}/${repo}/CODEBASE.md`;
|
|
99
|
-
const
|
|
100
|
-
const body = ["layout", "style", "tests", "devloop"]
|
|
101
|
-
.map((key) => sections[key])
|
|
102
|
-
.filter(Boolean)
|
|
103
|
-
.join("\n\n");
|
|
105
|
+
const body = codebaseBody(facts, overrides);
|
|
104
106
|
if (!body) {
|
|
105
107
|
await remove(path).catch(() => { });
|
|
106
108
|
return;
|
|
@@ -133,7 +135,7 @@ async function initOrRemake(options) {
|
|
|
133
135
|
const aiMetrics = emptyAiMetrics();
|
|
134
136
|
const previous = await readState(options.repo);
|
|
135
137
|
if (options.command === "remake" && !previous) {
|
|
136
|
-
throw new Error("
|
|
138
|
+
throw new Error("sync requires a previous init or sync for this repository");
|
|
137
139
|
}
|
|
138
140
|
if (options.command === "remake" && previous) {
|
|
139
141
|
options.maxCommits ??= previous.options.maxCommits;
|
|
@@ -182,6 +184,7 @@ async function initOrRemake(options) {
|
|
|
182
184
|
log("time", `extract deterministic facts · ${((performance.now() - factsStarted) / 1000).toFixed(2)}s`);
|
|
183
185
|
}
|
|
184
186
|
let overrides = {};
|
|
187
|
+
const skipped = [];
|
|
185
188
|
if (lowAi) {
|
|
186
189
|
log("ai", "starting extract_unit jobs");
|
|
187
190
|
const usage = async (job, response) => {
|
|
@@ -196,7 +199,9 @@ async function initOrRemake(options) {
|
|
|
196
199
|
aiMetrics.cost += response.cost;
|
|
197
200
|
await recordAiCost(options.repo, job, response);
|
|
198
201
|
};
|
|
199
|
-
|
|
202
|
+
const enriched = await timed("extract_unit AI", options.logTime, () => enrichFactsWithReport(lowAi, options.repo, facts, source, options, usage));
|
|
203
|
+
facts = enriched.facts;
|
|
204
|
+
skipped.push(...enriched.skipped);
|
|
200
205
|
log("ai", `extract_unit complete · ${facts.length} facts`);
|
|
201
206
|
const hashes = await factSectionHashes(facts);
|
|
202
207
|
const synthesisChanged = previous &&
|
|
@@ -211,7 +216,9 @@ async function initOrRemake(options) {
|
|
|
211
216
|
: undefined;
|
|
212
217
|
log("ai", `starting synth_section jobs${dirtySections ? ` · ${dirtySections.size} dirty sections` : ""}`);
|
|
213
218
|
if (highAi) {
|
|
214
|
-
|
|
219
|
+
const synth = await timed("synth_section AI", options.logTime, () => synthesizeSections(highAi, options.repo, facts, previousMarkdown, options.ai, options.highModel ?? "", options.aiConcurrent, usage, dirtySections));
|
|
220
|
+
overrides = synth.overrides;
|
|
221
|
+
skipped.push(...synth.skipped);
|
|
215
222
|
}
|
|
216
223
|
log("ai", `synth_section complete · ${Object.keys(overrides).length} sections`);
|
|
217
224
|
}
|
|
@@ -237,8 +244,8 @@ async function initOrRemake(options) {
|
|
|
237
244
|
const { writeTextFileAtomic } = await import("../util/atomic.js");
|
|
238
245
|
await withKnowledgeLock(options.repo, async () => {
|
|
239
246
|
await writeTextFileAtomic(path, result.markdown);
|
|
240
|
-
await writeCodebaseDocument(options.repo,
|
|
241
|
-
await writeReviewDocuments(options.repo, reviewDocuments);
|
|
247
|
+
await writeCodebaseDocument(options.repo, facts, overrides);
|
|
248
|
+
await writeReviewDocuments(options.repo, reviewDocuments, reviewSignalCount(facts));
|
|
242
249
|
const head = source.commits[0];
|
|
243
250
|
const baseSha = head?.sha ? String(head.sha) : new Date().toISOString();
|
|
244
251
|
markKnowledgeBuilt(options.repo, baseSha);
|
|
@@ -257,7 +264,7 @@ async function initOrRemake(options) {
|
|
|
257
264
|
},
|
|
258
265
|
updatedAt: new Date().toISOString(),
|
|
259
266
|
});
|
|
260
|
-
// Remember everything but the token, so `
|
|
267
|
+
// Remember everything but the token, so `sync owner/repo` alone
|
|
261
268
|
// (no flags, no prompts) reuses what this run resolved.
|
|
262
269
|
await writeRepoConfig(options.repo, {
|
|
263
270
|
auth: options.auth,
|
|
@@ -281,9 +288,20 @@ async function initOrRemake(options) {
|
|
|
281
288
|
? `updated ${result.changed.join(", ")}`
|
|
282
289
|
: "already current"}`);
|
|
283
290
|
log("done", `${facts.length} facts · ${source.pullRequests.length} pull requests · ${source.commits.length} commits`);
|
|
291
|
+
if (skipped.length) {
|
|
292
|
+
// F04: a skipped unit used to vanish into a log line and recur as a "cache
|
|
293
|
+
// hit" forever. Say it in the final line instead, in the plan's shape:
|
|
294
|
+
// `1 unit skipped (PR #3: output was not JSON)`.
|
|
295
|
+
const parts = skipped.map((unit) => unit.reason);
|
|
296
|
+
const line = skipped.length === 1
|
|
297
|
+
? `1 unit skipped (${parts[0]})`
|
|
298
|
+
: `${skipped.length} units skipped (${parts.join("; ")})`;
|
|
299
|
+
log("done", line);
|
|
300
|
+
}
|
|
301
|
+
printRunSummary(summaryFromMetrics(aiMetrics, performance.now() - operationStarted));
|
|
284
302
|
if (options.logTime) {
|
|
285
303
|
log("time", `AI total · calls=${aiMetrics.calls} · input=${aiMetrics.tokensIn} tokens · output=${aiMetrics.tokensOut} tokens · cost=${aiMetrics.costKnown ? aiMetrics.cost.toFixed(4) : "unknown"}`);
|
|
286
|
-
log("time", `total init/
|
|
304
|
+
log("time", `total init/sync · ${((performance.now() - operationStarted) / 1000).toFixed(2)}s`);
|
|
287
305
|
}
|
|
288
306
|
}
|
|
289
307
|
/** Builds `Options` for a server-triggered `init`/`remake` from config
|
|
@@ -299,7 +317,7 @@ export function optionsFromConfig(repo, command) {
|
|
|
299
317
|
const lowModel = repoConfig.lowModel ?? config.lowModel;
|
|
300
318
|
const highModel = repoConfig.highModel ?? config.highModel;
|
|
301
319
|
if (ai !== "none" && (!config.token || !lowModel || !highModel)) {
|
|
302
|
-
throw new Error("AI is enabled but token/low-model/high-model are not fully configured
|
|
320
|
+
throw new Error("AI is enabled but token/low-model/high-model are not fully configured. Run: " +
|
|
303
321
|
"co-maintainer set --token=... --low-model=... --high-model=...");
|
|
304
322
|
}
|
|
305
323
|
return {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SetupItem = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
detail: string;
|
|
5
|
+
done: boolean;
|
|
6
|
+
/** Where the fix lives. In-page anchors on /settings where one exists. */
|
|
7
|
+
href: string;
|
|
8
|
+
linkLabel: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function setupChecklist(webhookUrl: string): SetupItem[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** The "Finish setup" checklist shown on the home page (CORE-75, C01/F26).
|
|
2
|
+
*
|
|
3
|
+
* Each item is derived from state that already exists, so the card reflects
|
|
4
|
+
* what serve can actually do rather than a parallel bookkeeping of it. The
|
|
5
|
+
* webhook item reuses CORE-71's reachability check. */
|
|
6
|
+
import { readConfig, resolveAppPrivateKey } from "../config.js";
|
|
7
|
+
import { countReviews } from "../store/reviews.js";
|
|
8
|
+
import { listRemoteTokens } from "../store/remote_tokens.js";
|
|
9
|
+
import { listActiveRepos } from "../store/repos.js";
|
|
10
|
+
import { lastDelivery } from "../store/deliveries.js";
|
|
11
|
+
import { webhookReachabilityProblem } from "../util/webhook_reachability.js";
|
|
12
|
+
export function setupChecklist(webhookUrl) {
|
|
13
|
+
const config = readConfig();
|
|
14
|
+
const aiDone = Boolean(config.ai && config.ai !== "none" && config.token);
|
|
15
|
+
const appDone = Boolean(config.githubAppId && resolveAppPrivateKey(config));
|
|
16
|
+
const repoCount = listActiveRepos().length;
|
|
17
|
+
const reviewCount = countReviews();
|
|
18
|
+
const tokenCount = listRemoteTokens().length;
|
|
19
|
+
const reach = webhookReachabilityProblem(webhookUrl);
|
|
20
|
+
const reached = reach === undefined && lastDelivery() !== undefined;
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
id: "ai",
|
|
24
|
+
title: "Models and API key",
|
|
25
|
+
detail: aiDone
|
|
26
|
+
? "An AI provider and key are saved."
|
|
27
|
+
: "Add a provider and API key so reviews and guides can be written.",
|
|
28
|
+
done: aiDone,
|
|
29
|
+
href: "/settings#ai",
|
|
30
|
+
linkLabel: "Open models",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "app",
|
|
34
|
+
title: "GitHub App",
|
|
35
|
+
detail: appDone
|
|
36
|
+
? "The App is configured."
|
|
37
|
+
: "Create and install the App so reviews can be posted to pull requests.",
|
|
38
|
+
done: appDone,
|
|
39
|
+
href: "/settings#github",
|
|
40
|
+
linkLabel: "Open GitHub",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "webhook",
|
|
44
|
+
title: "Webhook reachable",
|
|
45
|
+
detail: !appDone
|
|
46
|
+
? "Set up the GitHub App first, then point its webhook at this server."
|
|
47
|
+
: reached
|
|
48
|
+
? "GitHub has delivered a webhook to this server."
|
|
49
|
+
: reach
|
|
50
|
+
? `GitHub cannot reach ${webhookUrl}. ${reach}`
|
|
51
|
+
: "Waiting for the first delivery. Open a pull request to confirm.",
|
|
52
|
+
done: reached,
|
|
53
|
+
href: "/settings#github",
|
|
54
|
+
linkLabel: "Open webhook",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "repo",
|
|
58
|
+
title: "First repository",
|
|
59
|
+
detail: repoCount === 0
|
|
60
|
+
? "Add a repository to generate its guides."
|
|
61
|
+
: `${repoCount} ${repoCount === 1 ? "repository" : "repositories"} added.`,
|
|
62
|
+
done: repoCount > 0,
|
|
63
|
+
href: "/repos/new",
|
|
64
|
+
linkLabel: "Add repository",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "review",
|
|
68
|
+
title: "First review",
|
|
69
|
+
detail: reviewCount === 0
|
|
70
|
+
? "A review appears after the first pull request, or run one by number."
|
|
71
|
+
: `${reviewCount} ${reviewCount === 1 ? "review" : "reviews"} so far.`,
|
|
72
|
+
done: reviewCount > 0,
|
|
73
|
+
href: "/activity",
|
|
74
|
+
linkLabel: "Open activity",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "cli",
|
|
78
|
+
title: "CLI connected",
|
|
79
|
+
detail: tokenCount === 0
|
|
80
|
+
? "Create a remote token to run co-maintainer review --remote."
|
|
81
|
+
: `${tokenCount} remote ${tokenCount === 1 ? "token" : "tokens"} created.`,
|
|
82
|
+
done: tokenCount > 0,
|
|
83
|
+
href: "/settings#remote",
|
|
84
|
+
linkLabel: "Open tokens",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
package/dist/src/store/app_db.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Database } from "./sqlite.js";
|
|
2
2
|
import { getCacheDir } from "../config.js";
|
|
3
|
+
import { CliError, EXIT_USAGE } from "../cli/error.js";
|
|
3
4
|
import { migrations } from "./migrations.js";
|
|
4
5
|
import { getEnv, isNotFound, isProcessAlive, mkdir, readTextFile, remove, writeTextFile, } from "../util/runtime.js";
|
|
5
6
|
export function appDbDir() {
|
|
@@ -27,7 +28,9 @@ async function acquireLock() {
|
|
|
27
28
|
if (existingPid !== undefined &&
|
|
28
29
|
Number.isInteger(existingPid) &&
|
|
29
30
|
isProcessAlive(existingPid)) {
|
|
30
|
-
|
|
31
|
+
// Says what is holding the database, what to do about it, and offers the
|
|
32
|
+
// path that does not need another process. CORE-12.
|
|
33
|
+
throw new CliError("serve_already_running", `A co-maintainer serve process (pid ${existingPid}) is using this data directory.`, "Stop it, or run sync from its dashboard.", EXIT_USAGE);
|
|
31
34
|
}
|
|
32
35
|
await writeTextFile(path, String(process.pid));
|
|
33
36
|
}
|
|
@@ -79,7 +82,7 @@ export async function openAppDb() {
|
|
|
79
82
|
}
|
|
80
83
|
export function getAppDb() {
|
|
81
84
|
if (!db)
|
|
82
|
-
throw new Error("app.db is not open
|
|
85
|
+
throw new Error("app.db is not open. Call openAppDb() first");
|
|
83
86
|
return db;
|
|
84
87
|
}
|
|
85
88
|
export async function closeAppDb() {
|