@fullstackhouse/cerber 0.24.0 → 0.24.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/README.md +7 -0
- package/dist/cli/index.js +18 -1
- package/dist/core/artifact.js +11 -0
- package/dist/core/refresh.js +40 -13
- package/dist/core/state.js +11 -1
- package/dist/runner/review.js +45 -28
- package/dist/server/index.js +38 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -384,6 +384,13 @@ shadow mode for a while: every would-send decision lands in
|
|
|
384
384
|
verdicts and comments survive your review — enable auto-send when its 90%
|
|
385
385
|
actually means 90%.
|
|
386
386
|
|
|
387
|
+
## How it works
|
|
388
|
+
|
|
389
|
+
[**docs/lifecycle.md**](docs/lifecycle.md) is the reference behind everything
|
|
390
|
+
above: the seven statuses and what sets each, which tab a PR lands in and why,
|
|
391
|
+
when the poll files or archives a row on its own, when a re-review happens and
|
|
392
|
+
what it will not overwrite, and what gets written to disk at each step.
|
|
393
|
+
|
|
387
394
|
## Development
|
|
388
395
|
|
|
389
396
|
```bash
|
package/dist/cli/index.js
CHANGED
|
@@ -8,8 +8,9 @@ import { configPath, loadConfig, saveConfig } from "../core/config.js";
|
|
|
8
8
|
import { TrustRuleError, describeRule, explainRule, parseTrustRule } from "../core/trust.js";
|
|
9
9
|
import { parsePrRef, searchAwaitingMe, submitReview } from "../core/gh.js";
|
|
10
10
|
import { buildReviewPayload, computeCalibration, eventForRecommendation } from "../core/send.js";
|
|
11
|
-
import { cerberHome, listArtifacts, loadArtifact, readAutoSendLog, updateArtifactByKey, } from "../core/state.js";
|
|
11
|
+
import { cerberHome, listArtifacts, loadArtifact, readAutoSendLog, reconcileRunning, updateArtifactByKey, } from "../core/state.js";
|
|
12
12
|
import { pool, reviewPr } from "../runner/review.js";
|
|
13
|
+
import { isReviewRunning } from "../runner/inflight.js";
|
|
13
14
|
import { startDaemon } from "../server/daemon.js";
|
|
14
15
|
import { startServer } from "../server/index.js";
|
|
15
16
|
const program = new Command();
|
|
@@ -135,6 +136,15 @@ program
|
|
|
135
136
|
console.error(`Already sent at ${artifact.sent.at}${artifact.sent.url ? ` — ${artifact.sent.url}` : ""}`);
|
|
136
137
|
process.exit(1);
|
|
137
138
|
}
|
|
139
|
+
// The persisted status is the only signal here: a run started by `serve` or
|
|
140
|
+
// by another terminal is invisible to this process. Sending mid-run vouches
|
|
141
|
+
// for a draft that is being replaced as you read it.
|
|
142
|
+
if (artifact.status === "running") {
|
|
143
|
+
console.error(`A review of ${id} is running — wait for it to finish, then send. ` +
|
|
144
|
+
`If nothing is actually running, the last run died mid-flight: re-run it, or restart \`cerber serve\`, ` +
|
|
145
|
+
`either of which clears the stale status.`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
138
148
|
const event = (opts.event ??
|
|
139
149
|
(artifact.verdict ? eventForRecommendation(artifact.verdict.recommendation) : "COMMENT")).toUpperCase();
|
|
140
150
|
if (!["APPROVE", "COMMENT", "REQUEST_CHANGES"].includes(event)) {
|
|
@@ -352,6 +362,13 @@ program
|
|
|
352
362
|
`matching PRs, with nobody watching. Pass --no-trust to keep every review read-only, ` +
|
|
353
363
|
`or --no-auto-review to only review on click.`);
|
|
354
364
|
}
|
|
365
|
+
// Before anything polls. Reconciliation is what makes "nothing is running"
|
|
366
|
+
// true after an unclean shutdown, and the daemon's first tick fires the
|
|
367
|
+
// moment it is constructed — so starting it first put both of them on the
|
|
368
|
+
// same leftover artifact with no ordering between them.
|
|
369
|
+
const cleared = await reconcileRunning({ inUse: isReviewRunning });
|
|
370
|
+
if (cleared > 0)
|
|
371
|
+
console.log(`Cleared ${cleared} interrupted run(s) left by a previous process.`);
|
|
355
372
|
const daemon = opts.poll
|
|
356
373
|
? startDaemon({
|
|
357
374
|
repos: opts.repo.length > 0 ? opts.repo : d.repos,
|
package/dist/core/artifact.js
CHANGED
|
@@ -97,6 +97,17 @@ export const RunInfoSchema = z.object({
|
|
|
97
97
|
* be filed away automatically and one that must not be.
|
|
98
98
|
*/
|
|
99
99
|
trigger: z.enum(["daemon", "user"]).nullable().default(null),
|
|
100
|
+
/**
|
|
101
|
+
* The head commit this run actually read, set when it finishes.
|
|
102
|
+
*
|
|
103
|
+
* `pr.headSha` cannot answer that question: a refresh moves it forward to
|
|
104
|
+
* keep the comments anchored to current code, without anybody re-reading
|
|
105
|
+
* anything. The freshness guard compares against *this* instead, so merely
|
|
106
|
+
* opening a draft after a push no longer convinces the poll that the draft
|
|
107
|
+
* is up to date. Null on runs recorded before this existed, and on one still
|
|
108
|
+
* in flight — both fall back to the old comparison.
|
|
109
|
+
*/
|
|
110
|
+
reviewedSha: z.string().nullable().default(null),
|
|
100
111
|
});
|
|
101
112
|
export const SentInfoSchema = z.object({
|
|
102
113
|
at: z.string(),
|
package/dist/core/refresh.js
CHANGED
|
@@ -34,21 +34,48 @@ export function refreshArtifact(artifact, pr, diff) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Is this artifact's status the user's to keep, rather than the run's to set?
|
|
38
|
+
*
|
|
39
|
+
* A send or a settle is a decision about the PR; a run finishing is a fact
|
|
40
|
+
* about the code. Whichever way the run went — a draft or an error — it does
|
|
41
|
+
* not get to reopen one, which is the same rule `SETTLED_BY_YOU` applies to
|
|
42
|
+
* starting a run in the first place.
|
|
40
43
|
*/
|
|
41
|
-
export function
|
|
42
|
-
return
|
|
44
|
+
export function userOwnsStatus(a) {
|
|
45
|
+
return a.sent !== null || a.status === "reviewed" || a.status === "skipped";
|
|
43
46
|
}
|
|
44
47
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
48
|
+
* Fold a finished review run onto whatever the artifact says now.
|
|
49
|
+
*
|
|
50
|
+
* A run takes minutes and the cockpit stays live throughout, so by the time one
|
|
51
|
+
* lands the artifact may have moved under it. Saving the result wholesale
|
|
52
|
+
* silently undid whatever had happened in between — the same mistake
|
|
53
|
+
* `mergeConcurrentEdits` exists to stop a chat turn making.
|
|
54
|
+
*
|
|
55
|
+
* `fresh` is what the run produced; `current` is what is on disk now. The run
|
|
56
|
+
* owns the draft — summary, chapters, verdict, comments — and that includes
|
|
57
|
+
* replacing comments the user wrote: a re-review starts fresh by design, and
|
|
58
|
+
* `docs/lifecycle.md` says so plainly rather than leaving anyone to count on
|
|
59
|
+
* work surviving one. What it does *not* own are the decisions:
|
|
60
|
+
*
|
|
61
|
+
* - **a send** stands, and takes the status with it. Without this a run
|
|
62
|
+
* finishing after a send wrote `sent: null` back over the record, and the
|
|
63
|
+
* "already sent" guard would then wave a second submission through.
|
|
64
|
+
* - **a settle** stands too: `reviewed` and `skipped` are decisions about the
|
|
65
|
+
* PR, not facts about the code, so a run completing does not reopen one.
|
|
66
|
+
* The fresh draft still lands underneath, which is what the row shows if
|
|
67
|
+
* the user changes their mind.
|
|
68
|
+
* - **the conversation** is the user's writing, snapshot and all.
|
|
47
69
|
*/
|
|
48
|
-
export function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
export function mergeRunResult(fresh, current) {
|
|
71
|
+
return {
|
|
72
|
+
...fresh,
|
|
73
|
+
status: userOwnsStatus(current) ? current.status : fresh.status,
|
|
74
|
+
sent: current.sent,
|
|
75
|
+
calibration: current.calibration,
|
|
76
|
+
filed: current.filed,
|
|
77
|
+
chat: current.chat,
|
|
78
|
+
preChat: current.preChat,
|
|
79
|
+
pendingChat: current.pendingChat,
|
|
80
|
+
};
|
|
54
81
|
}
|
package/dist/core/state.js
CHANGED
|
@@ -98,11 +98,21 @@ export async function updateArtifactByKey(key, mutate) {
|
|
|
98
98
|
* this it stays wedged forever, with no way to retry it from the cockpit. A
|
|
99
99
|
* `cerber review` running in another terminal is the one false positive; it
|
|
100
100
|
* overwrites the artifact when it finishes anyway.
|
|
101
|
+
*
|
|
102
|
+
* `inUse` names the runs this process genuinely owns, and they are left alone.
|
|
103
|
+
* Callers should still reconcile before starting anything that polls — but
|
|
104
|
+
* ordering alone is a promise about wiring, and this is the guard that holds
|
|
105
|
+
* whatever order they are started in: without it, reconciliation racing a
|
|
106
|
+
* daemon's first tick could stamp "interrupted" over a review that had just
|
|
107
|
+
* legitimately begun.
|
|
101
108
|
*/
|
|
102
|
-
export async function reconcileRunning() {
|
|
109
|
+
export async function reconcileRunning(opts = {}) {
|
|
110
|
+
const inUse = opts.inUse ?? (() => false);
|
|
103
111
|
const artifacts = await listArtifacts();
|
|
104
112
|
let cleared = 0;
|
|
105
113
|
for (const artifact of artifacts) {
|
|
114
|
+
if (inUse(artifact.id))
|
|
115
|
+
continue;
|
|
106
116
|
const stuckRun = artifact.status === "running";
|
|
107
117
|
const stuckChat = artifact.pendingChat && artifact.pendingChat.error == null ? artifact.pendingChat : null;
|
|
108
118
|
if (!stuckRun && !stuckChat)
|
package/dist/runner/review.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { AiReviewSchema, SCHEMA_VERSION, artifactId, } from "../core/artifact.js";
|
|
2
|
+
import { AiReviewSchema, SCHEMA_VERSION, artifactId, artifactKey, } from "../core/artifact.js";
|
|
3
3
|
import { createRunDir, evictOldCheckouts, prepareCheckout, removeRunDir } from "../core/checkout.js";
|
|
4
4
|
import { fetchPrDiff, fetchPrInfo, isOrgMember, isTeamMember } from "../core/gh.js";
|
|
5
|
-
import {
|
|
6
|
-
import { loadArtifact, saveArtifact } from "../core/state.js";
|
|
5
|
+
import { mergeRunResult, userOwnsStatus } from "../core/refresh.js";
|
|
6
|
+
import { loadArtifact, saveArtifact, updateArtifactByKey } from "../core/state.js";
|
|
7
7
|
import { loadConfig } from "../core/config.js";
|
|
8
8
|
import { decideTrust, membershipQueries, parseTrustRules } from "../core/trust.js";
|
|
9
9
|
import { extractJson, runClaude, unauthenticatedEnv } from "./claude.js";
|
|
@@ -91,10 +91,15 @@ async function runReview(ref, opts) {
|
|
|
91
91
|
log(`You marked this ${existing.status} — leaving it alone. Use --force to re-review.`);
|
|
92
92
|
return { artifact: existing, skipped: true };
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// The sha the AI *read*, not the one the artifact happens to mention.
|
|
95
|
+
// `pr.headSha` is moved forward by the refresh that runs whenever a review
|
|
96
|
+
// is opened, so comparing against it meant that merely looking at a draft
|
|
97
|
+
// after a push convinced this guard the draft was current, and the poll
|
|
98
|
+
// never re-reviewed it again. Older artifacts have no `reviewedSha` and
|
|
99
|
+
// fall back to the old comparison.
|
|
100
|
+
const reviewedSha = existing.run?.reviewedSha ?? existing.pr.headSha;
|
|
101
|
+
if (HEAD_SENSITIVE.has(existing.status) && reviewedSha !== "" && reviewedSha === pr.headSha) {
|
|
102
|
+
log(`Up to date (reviewed at ${reviewedSha.slice(0, 7)}, status ${existing.status}) — skipping. Use --force to re-review.`);
|
|
98
103
|
return { artifact: existing, skipped: true };
|
|
99
104
|
}
|
|
100
105
|
}
|
|
@@ -120,6 +125,14 @@ async function runReview(ref, opts) {
|
|
|
120
125
|
`reviewing from the diff alone.`);
|
|
121
126
|
}
|
|
122
127
|
}
|
|
128
|
+
// A re-review regenerates the draft, comments and all — deliberately, and
|
|
129
|
+
// including any the user wrote. Carrying them across is a feature this tool
|
|
130
|
+
// has decided not to have, so nothing here tries to half-keep them: they go
|
|
131
|
+
// when the run starts, whichever way it ends. `docs/lifecycle.md` says so
|
|
132
|
+
// where a reader would otherwise assume otherwise.
|
|
133
|
+
if (existing && existing.comments.length > 0) {
|
|
134
|
+
log(`Replacing the previous draft's ${existing.comments.length} comment(s) — a re-review starts fresh.`);
|
|
135
|
+
}
|
|
123
136
|
let artifact = {
|
|
124
137
|
schemaVersion: SCHEMA_VERSION,
|
|
125
138
|
id: artifactId(pr),
|
|
@@ -142,6 +155,7 @@ async function runReview(ref, opts) {
|
|
|
142
155
|
trusted: trusted && source !== null,
|
|
143
156
|
sessionId: null,
|
|
144
157
|
trigger: opts.trigger ?? "user",
|
|
158
|
+
reviewedSha: null,
|
|
145
159
|
},
|
|
146
160
|
sent: null,
|
|
147
161
|
refresh: null,
|
|
@@ -204,34 +218,37 @@ async function runReview(ref, opts) {
|
|
|
204
218
|
// whose working directory was empty has nothing a chat turn could read.
|
|
205
219
|
sessionId: source ? review.sessionId : null,
|
|
206
220
|
trigger: artifact.run.trigger,
|
|
221
|
+
// What this run actually read — the thing the freshness guard needs
|
|
222
|
+
// and `pr.headSha` cannot say, because a refresh moves that forward
|
|
223
|
+
// without anybody re-reading the code.
|
|
224
|
+
reviewedSha: pr.headSha,
|
|
207
225
|
},
|
|
208
226
|
};
|
|
209
|
-
// A re-review regenerates the AI's comments, but the human's own comments
|
|
210
|
-
// and their rewrites of AI ones are work the run must not throw away.
|
|
211
|
-
if (existing) {
|
|
212
|
-
const { comments, carried, drifted } = carryOverComments(existing, artifact);
|
|
213
|
-
if (carried > 0) {
|
|
214
|
-
artifact = { ...artifact, comments };
|
|
215
|
-
log(`Carried over ${carried} comment(s) you wrote or edited` +
|
|
216
|
-
(drifted > 0 ? `; ${drifted} no longer match the diff and will post in the body.` : "."));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
227
|
}
|
|
220
228
|
catch (err) {
|
|
221
|
-
|
|
222
|
-
...artifact,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
run: {
|
|
226
|
-
...artifact.run,
|
|
227
|
-
finishedAt: now(),
|
|
228
|
-
error: err instanceof Error ? err.message : String(err),
|
|
229
|
-
},
|
|
229
|
+
const run = {
|
|
230
|
+
...artifact.run,
|
|
231
|
+
finishedAt: now(),
|
|
232
|
+
error: err instanceof Error ? err.message : String(err),
|
|
230
233
|
};
|
|
231
|
-
|
|
234
|
+
// Onto what is on disk, not over it: the artifact this run built holds none
|
|
235
|
+
// of the comments or marks that may have landed while it ran, and a failure
|
|
236
|
+
// is no reason to lose them — nor to reopen a decision the user made.
|
|
237
|
+
const saved = await updateArtifactByKey(artifactKey(artifact.id), (a) => ({
|
|
238
|
+
...a,
|
|
239
|
+
status: userOwnsStatus(a) ? a.status : "failed",
|
|
240
|
+
run,
|
|
241
|
+
}));
|
|
242
|
+
artifact = saved ?? { ...artifact, status: "failed", run, updatedAt: now() };
|
|
243
|
+
if (!saved)
|
|
244
|
+
await saveArtifact(artifact);
|
|
232
245
|
throw err;
|
|
233
246
|
}
|
|
234
|
-
await
|
|
247
|
+
const merged = await updateArtifactByKey(artifactKey(artifact.id), (current) => mergeRunResult(artifact, current));
|
|
248
|
+
if (merged)
|
|
249
|
+
artifact = merged;
|
|
250
|
+
else
|
|
251
|
+
await saveArtifact(artifact);
|
|
235
252
|
return { artifact, skipped: false };
|
|
236
253
|
}
|
|
237
254
|
/** Run tasks with bounded concurrency, preserving order of results. */
|
package/dist/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { DaemonConfigSchema, configPath, loadConfig, saveConfig } from "../core/
|
|
|
15
15
|
import { refreshArtifact } from "../core/refresh.js";
|
|
16
16
|
import { TrustRuleError, describeRule, explainRule, parseTrustRule } from "../core/trust.js";
|
|
17
17
|
import { buildReviewPayload, computeCalibration } from "../core/send.js";
|
|
18
|
-
import { listArtifacts, loadArtifact, loadArtifactByKey,
|
|
18
|
+
import { listArtifacts, loadArtifact, loadArtifactByKey, saveArtifact, updateArtifactByKey, } from "../core/state.js";
|
|
19
19
|
import { isReviewRunning } from "../runner/inflight.js";
|
|
20
20
|
import { reviewPr } from "../runner/review.js";
|
|
21
21
|
import { runChatTurn } from "../runner/chat.js";
|
|
@@ -23,6 +23,20 @@ import { mergeConcurrentEdits, restoreReview } from "../core/revise.js";
|
|
|
23
23
|
import { progressWriter } from "./progress.js";
|
|
24
24
|
import { ChatTurnSchema } from "../core/artifact.js";
|
|
25
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
/**
|
|
27
|
+
* The statuses `PATCH /api/reviews/:key` will set: the two that are the user's
|
|
28
|
+
* own decision about a PR. Everything else is a fact some other path owns.
|
|
29
|
+
*/
|
|
30
|
+
const SETTLEABLE = ["reviewed", "skipped"];
|
|
31
|
+
/**
|
|
32
|
+
* Is an AI run rewriting this draft right now?
|
|
33
|
+
*
|
|
34
|
+
* Two questions, because neither alone is enough: `isReviewRunning` is an
|
|
35
|
+
* in-memory claim and cannot see a `cerber review` going in another terminal,
|
|
36
|
+
* while the persisted status cannot see a run this process started moments ago
|
|
37
|
+
* that has not written yet.
|
|
38
|
+
*/
|
|
39
|
+
const inFlight = (a) => a.status === "running" || isReviewRunning(a.id);
|
|
26
40
|
export async function buildApp(opts) {
|
|
27
41
|
const app = new Hono();
|
|
28
42
|
if (opts.token) {
|
|
@@ -251,6 +265,7 @@ export async function buildApp(opts) {
|
|
|
251
265
|
trusted: false,
|
|
252
266
|
sessionId: null,
|
|
253
267
|
trigger: "user",
|
|
268
|
+
reviewedSha: null,
|
|
254
269
|
},
|
|
255
270
|
sent: null,
|
|
256
271
|
refresh: null,
|
|
@@ -284,6 +299,14 @@ export async function buildApp(opts) {
|
|
|
284
299
|
// ---- Mutations (all local — artifact edits only) ----
|
|
285
300
|
app.patch("/api/reviews/:key", async (c) => {
|
|
286
301
|
const body = await c.req.json();
|
|
302
|
+
// Only the two statuses that are the user's own decision. The schema
|
|
303
|
+
// permits all seven, and taking them all meant this endpoint would happily
|
|
304
|
+
// write `status: "sent"` with no `sent` record and nothing submitted —
|
|
305
|
+
// a row that claims a review reached GitHub, which no honest path produces.
|
|
306
|
+
// `running`/`ready`/`failed` belong to the runner, `sent` to the send path.
|
|
307
|
+
if (body.status !== undefined && !SETTLEABLE.includes(body.status)) {
|
|
308
|
+
return c.json({ error: `status must be one of ${SETTLEABLE.join(", ")} — got ${body.status}` }, 400);
|
|
309
|
+
}
|
|
287
310
|
const updated = await updateArtifactByKey(c.req.param("key"), (a) => {
|
|
288
311
|
const next = { ...a };
|
|
289
312
|
if (body.status !== undefined) {
|
|
@@ -429,6 +452,7 @@ export async function buildApp(opts) {
|
|
|
429
452
|
trusted: false,
|
|
430
453
|
sessionId: null,
|
|
431
454
|
trigger: "user",
|
|
455
|
+
reviewedSha: null,
|
|
432
456
|
},
|
|
433
457
|
}));
|
|
434
458
|
// Minutes-long: run it detached and let the cockpit poll the artifact.
|
|
@@ -589,6 +613,13 @@ export async function buildApp(opts) {
|
|
|
589
613
|
if (artifact.sent) {
|
|
590
614
|
return c.json({ error: `already sent at ${artifact.sent.at}` }, 409);
|
|
591
615
|
}
|
|
616
|
+
// Not while a run is rewriting this draft. Vouching for a review that is
|
|
617
|
+
// being replaced under you is reason enough on its own; on top of that the
|
|
618
|
+
// run used to save `sent: null` back over the record when it landed, so the
|
|
619
|
+
// guard above would wave a *second* submission through afterwards.
|
|
620
|
+
if (inFlight(artifact)) {
|
|
621
|
+
return c.json({ error: "a review of this PR is running — wait for it to finish" }, 409);
|
|
622
|
+
}
|
|
592
623
|
const payload = buildReviewPayload(artifact, event);
|
|
593
624
|
try {
|
|
594
625
|
const { url } = await submitReview({ owner: artifact.pr.owner, repo: artifact.pr.repo, number: artifact.pr.number }, { event: payload.event, body: payload.body, comments: payload.comments, commitId: payload.commitId });
|
|
@@ -628,11 +659,13 @@ export async function buildApp(opts) {
|
|
|
628
659
|
}
|
|
629
660
|
return app;
|
|
630
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Serve the cockpit. `reconcileRunning` is deliberately *not* called here: the
|
|
664
|
+
* caller must have done it before starting the daemon, because the daemon polls
|
|
665
|
+
* as soon as it is constructed. Doing it here as well would have marked that
|
|
666
|
+
* poll's own fresh run as an interrupted leftover.
|
|
667
|
+
*/
|
|
631
668
|
export async function startServer(opts) {
|
|
632
|
-
const cleared = await reconcileRunning();
|
|
633
|
-
if (cleared > 0) {
|
|
634
|
-
console.log(`Cleared ${cleared} AI run(s) left in flight by a previous process — start them again from the cockpit.`);
|
|
635
|
-
}
|
|
636
669
|
const app = await buildApp(opts);
|
|
637
670
|
serve({ fetch: app.fetch, port: opts.port, hostname: opts.host }, (info) => {
|
|
638
671
|
const tokenHint = opts.token ? `/?token=${opts.token}` : "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackhouse/cerber",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
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
|
"author": "Full Stack House (https://fullstack.house)",
|