@fullstackhouse/cerber 0.7.0 → 0.9.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/README.md +141 -6
- package/dist/cli/index.js +146 -13
- package/dist/core/artifact.js +4 -0
- package/dist/core/checkout.js +201 -0
- package/dist/core/config.js +100 -0
- package/dist/core/gh.js +26 -1
- package/dist/core/state.js +15 -0
- package/dist/core/trust.js +91 -0
- package/dist/runner/claude.js +84 -4
- package/dist/runner/prompt.js +31 -4
- package/dist/runner/review.js +122 -20
- package/dist/server/daemon.js +209 -46
- package/dist/server/index.js +113 -4
- package/package.json +1 -1
- package/web/dist/assets/index-CsWW0EBv.js +236 -0
- package/web/dist/assets/{index-CwjpyotK.css → index-Dmz5lQnG.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Bq-5Sr6-.js +0 -236
package/dist/server/daemon.js
CHANGED
|
@@ -1,13 +1,63 @@
|
|
|
1
|
-
import { artifactKey } from "../core/artifact.js";
|
|
1
|
+
import { SCHEMA_VERSION, artifactId, artifactKey } from "../core/artifact.js";
|
|
2
2
|
import { evaluateAutoSend } from "../core/autosend.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadConfig } from "../core/config.js";
|
|
4
|
+
import { fetchPrInfo, searchAwaitingMe, submitReview } from "../core/gh.js";
|
|
4
5
|
import { buildReviewPayload, computeCalibration } from "../core/send.js";
|
|
5
|
-
import { appendAutoSendLog, updateArtifactByKey } from "../core/state.js";
|
|
6
|
+
import { appendAutoSendLog, deleteArtifact, listArtifacts, loadArtifact, saveArtifact, updateArtifactByKey, } from "../core/state.js";
|
|
6
7
|
import { ReviewInProgressError } from "../runner/inflight.js";
|
|
7
8
|
import { pool, reviewPr } from "../runner/review.js";
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* A pure discovery stub: enough artifact for the queue to show an awaiting row.
|
|
11
|
+
* The AI run that follows (automatic or clicked) replaces it wholesale.
|
|
12
|
+
*/
|
|
13
|
+
export function stubArtifact(ref) {
|
|
14
|
+
const now = new Date().toISOString();
|
|
15
|
+
return {
|
|
16
|
+
schemaVersion: SCHEMA_VERSION,
|
|
17
|
+
id: artifactId(ref),
|
|
18
|
+
status: "awaiting",
|
|
19
|
+
createdAt: now,
|
|
20
|
+
updatedAt: now,
|
|
21
|
+
pr: {
|
|
22
|
+
owner: ref.owner,
|
|
23
|
+
repo: ref.repo,
|
|
24
|
+
number: ref.number,
|
|
25
|
+
title: ref.title,
|
|
26
|
+
url: ref.url,
|
|
27
|
+
author: ref.author,
|
|
28
|
+
body: "",
|
|
29
|
+
baseRefName: "",
|
|
30
|
+
headRefName: "",
|
|
31
|
+
headSha: "",
|
|
32
|
+
state: "OPEN",
|
|
33
|
+
additions: 0,
|
|
34
|
+
deletions: 0,
|
|
35
|
+
changedFiles: 0,
|
|
36
|
+
},
|
|
37
|
+
diff: "",
|
|
38
|
+
summary: "",
|
|
39
|
+
chapters: [],
|
|
40
|
+
comments: [],
|
|
41
|
+
verdict: null,
|
|
42
|
+
run: null,
|
|
43
|
+
sent: null,
|
|
44
|
+
refresh: null,
|
|
45
|
+
calibration: null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** An artifact nothing has touched yet — safe to delete when its PR leaves the queue. */
|
|
49
|
+
export function isPureStub(artifact) {
|
|
50
|
+
return artifact.status === "awaiting" && artifact.comments.length === 0 && artifact.run === null;
|
|
51
|
+
}
|
|
52
|
+
/** How many open-PR artifacts to re-check against GitHub per poll, and how often each. */
|
|
53
|
+
const STATE_REFRESH_CAP = 10;
|
|
54
|
+
const STATE_REFRESH_MIN_MS = 30 * 60_000;
|
|
55
|
+
/**
|
|
56
|
+
* The inbox loop behind `cerber serve`: poll GitHub for PRs awaiting the
|
|
57
|
+
* user's review, list them as `awaiting`, and (unless auto-review is off)
|
|
58
|
+
* draft a review for each. Merged/closed PRs are archived out of the queue.
|
|
59
|
+
* Reviews stay local; the daemon NEVER sends anything except via the
|
|
60
|
+
* explicitly opted-in auto-send path.
|
|
11
61
|
*/
|
|
12
62
|
export function startDaemon(opts) {
|
|
13
63
|
let timer = null;
|
|
@@ -17,68 +67,181 @@ export function startDaemon(opts) {
|
|
|
17
67
|
polling: false,
|
|
18
68
|
repos: opts.repos,
|
|
19
69
|
intervalMs: opts.intervalMs,
|
|
70
|
+
pollEnabled: true,
|
|
71
|
+
autoReview: opts.autoReview,
|
|
72
|
+
trustedRuns: false,
|
|
20
73
|
polls: 0,
|
|
21
74
|
errors: 0,
|
|
22
75
|
lastPollAt: null,
|
|
23
76
|
nextPollAt: new Date(Date.now()).toISOString(),
|
|
24
77
|
lastSummary: null,
|
|
78
|
+
lastPollError: null,
|
|
25
79
|
autoSend: opts.autoSend,
|
|
26
80
|
autoSendThreshold: opts.autoSendThreshold,
|
|
27
81
|
autoSent: 0,
|
|
28
82
|
autoSendCandidates: 0,
|
|
29
83
|
};
|
|
30
84
|
const log = (m) => console.log(`[daemon] ${m}`);
|
|
85
|
+
/** When GitHub last confirmed an artifact's PR state, by artifact id. In-memory:
|
|
86
|
+
* a restart just re-checks sooner, which is harmless. */
|
|
87
|
+
const stateCheckedAt = new Map();
|
|
88
|
+
async function discover() {
|
|
89
|
+
const filters = opts.repos.length > 0 ? opts.repos : [undefined];
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
return (await Promise.all(filters.map((repo) => searchAwaitingMe(repo))))
|
|
92
|
+
.flat()
|
|
93
|
+
.filter((r) => {
|
|
94
|
+
const id = artifactId(r);
|
|
95
|
+
if (seen.has(id))
|
|
96
|
+
return false;
|
|
97
|
+
seen.add(id);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Make the local queue mirror what's actually awaiting: stub artifacts for
|
|
103
|
+
* newly discovered PRs, archive merged/closed ones, drop stubs whose review
|
|
104
|
+
* request went away. Freshness decisions always come from local artifacts —
|
|
105
|
+
* search results lag GitHub by minutes and never override them.
|
|
106
|
+
*/
|
|
107
|
+
async function syncQueue(refs) {
|
|
108
|
+
let discovered = 0;
|
|
109
|
+
for (const ref of refs) {
|
|
110
|
+
if (await loadArtifact(artifactId(ref)))
|
|
111
|
+
continue;
|
|
112
|
+
await saveArtifact(stubArtifact(ref));
|
|
113
|
+
discovered++;
|
|
114
|
+
}
|
|
115
|
+
const awaitingIds = new Set(refs.map((r) => artifactId(r)));
|
|
116
|
+
let refreshed = 0;
|
|
117
|
+
for (const artifact of await listArtifacts()) {
|
|
118
|
+
if (artifact.status === "running")
|
|
119
|
+
continue;
|
|
120
|
+
if (awaitingIds.has(artifact.id))
|
|
121
|
+
continue;
|
|
122
|
+
if (artifact.pr.state !== "OPEN")
|
|
123
|
+
continue;
|
|
124
|
+
if (isPureStub(artifact)) {
|
|
125
|
+
// Discovered earlier, gone from the search now: merged/closed (archive
|
|
126
|
+
// it), or the request was withdrawn / handled elsewhere (drop it — it
|
|
127
|
+
// holds no work, and search lag at worst re-creates it next poll).
|
|
128
|
+
try {
|
|
129
|
+
const pr = await fetchPrInfo(artifact.pr);
|
|
130
|
+
if (pr.state !== "OPEN") {
|
|
131
|
+
await updateArtifactByKey(artifactKey(artifact.id), (a) => ({ ...a, pr }));
|
|
132
|
+
log(`[${artifact.id}] ${pr.state.toLowerCase()} — archived`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
await deleteArtifact(artifact.id);
|
|
136
|
+
log(`[${artifact.id}] no longer awaiting your review — removed from the queue`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// GitHub unreachable — leave the stub; next poll retries.
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Anything with real work in it: periodically confirm the PR is still
|
|
145
|
+
// open, so merged/closed ones stop cluttering the queue. Capped and
|
|
146
|
+
// spaced out — this is book-keeping, not a data feed.
|
|
147
|
+
if (refreshed >= STATE_REFRESH_CAP)
|
|
148
|
+
continue;
|
|
149
|
+
const last = stateCheckedAt.get(artifact.id) ?? 0;
|
|
150
|
+
if (Date.now() - last < STATE_REFRESH_MIN_MS)
|
|
151
|
+
continue;
|
|
152
|
+
refreshed++;
|
|
153
|
+
stateCheckedAt.set(artifact.id, Date.now());
|
|
154
|
+
try {
|
|
155
|
+
const pr = await fetchPrInfo(artifact.pr);
|
|
156
|
+
if (pr.state !== "OPEN") {
|
|
157
|
+
await updateArtifactByKey(artifactKey(artifact.id), (a) => ({
|
|
158
|
+
...a,
|
|
159
|
+
pr: { ...a.pr, state: pr.state },
|
|
160
|
+
}));
|
|
161
|
+
log(`[${artifact.id}] ${pr.state.toLowerCase()} — archived`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Same: book-keeping can wait for the next poll.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { discovered };
|
|
169
|
+
}
|
|
170
|
+
async function reviewAll(refs) {
|
|
171
|
+
let reviewed = 0;
|
|
172
|
+
let skipped = 0;
|
|
173
|
+
let failed = 0;
|
|
174
|
+
await pool(refs, opts.parallel, async (ref) => {
|
|
175
|
+
const label = artifactId(ref);
|
|
176
|
+
try {
|
|
177
|
+
const result = await reviewPr(ref, {
|
|
178
|
+
model: opts.model,
|
|
179
|
+
withSource: opts.withSource,
|
|
180
|
+
trust: opts.trust,
|
|
181
|
+
onProgress: (m) => log(`[${label}] ${m}`),
|
|
182
|
+
});
|
|
183
|
+
if (result.skipped)
|
|
184
|
+
skipped++;
|
|
185
|
+
else {
|
|
186
|
+
reviewed++;
|
|
187
|
+
await handleAutoSend(result.artifact);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof ReviewInProgressError) {
|
|
192
|
+
// The cockpit (or an earlier, slower poll) is already on it.
|
|
193
|
+
skipped++;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
failed++;
|
|
197
|
+
status.errors++;
|
|
198
|
+
log(`[${label}] failed: ${err instanceof Error ? err.message : err}`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return { reviewed, skipped, failed };
|
|
202
|
+
}
|
|
31
203
|
async function poll() {
|
|
32
204
|
if (status.polling)
|
|
33
205
|
return;
|
|
34
206
|
status.polling = true;
|
|
35
207
|
status.lastPollAt = new Date().toISOString();
|
|
36
208
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// The cockpit (or an earlier, slower poll) is already on it.
|
|
68
|
-
skipped++;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
failed++;
|
|
72
|
-
status.errors++;
|
|
73
|
-
log(`[${label}] failed: ${err instanceof Error ? err.message : err}`);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
status.lastSummary = `${refs.length} awaiting; ${reviewed} reviewed, ${skipped} up to date${failed > 0 ? `, ${failed} failed` : ""}`;
|
|
209
|
+
// The config is re-read every poll, so the cockpit's Settings toggles
|
|
210
|
+
// take effect on the next tick — no restart. CLI --no-* flags cap it.
|
|
211
|
+
// loadConfig is strict on purpose (a typo must not silently change trust
|
|
212
|
+
// or the knobs), so a broken file skips the tick loudly instead of
|
|
213
|
+
// proceeding on guessed defaults.
|
|
214
|
+
const config = await loadConfig();
|
|
215
|
+
const knobs = config.daemon;
|
|
216
|
+
const autoReview = opts.autoReview && knobs.autoReview;
|
|
217
|
+
status.pollEnabled = knobs.poll;
|
|
218
|
+
status.autoReview = autoReview;
|
|
219
|
+
// Recomputed too, so a trust rule added mid-run shows up in the strip.
|
|
220
|
+
status.trustedRuns = autoReview && opts.trust !== false && config.trust.length > 0;
|
|
221
|
+
if (!knobs.poll) {
|
|
222
|
+
// Deliberately off is not an outage — don't leave a stale error up.
|
|
223
|
+
status.lastPollError = null;
|
|
224
|
+
status.lastSummary = "polling off (settings) — the queue only shows what you review by hand";
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const refs = await discover();
|
|
228
|
+
status.lastPollError = null;
|
|
229
|
+
const { discovered } = await syncQueue(refs);
|
|
230
|
+
if (autoReview) {
|
|
231
|
+
const { reviewed, skipped, failed } = await reviewAll(refs);
|
|
232
|
+
status.lastSummary =
|
|
233
|
+
`${refs.length} awaiting; ${reviewed} reviewed, ${skipped} up to date` +
|
|
234
|
+
`${failed > 0 ? `, ${failed} failed` : ""}`;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
status.lastSummary = `${refs.length} awaiting${discovered > 0 ? `, ${discovered} new` : ""} (auto-review off)`;
|
|
238
|
+
}
|
|
77
239
|
log(status.lastSummary);
|
|
78
240
|
}
|
|
79
241
|
catch (err) {
|
|
80
242
|
status.errors++;
|
|
81
|
-
status.
|
|
243
|
+
status.lastPollError = err instanceof Error ? err.message : String(err);
|
|
244
|
+
status.lastSummary = `poll failed: ${status.lastPollError}`;
|
|
82
245
|
log(status.lastSummary);
|
|
83
246
|
}
|
|
84
247
|
finally {
|
package/dist/server/index.js
CHANGED
|
@@ -9,7 +9,10 @@ import { randomUUID } from "node:crypto";
|
|
|
9
9
|
import { ArtifactStatusSchema, VerdictSchema, artifactKey } from "../core/artifact.js";
|
|
10
10
|
import { toMarkdown } from "../core/export.js";
|
|
11
11
|
import { fetchPrDiff, fetchPrInfo, submitReview } from "../core/gh.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { DaemonConfigSchema, configPath, loadConfig, saveConfig } from "../core/config.js";
|
|
12
14
|
import { refreshArtifact } from "../core/refresh.js";
|
|
15
|
+
import { TrustRuleError, describeRule, explainRule, parseTrustRule } from "../core/trust.js";
|
|
13
16
|
import { buildReviewPayload, computeCalibration } from "../core/send.js";
|
|
14
17
|
import { listArtifacts, loadArtifactByKey, reconcileRunning, updateArtifactByKey, } from "../core/state.js";
|
|
15
18
|
import { isReviewRunning } from "../runner/inflight.js";
|
|
@@ -34,6 +37,91 @@ export async function buildApp(opts) {
|
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
39
|
app.get("/api/daemon", (c) => c.json(opts.daemon ? opts.daemon.status() : { enabled: false }));
|
|
40
|
+
// ---- Config: trust rules, edited from the cockpit's Settings view ----
|
|
41
|
+
const trustView = (lines) => lines.flatMap((line) => {
|
|
42
|
+
try {
|
|
43
|
+
const rule = parseTrustRule(line);
|
|
44
|
+
return rule ? [{ rule: describeRule(rule), explanation: explainRule(rule), denies: rule.negated }] : [];
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// loadConfig rejects these, so reaching here means the file changed
|
|
48
|
+
// under us; showing the rest beats failing the whole screen.
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
app.get("/api/config", async (c) => {
|
|
53
|
+
try {
|
|
54
|
+
const config = await loadConfig();
|
|
55
|
+
return c.json({ path: configPath(), trust: trustView(config.trust), daemon: config.daemon });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// The inbox knobs. The daemon re-reads the config every poll, so a toggle
|
|
62
|
+
// here applies on the next tick without restarting serve.
|
|
63
|
+
app.post("/api/config/daemon", async (c) => {
|
|
64
|
+
let body;
|
|
65
|
+
try {
|
|
66
|
+
body = await c.req.json();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return c.json({ error: "body must be JSON" }, 400);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const config = await loadConfig();
|
|
73
|
+
const daemon = DaemonConfigSchema.parse({ ...config.daemon, ...body });
|
|
74
|
+
await saveConfig({ ...config, daemon });
|
|
75
|
+
return c.json({ path: configPath(), trust: trustView(config.trust), daemon });
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err instanceof z.ZodError) {
|
|
79
|
+
return c.json({ error: err.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ") }, 400);
|
|
80
|
+
}
|
|
81
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
app.post("/api/config/trust", async (c) => {
|
|
85
|
+
let body;
|
|
86
|
+
try {
|
|
87
|
+
body = await c.req.json();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return c.json({ error: "body must be JSON" }, 400);
|
|
91
|
+
}
|
|
92
|
+
const { rule, remove } = body;
|
|
93
|
+
if (typeof rule !== "string" || !rule.trim()) {
|
|
94
|
+
return c.json({ error: "rule is required" }, 400);
|
|
95
|
+
}
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = parseTrustRule(rule);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
// The message explains what to write instead — show it verbatim.
|
|
102
|
+
if (err instanceof TrustRuleError)
|
|
103
|
+
return c.json({ error: err.message }, 400);
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
if (!parsed)
|
|
107
|
+
return c.json({ error: `not a trust rule: ${rule}` }, 400);
|
|
108
|
+
const canonical = describeRule(parsed);
|
|
109
|
+
try {
|
|
110
|
+
const config = await loadConfig();
|
|
111
|
+
const without = config.trust.filter((line) => {
|
|
112
|
+
const existing = parseTrustRule(line);
|
|
113
|
+
return !existing || describeRule(existing) !== canonical;
|
|
114
|
+
});
|
|
115
|
+
const trust = remove === true ? without : [...without, canonical];
|
|
116
|
+
await saveConfig({ ...config, trust });
|
|
117
|
+
// Full ConfigView — the cockpit replaces its config state with this
|
|
118
|
+
// wholesale, so omitting daemon would crash the Settings toggles.
|
|
119
|
+
return c.json({ path: configPath(), trust: trustView(trust), daemon: config.daemon });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
37
125
|
app.get("/api/reviews", async (c) => {
|
|
38
126
|
const artifacts = await listArtifacts();
|
|
39
127
|
return c.json(artifacts.map((a) => ({
|
|
@@ -48,6 +136,7 @@ export async function buildApp(opts) {
|
|
|
48
136
|
owner: a.pr.owner,
|
|
49
137
|
repo: a.pr.repo,
|
|
50
138
|
number: a.pr.number,
|
|
139
|
+
state: a.pr.state,
|
|
51
140
|
additions: a.pr.additions,
|
|
52
141
|
deletions: a.pr.deletions,
|
|
53
142
|
changedFiles: a.pr.changedFiles,
|
|
@@ -188,15 +277,32 @@ export async function buildApp(opts) {
|
|
|
188
277
|
return c.json({ error: "a review of this PR is already running" }, 409);
|
|
189
278
|
}
|
|
190
279
|
const ref = { owner: artifact.pr.owner, repo: artifact.pr.repo, number: artifact.pr.number };
|
|
280
|
+
// Source-backed unless the server was started with --no-source; ?source=1
|
|
281
|
+
// overrides that per re-review, ?source=0 opts one out.
|
|
282
|
+
const sourceParam = c.req.query("source");
|
|
283
|
+
const withSource = sourceParam == null ? opts.withSource : sourceParam !== "0";
|
|
191
284
|
// Mark it running before responding, so the cockpit's next poll can't catch
|
|
192
285
|
// the old "ready" status and conclude the run already finished.
|
|
193
286
|
const running = await updateArtifactByKey(key, (a) => ({
|
|
194
287
|
...a,
|
|
195
288
|
status: "running",
|
|
196
|
-
run: {
|
|
289
|
+
run: {
|
|
290
|
+
model: null,
|
|
291
|
+
startedAt: new Date().toISOString(),
|
|
292
|
+
finishedAt: null,
|
|
293
|
+
costUsd: null,
|
|
294
|
+
error: null,
|
|
295
|
+
withSource: withSource !== false,
|
|
296
|
+
trusted: false,
|
|
297
|
+
},
|
|
197
298
|
}));
|
|
198
299
|
// Minutes-long: run it detached and let the cockpit poll the artifact.
|
|
199
|
-
void reviewPr(ref, {
|
|
300
|
+
void reviewPr(ref, {
|
|
301
|
+
force: true,
|
|
302
|
+
withSource,
|
|
303
|
+
trust: opts.trust,
|
|
304
|
+
onProgress: (m) => console.log(`[rerun ${artifact.id}] ${m}`),
|
|
305
|
+
})
|
|
200
306
|
.catch(async (err) => {
|
|
201
307
|
const message = err instanceof Error ? err.message : String(err);
|
|
202
308
|
console.error(`[rerun ${artifact.id}] failed: ${message}`);
|
|
@@ -281,7 +387,10 @@ export async function startServer(opts) {
|
|
|
281
387
|
serve({ fetch: app.fetch, port: opts.port, hostname: opts.host }, (info) => {
|
|
282
388
|
const tokenHint = opts.token ? `/?token=${opts.token}` : "";
|
|
283
389
|
console.log(`cerber cockpit: http://${opts.host}:${info.port}${tokenHint}`);
|
|
284
|
-
if (opts.daemon)
|
|
285
|
-
|
|
390
|
+
if (opts.daemon) {
|
|
391
|
+
const s = opts.daemon.status();
|
|
392
|
+
console.log(`inbox: polling every ${Math.round(s.intervalMs / 60_000)}m` +
|
|
393
|
+
`${s.autoReview ? ", drafting a review for each PR awaiting you" : " (auto-review off — click review in the queue)"}`);
|
|
394
|
+
}
|
|
286
395
|
});
|
|
287
396
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackhouse/cerber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "AI code-review cockpit: Claude reviews your PRs into local artifacts; nothing reaches GitHub until you say so.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:fullstackhouse/cerber",
|