@clonst/clonst 1.0.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/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/core/consensus.js +175 -0
- package/dist/core/consensus.js.map +1 -0
- package/dist/core/formatter.js +186 -0
- package/dist/core/formatter.js.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/base.js +17 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/codex.js +300 -0
- package/dist/providers/codex.js.map +1 -0
- package/dist/tools/review.js +440 -0
- package/dist/tools/review.js.map +1 -0
- package/dist/utils/config.js +97 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.js +141 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.js +16 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/process.js +186 -0
- package/dist/utils/process.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { isConsensus, parseReviewerResponse } from "../core/consensus.js";
|
|
6
|
+
import { buildFirstRoundPrompt, buildFollowupRoundPrompt } from "../core/formatter.js";
|
|
7
|
+
import { ProviderError } from "../providers/base.js";
|
|
8
|
+
import { SessionLogger, logStderr } from "../utils/logger.js";
|
|
9
|
+
/**
|
|
10
|
+
* language[-Script][-Region] ONLY (e.g. "fr", "pt-BR", "zh-Hans", "zh-Hans-CN").
|
|
11
|
+
* Variant and extension subtags are deliberately excluded: Intl.DisplayNames
|
|
12
|
+
* echoes unknown variants back verbatim ("fr-approved" -> "French (APPROVED)"),
|
|
13
|
+
* which would let a caller inject chosen words into the reviewer prompt (found
|
|
14
|
+
* by the Codex review). Script and Region render only Intl-generated vocabulary.
|
|
15
|
+
*/
|
|
16
|
+
const SAFE_LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z]{4})?(-([A-Za-z]{2}|\d{3}))?$/;
|
|
17
|
+
/**
|
|
18
|
+
* Input schema of clonst_review (ZodRawShape for registerTool).
|
|
19
|
+
* The descriptions address the calling LLM: they drive the revision loop.
|
|
20
|
+
*/
|
|
21
|
+
export const reviewInputShape = {
|
|
22
|
+
content: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.describe("The plan, code or proposal to review, COMPLETE (on later rounds: the full revised version, not a diff)."),
|
|
26
|
+
context: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Round 1 only: context for the reviewer (project goal, constraints, decisions already made with the user). Ignored when thread_id is provided (the reviewer already holds the context in session memory)."),
|
|
30
|
+
thread_id: z
|
|
31
|
+
.string()
|
|
32
|
+
// Aligned with the SessionLogger's isValidSessionId: "." and ".." would pass a
|
|
33
|
+
// plain whitelist regex, then fail later as an opaque "internal" error.
|
|
34
|
+
.regex(/^(?!\.+$)[A-Za-z0-9._-]+$/)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Later rounds: the identifier returned by the previous call. Resumes the reviewer's session, so it remembers its critiques. Omit on round 1."),
|
|
37
|
+
round: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(50)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Round number (default: 1 without thread_id, 2 with). Hard safety cap: 50 rounds (a ping-pong reaching that point is a runaway, not a review)."),
|
|
44
|
+
max_rounds: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(50)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Round limit set by the user for THIS ping-pong (the reviewer is informed and calibrates its demands). Omit by default: no limit, the loop continues until consensus. Pass the same value on every round."),
|
|
51
|
+
project_path: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Absolute path of the project: the reviewer runs there and can read the real files. Reserve it for reviews that must verify existing APIs/contracts/files: by default the content parameter is enough, and the ENTIRE project (including .env and secrets) becomes readable by the reviewer when this parameter is set."),
|
|
55
|
+
language: z
|
|
56
|
+
.string()
|
|
57
|
+
.regex(SAFE_LANGUAGE_TAG)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Language code for the reviewer\'s free-text output: language[-Script][-Region] only (e.g. "fr", "pt-BR", "zh-Hans"). Pass the language of your conversation with the user so critiques read naturally to them. Omit to let the reviewer use the language of the reviewed content.'),
|
|
60
|
+
review_focus: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Round 1: review focus ('bugs', 'architecture', 'performance', 'security', 'all'). Default: all."),
|
|
64
|
+
changes_made: z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Later rounds: summary of the changes made in response to the previous critiques."),
|
|
68
|
+
changes_rejected: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Later rounds: rejected critiques, each with its justification (the reviewer will evaluate them)."),
|
|
72
|
+
};
|
|
73
|
+
const reviewInputObject = z.object(reviewInputShape);
|
|
74
|
+
/**
|
|
75
|
+
* MCP output schema (outputSchema): the calling LLM receives these fields as
|
|
76
|
+
* typed structuredContent, no text re-parsing needed.
|
|
77
|
+
*/
|
|
78
|
+
export const reviewOutputShape = {
|
|
79
|
+
status: z.literal("ok"),
|
|
80
|
+
round: z.number().int(),
|
|
81
|
+
verdict: z.enum(["APPROVED", "CHANGES_NEEDED"]),
|
|
82
|
+
consensus: z.boolean(),
|
|
83
|
+
score: z.number().nullable(),
|
|
84
|
+
critique: z.string(),
|
|
85
|
+
required_changes: z.array(z.string()),
|
|
86
|
+
suggestions: z.array(z.string()),
|
|
87
|
+
risks_identified: z.array(z.string()),
|
|
88
|
+
reviewer_feedback: z.string().nullable(),
|
|
89
|
+
parsed_from_fallback: z.boolean(),
|
|
90
|
+
thread_id: z.string().nullable(),
|
|
91
|
+
duration_seconds: z.number(),
|
|
92
|
+
total_duration_seconds: z.number(),
|
|
93
|
+
usage: z.record(z.string(), z.number()).nullable(),
|
|
94
|
+
total_usage: z.record(z.string(), z.number()).nullable(),
|
|
95
|
+
reviewer_model: z.string().nullable(),
|
|
96
|
+
reviewer_reasoning_effort: z.string().nullable(),
|
|
97
|
+
session_log: z.string(),
|
|
98
|
+
session_migrated: z.boolean().nullable(),
|
|
99
|
+
next_action: z.string(),
|
|
100
|
+
next_action_kind: z.enum(["done", "arbitrate", "checkpoint", "continue"]),
|
|
101
|
+
should_reinvoke: z.boolean(),
|
|
102
|
+
next_round: z.number().int().nullable(),
|
|
103
|
+
requires_user_input: z.boolean(),
|
|
104
|
+
};
|
|
105
|
+
/** Input error detected before any spawn (no quota consumed). */
|
|
106
|
+
export class InvalidInputError extends Error {
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolves a language code ("fr", "pt-BR") into an English language name
|
|
110
|
+
* ("French", "Brazilian Portuguese") through Intl.DisplayNames. The RAW input
|
|
111
|
+
* value never reaches the reviewer prompt: only this resolved name does. The
|
|
112
|
+
* language[-Script][-Region] shape is validated HERE as well as in the input
|
|
113
|
+
* schema (defense in depth: a future schema change cannot reopen the variant
|
|
114
|
+
* echo hole). Returns undefined for unknown or invalid codes (Intl either
|
|
115
|
+
* echoes the code back or throws): the caller then falls back to the default
|
|
116
|
+
* rule (language of the reviewed content). Exported for tests.
|
|
117
|
+
*/
|
|
118
|
+
export function resolveLanguageName(code) {
|
|
119
|
+
if (!SAFE_LANGUAGE_TAG.test(code))
|
|
120
|
+
return undefined;
|
|
121
|
+
try {
|
|
122
|
+
const resolved = new Intl.DisplayNames(["en"], { type: "language" }).of(code);
|
|
123
|
+
if (resolved !== undefined && resolved.toLowerCase() !== code.toLowerCase()) {
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// structurally invalid tag: fall through to undefined
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* File (in the session's raw directory) holding the last round's verdict and
|
|
134
|
+
* the ping-pong cumulative totals (duration, token usage).
|
|
135
|
+
*/
|
|
136
|
+
const LAST_VERDICT_FILE = "last_verdict.json";
|
|
137
|
+
/** Sums two token usage reports per key. null on both sides = null. */
|
|
138
|
+
function mergeUsage(previous, current) {
|
|
139
|
+
if (previous === null)
|
|
140
|
+
return current;
|
|
141
|
+
if (current === null)
|
|
142
|
+
return previous;
|
|
143
|
+
const merged = { ...previous };
|
|
144
|
+
for (const [key, value] of Object.entries(current)) {
|
|
145
|
+
merged[key] = (merged[key] ?? 0) + value;
|
|
146
|
+
}
|
|
147
|
+
return merged;
|
|
148
|
+
}
|
|
149
|
+
const SUGGESTIONS_POLICY = `For suggestions and risks_identified: ANALYZE them and decide YOURSELF whether to ` +
|
|
150
|
+
`apply or discard each one, based on your knowledge of the project and the decisions ` +
|
|
151
|
+
`already made - documenting every decision. Only involve the user when a suggestion ` +
|
|
152
|
+
`implies a genuine product/business choice, a decision that belongs to them, or ` +
|
|
153
|
+
`information you do not have.`;
|
|
154
|
+
/** Number agreement: "1 round", "3 rounds". */
|
|
155
|
+
function roundsLabel(n) {
|
|
156
|
+
return `${n} round${n > 1 ? "s" : ""}`;
|
|
157
|
+
}
|
|
158
|
+
function buildNextAction(consensus, threadId, round, explicitMaxRounds, checkpointRounds) {
|
|
159
|
+
if (consensus) {
|
|
160
|
+
return {
|
|
161
|
+
kind: "done",
|
|
162
|
+
should_reinvoke: false,
|
|
163
|
+
next_round: null,
|
|
164
|
+
requires_user_input: false,
|
|
165
|
+
text: `Consensus reached at round ${round}. ${SUGGESTIONS_POLICY} Then ALWAYS end ` +
|
|
166
|
+
`with a brief review report to the user (2-3 sentences maximum): the reviewer ` +
|
|
167
|
+
`used (reviewer_model / reviewer_reasoning_effort, "unknown" if null), the ` +
|
|
168
|
+
`number of rounds, the total review duration (total_duration_seconds field, ` +
|
|
169
|
+
`converted to readable minutes) with the tokens consumed (total_usage, ` +
|
|
170
|
+
`briefly), what the review concretely brought (bugs avoided, changes the ` +
|
|
171
|
+
`reviewer demanded, critiques you rejected), and your decisions on the ` +
|
|
172
|
+
`suggestions. Do NOT walk through the rounds one by one in this final report, ` +
|
|
173
|
+
`unless the user asks for it.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// Explicit limit set by the user and reached: arbitration, no re-invocation.
|
|
177
|
+
if (explicitMaxRounds !== undefined && round >= explicitMaxRounds) {
|
|
178
|
+
return {
|
|
179
|
+
kind: "arbitrate",
|
|
180
|
+
should_reinvoke: false,
|
|
181
|
+
next_round: null,
|
|
182
|
+
requires_user_input: true,
|
|
183
|
+
text: `No consensus at round ${round}: the limit of ${roundsLabel(explicitMaxRounds)} set for this ` +
|
|
184
|
+
`review is reached. Do NOT re-invoke clonst_review: present the state of the disagreement to ` +
|
|
185
|
+
`the user (the remaining required_changes and your rejection justifications) and ask them how ` +
|
|
186
|
+
`to settle it: accept the current version, follow the reviewer's position, or start a new review.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Resume impossible (the reviewer emitted no session identifier): this round's
|
|
190
|
+
// critique remains valid, but re-invoking means starting over (new session,
|
|
191
|
+
// reviewer memory lost, context to resend, quota spent again). That decision
|
|
192
|
+
// belongs to the user: text AND structured fields must say the same thing.
|
|
193
|
+
if (threadId === null) {
|
|
194
|
+
return {
|
|
195
|
+
kind: "checkpoint",
|
|
196
|
+
should_reinvoke: false,
|
|
197
|
+
next_round: null,
|
|
198
|
+
requires_user_input: true,
|
|
199
|
+
text: `No consensus, and the reviewer emitted no session identifier: resuming is ` +
|
|
200
|
+
`impossible (anomaly; this round's critique remains valid). Do NOT re-invoke ` +
|
|
201
|
+
`clonst_review without consulting the user: present the critique and ask whether ` +
|
|
202
|
+
`they want to restart a NEW review (round 1, no thread_id, context resent via ` +
|
|
203
|
+
`context - the reviewer starts over without memory of its critiques) or continue ` +
|
|
204
|
+
`without review. ` +
|
|
205
|
+
SUGGESTIONS_POLICY,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const resumeInstruction = `re-invoke clonst_review with: content = the COMPLETE revised version (the full document, never a diff or a summary), thread_id="${threadId}", round=${round + 1}, changes_made (summary of your changes) and changes_rejected (rejected critiques with justification).`;
|
|
209
|
+
// Without an explicit limit: PERIODIC check-in (rounds 5, 10, 15... for a
|
|
210
|
+
// checkpoint of 5), not permanent: between two multiples the loop resumes
|
|
211
|
+
// normally. The loop never stops on its own, but it also never runs
|
|
212
|
+
// indefinitely without the user being consulted.
|
|
213
|
+
if (explicitMaxRounds === undefined && checkpointRounds > 0 && round % checkpointRounds === 0) {
|
|
214
|
+
return {
|
|
215
|
+
kind: "checkpoint",
|
|
216
|
+
should_reinvoke: false,
|
|
217
|
+
next_round: round + 1,
|
|
218
|
+
requires_user_input: true,
|
|
219
|
+
text: `No consensus after ${roundsLabel(round)}. No limit is set for this review, but make a ` +
|
|
220
|
+
`CHECK-IN before continuing: present the state of the disagreement to the user and ask ` +
|
|
221
|
+
`whether they want to continue the ping-pong, settle it themselves, or accept the current ` +
|
|
222
|
+
`version. If they continue: apply the required_changes then ${resumeInstruction} ` +
|
|
223
|
+
SUGGESTIONS_POLICY,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
kind: "continue",
|
|
228
|
+
should_reinvoke: true,
|
|
229
|
+
next_round: round + 1,
|
|
230
|
+
requires_user_input: false,
|
|
231
|
+
text: `No consensus. Apply every item in required_changes (or prepare a rejection ` +
|
|
232
|
+
`justification when a point is factually wrong or contradicts the user's decisions), then ${resumeInstruction} ` +
|
|
233
|
+
SUGGESTIONS_POLICY,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Runs one review round. The provider is injected (testability); in production
|
|
238
|
+
* it is CodexProvider. Throws ProviderError or InvalidInputError: the MCP
|
|
239
|
+
* handler formats them through formatReviewError.
|
|
240
|
+
*/
|
|
241
|
+
export async function runReview(input, provider, config) {
|
|
242
|
+
// Every input validation happens BEFORE any spawn: a bad call must never
|
|
243
|
+
// consume quota.
|
|
244
|
+
if (input.project_path !== undefined) {
|
|
245
|
+
if (!path.isAbsolute(input.project_path)) {
|
|
246
|
+
throw new InvalidInputError(`project_path must be an ABSOLUTE path: "${input.project_path}" would resolve against the MCP server's cwd, not your conversation's.`);
|
|
247
|
+
}
|
|
248
|
+
if (!existsSync(input.project_path) || !statSync(input.project_path).isDirectory()) {
|
|
249
|
+
throw new InvalidInputError(`project_path is not an existing directory: "${input.project_path}". Provide the project root directory or omit the parameter.`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (input.thread_id === undefined && input.round !== undefined && input.round > 1) {
|
|
253
|
+
throw new InvalidInputError(`round=${input.round} without thread_id: the reviewer's session cannot be resumed. Provide the thread_id returned by the previous round, or omit round to start a new review.`);
|
|
254
|
+
}
|
|
255
|
+
if (input.thread_id !== undefined && input.round !== undefined && input.round < 2) {
|
|
256
|
+
throw new InvalidInputError(`thread_id provided with round=${input.round}: a thread_id implies resuming an existing session (round >= 2). Omit round (default: 2) or omit thread_id for a round 1.`);
|
|
257
|
+
}
|
|
258
|
+
// The effective round is computed BEFORE the limit validation: without this,
|
|
259
|
+
// an implicit call like { thread_id, max_rounds: 1 } (round inferred as 2)
|
|
260
|
+
// would pass validation and spawn anyway (found by the Codex review).
|
|
261
|
+
const round = input.round ?? (input.thread_id !== undefined ? 2 : 1);
|
|
262
|
+
if (input.max_rounds !== undefined && round > input.max_rounds) {
|
|
263
|
+
throw new InvalidInputError(`effective round ${round} exceeds max_rounds=${input.max_rounds}: the limit set for this review is already reached. Present the disagreement to the user instead of re-invoking.`);
|
|
264
|
+
}
|
|
265
|
+
// Later rounds: the logs live under the thread_id (session continuity);
|
|
266
|
+
// round 1: generated identifier, the thread_id does not exist yet.
|
|
267
|
+
const logger = new SessionLogger(input.thread_id ?? randomUUID());
|
|
268
|
+
// Server-side recall of the previous round's required_changes: we never depend
|
|
269
|
+
// solely on the reviewer's session memory (it can drift on long ping-pongs).
|
|
270
|
+
// Persisted in the session's raw directory on every round.
|
|
271
|
+
let previousRequiredChanges;
|
|
272
|
+
let previousTotalSeconds = 0;
|
|
273
|
+
let previousTotalUsage = null;
|
|
274
|
+
if (input.thread_id !== undefined) {
|
|
275
|
+
const raw = logger.readRaw(LAST_VERDICT_FILE);
|
|
276
|
+
if (raw !== null) {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(raw);
|
|
279
|
+
if (Array.isArray(parsed.required_changes)) {
|
|
280
|
+
previousRequiredChanges = parsed.required_changes.filter((item) => typeof item === "string");
|
|
281
|
+
}
|
|
282
|
+
// Ping-pong cumulative totals: invalid values are ignored (the total then
|
|
283
|
+
// restarts from the current round, never a crash for a degraded state file).
|
|
284
|
+
if (typeof parsed.cumulative_duration_seconds === "number" &&
|
|
285
|
+
Number.isFinite(parsed.cumulative_duration_seconds) &&
|
|
286
|
+
parsed.cumulative_duration_seconds >= 0) {
|
|
287
|
+
previousTotalSeconds = parsed.cumulative_duration_seconds;
|
|
288
|
+
}
|
|
289
|
+
if (typeof parsed.cumulative_usage === "object" &&
|
|
290
|
+
parsed.cumulative_usage !== null &&
|
|
291
|
+
!Array.isArray(parsed.cumulative_usage)) {
|
|
292
|
+
// All-or-nothing: a single invalid value (non-number, non-finite,
|
|
293
|
+
// negative) disqualifies the whole object and the total restarts from
|
|
294
|
+
// the current round. A negative token count is as invalid as a
|
|
295
|
+
// negative duration (Codex review).
|
|
296
|
+
const entries = Object.entries(parsed.cumulative_usage);
|
|
297
|
+
const allValid = entries.length > 0 &&
|
|
298
|
+
entries.every(([, v]) => typeof v === "number" && Number.isFinite(v) && v >= 0);
|
|
299
|
+
if (allValid)
|
|
300
|
+
previousTotalUsage = Object.fromEntries(entries);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
logStderr(`${LAST_VERDICT_FILE} unreadable, previous required_changes recall and totals omitted`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Resolved English name only (never the raw parameter): see resolveLanguageName.
|
|
309
|
+
let languageName;
|
|
310
|
+
if (input.language !== undefined) {
|
|
311
|
+
languageName = resolveLanguageName(input.language);
|
|
312
|
+
if (languageName === undefined) {
|
|
313
|
+
logStderr(`unknown language code "${input.language}", the reviewer will use the content's language`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const prompt = input.thread_id !== undefined
|
|
317
|
+
? buildFollowupRoundPrompt({
|
|
318
|
+
round,
|
|
319
|
+
maxRounds: input.max_rounds,
|
|
320
|
+
revisedContent: input.content,
|
|
321
|
+
changesMade: input.changes_made,
|
|
322
|
+
changesRejected: input.changes_rejected,
|
|
323
|
+
previousRequiredChanges,
|
|
324
|
+
language: languageName,
|
|
325
|
+
})
|
|
326
|
+
: buildFirstRoundPrompt({
|
|
327
|
+
content: input.content,
|
|
328
|
+
context: input.context,
|
|
329
|
+
reviewFocus: input.review_focus,
|
|
330
|
+
hasProjectAccess: input.project_path !== undefined,
|
|
331
|
+
maxRounds: input.max_rounds,
|
|
332
|
+
language: languageName,
|
|
333
|
+
});
|
|
334
|
+
logger.log({
|
|
335
|
+
event: "review_round_start",
|
|
336
|
+
round,
|
|
337
|
+
reviewer: provider.name,
|
|
338
|
+
resumed: input.thread_id !== undefined,
|
|
339
|
+
project_path: input.project_path ?? null,
|
|
340
|
+
content_chars: input.content.length,
|
|
341
|
+
// Clonst config overrides (null = the reviewer CLI's own defaults): traced so
|
|
342
|
+
// a review can be attributed to its real model/effort during diagnostics.
|
|
343
|
+
model_override: config.codex_model,
|
|
344
|
+
reasoning_effort_override: config.codex_reasoning_effort,
|
|
345
|
+
});
|
|
346
|
+
const invocation = await provider.invoke({
|
|
347
|
+
prompt,
|
|
348
|
+
cwd: input.project_path,
|
|
349
|
+
threadId: input.thread_id,
|
|
350
|
+
timeoutMs: config.timeout_per_call_seconds * 1000,
|
|
351
|
+
tag: `round_${round}`,
|
|
352
|
+
logger,
|
|
353
|
+
model: config.codex_model ?? undefined,
|
|
354
|
+
reasoningEffort: config.codex_reasoning_effort ?? undefined,
|
|
355
|
+
});
|
|
356
|
+
// Log continuity: round 1 started under a provisional identifier; as soon as
|
|
357
|
+
// the reviewer's thread_id is known, the session is migrated onto it so ALL
|
|
358
|
+
// rounds of one ping-pong live in the same JSONL file.
|
|
359
|
+
let sessionMigrated = null;
|
|
360
|
+
if (input.thread_id === undefined) {
|
|
361
|
+
sessionMigrated = invocation.threadId !== null && logger.migrateTo(invocation.threadId);
|
|
362
|
+
}
|
|
363
|
+
else if (invocation.threadId !== null && invocation.threadId !== input.thread_id) {
|
|
364
|
+
// thread_id_mismatch anomaly (already logged by the provider): the caller
|
|
365
|
+
// will pass the NEW thread_id on the next round. The logs and
|
|
366
|
+
// last_verdict.json migrate onto it, otherwise session continuity and the
|
|
367
|
+
// server-side recall would be lost.
|
|
368
|
+
sessionMigrated = logger.migrateTo(invocation.threadId);
|
|
369
|
+
}
|
|
370
|
+
const verdict = parseReviewerResponse(invocation.text);
|
|
371
|
+
const consensus = isConsensus(verdict);
|
|
372
|
+
const durationSeconds = Math.round(invocation.durationMs / 100) / 10;
|
|
373
|
+
const totalDurationSeconds = Math.round((previousTotalSeconds + durationSeconds) * 10) / 10;
|
|
374
|
+
const totalUsage = mergeUsage(previousTotalUsage, invocation.usage);
|
|
375
|
+
// Persisted AFTER any migration: the file lives in the final session directory
|
|
376
|
+
// and is re-read on the next round for the server-side recall and the totals.
|
|
377
|
+
logger.saveRaw(LAST_VERDICT_FILE, JSON.stringify({
|
|
378
|
+
round,
|
|
379
|
+
verdict: verdict.verdict,
|
|
380
|
+
required_changes: verdict.required_changes,
|
|
381
|
+
cumulative_duration_seconds: totalDurationSeconds,
|
|
382
|
+
cumulative_usage: totalUsage,
|
|
383
|
+
}, null, 2));
|
|
384
|
+
logger.log({
|
|
385
|
+
event: "review_round_done",
|
|
386
|
+
round,
|
|
387
|
+
verdict: verdict.verdict,
|
|
388
|
+
consensus,
|
|
389
|
+
score: verdict.score,
|
|
390
|
+
required_changes_count: verdict.required_changes.length,
|
|
391
|
+
parsed_from_fallback: verdict.parsed_from_fallback,
|
|
392
|
+
});
|
|
393
|
+
const nextAction = buildNextAction(consensus, invocation.threadId, round, input.max_rounds, config.suggested_max_rounds);
|
|
394
|
+
return {
|
|
395
|
+
status: "ok",
|
|
396
|
+
round,
|
|
397
|
+
verdict: verdict.verdict,
|
|
398
|
+
consensus,
|
|
399
|
+
score: verdict.score,
|
|
400
|
+
critique: verdict.critique || verdict.raw_text,
|
|
401
|
+
required_changes: verdict.required_changes,
|
|
402
|
+
suggestions: verdict.suggestions,
|
|
403
|
+
risks_identified: verdict.risks_identified,
|
|
404
|
+
reviewer_feedback: verdict.feedback,
|
|
405
|
+
parsed_from_fallback: verdict.parsed_from_fallback,
|
|
406
|
+
thread_id: invocation.threadId,
|
|
407
|
+
duration_seconds: durationSeconds,
|
|
408
|
+
total_duration_seconds: totalDurationSeconds,
|
|
409
|
+
usage: invocation.usage,
|
|
410
|
+
total_usage: totalUsage,
|
|
411
|
+
reviewer_model: invocation.model,
|
|
412
|
+
reviewer_reasoning_effort: invocation.reasoningEffort,
|
|
413
|
+
session_log: logger.jsonlPath,
|
|
414
|
+
session_migrated: sessionMigrated,
|
|
415
|
+
next_action: nextAction.text,
|
|
416
|
+
next_action_kind: nextAction.kind,
|
|
417
|
+
should_reinvoke: nextAction.should_reinvoke,
|
|
418
|
+
next_round: nextAction.next_round,
|
|
419
|
+
requires_user_input: nextAction.requires_user_input,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
export function formatReviewError(err) {
|
|
423
|
+
if (err instanceof ProviderError) {
|
|
424
|
+
return {
|
|
425
|
+
status: "error",
|
|
426
|
+
kind: err.kind,
|
|
427
|
+
message: err.message,
|
|
428
|
+
...(err.hint !== undefined ? { hint: err.hint } : {}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (err instanceof InvalidInputError) {
|
|
432
|
+
return { status: "error", kind: "invalid_input", message: err.message };
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
status: "error",
|
|
436
|
+
kind: "internal",
|
|
437
|
+
message: err instanceof Error ? (err.stack ?? err.message) : String(err),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
//# sourceMappingURL=review.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/tools/review.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { clonstHome, configPath } from "./paths.js";
|
|
4
|
+
import { logStderr } from "./logger.js";
|
|
5
|
+
// NB: no default_reviewer field as long as Codex is the only implemented
|
|
6
|
+
// provider (a config promising "gemini" without an implementation would lie).
|
|
7
|
+
export const DEFAULT_CONFIG = {
|
|
8
|
+
suggested_max_rounds: 5,
|
|
9
|
+
timeout_per_call_seconds: 600,
|
|
10
|
+
codex_model: null,
|
|
11
|
+
codex_reasoning_effort: null,
|
|
12
|
+
};
|
|
13
|
+
// These values become CLI arguments (-c model=...): same whitelist as
|
|
14
|
+
// assertSafeArg, never a space or a shell metacharacter. No closed enum
|
|
15
|
+
// (codex models and effort tiers evolve): codex itself rejects an unknown
|
|
16
|
+
// value with a clear message.
|
|
17
|
+
const SAFE_OVERRIDE = /^[A-Za-z0-9._-]+$/;
|
|
18
|
+
function normalizeOverride(parsed, key) {
|
|
19
|
+
const value = parsed[key];
|
|
20
|
+
if (value === undefined || value === null)
|
|
21
|
+
return null;
|
|
22
|
+
if (typeof value === "string" && SAFE_OVERRIDE.test(value))
|
|
23
|
+
return value;
|
|
24
|
+
logStderr(`config: invalid ${key} (${JSON.stringify(value)}), inheriting the global codex config`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validates the type and bounds of each key: an invalid value falls back to the
|
|
29
|
+
* default with a stderr warning (an aberrant value is never propagated silently).
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeConfig(parsed) {
|
|
32
|
+
const config = { ...DEFAULT_CONFIG };
|
|
33
|
+
const rounds = parsed["suggested_max_rounds"];
|
|
34
|
+
if (rounds !== undefined) {
|
|
35
|
+
if (typeof rounds === "number" && Number.isInteger(rounds) && rounds >= 1 && rounds <= 50) {
|
|
36
|
+
config.suggested_max_rounds = rounds;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logStderr(`config: invalid suggested_max_rounds (${JSON.stringify(rounds)}), using default ${DEFAULT_CONFIG.suggested_max_rounds}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const timeout = parsed["timeout_per_call_seconds"];
|
|
43
|
+
if (timeout !== undefined) {
|
|
44
|
+
if (typeof timeout === "number" && Number.isInteger(timeout) && timeout >= 10 && timeout <= 7200) {
|
|
45
|
+
config.timeout_per_call_seconds = timeout;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
logStderr(`config: invalid timeout_per_call_seconds (${JSON.stringify(timeout)}), using default ${DEFAULT_CONFIG.timeout_per_call_seconds}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
config.codex_model = normalizeOverride(parsed, "codex_model");
|
|
52
|
+
config.codex_reasoning_effort = normalizeOverride(parsed, "codex_reasoning_effort");
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Loads the persistent config, merged with the defaults (absent keys take the
|
|
57
|
+
* default value, invalid values too, with a warning). Absent file = defaults.
|
|
58
|
+
* Corrupt file = defaults, with the corrupt file backed up (never a silent loss).
|
|
59
|
+
*/
|
|
60
|
+
export function loadConfig() {
|
|
61
|
+
const file = configPath();
|
|
62
|
+
if (!existsSync(file)) {
|
|
63
|
+
return { ...DEFAULT_CONFIG };
|
|
64
|
+
}
|
|
65
|
+
let raw;
|
|
66
|
+
try {
|
|
67
|
+
raw = readFileSync(file, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
70
|
+
throw new Error("the JSON root is not an object");
|
|
71
|
+
}
|
|
72
|
+
return normalizeConfig(parsed);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const backup = `${file}.corrupt-${Date.now()}`;
|
|
76
|
+
try {
|
|
77
|
+
renameSync(file, backup);
|
|
78
|
+
logStderr(`config.json corrupt (${err instanceof Error ? err.message : String(err)}), backed up to ${backup}, using defaults`);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
logStderr(`config.json corrupt and backup impossible, using defaults`);
|
|
82
|
+
}
|
|
83
|
+
return { ...DEFAULT_CONFIG };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Writes the config atomically (tmp + rename) to avoid corruption if a crash
|
|
88
|
+
* happens mid-write.
|
|
89
|
+
*/
|
|
90
|
+
export function saveConfig(config) {
|
|
91
|
+
const file = configPath();
|
|
92
|
+
mkdirSync(clonstHome(), { recursive: true });
|
|
93
|
+
const tmp = path.join(path.dirname(file), `.config.json.tmp-${process.pid}`);
|
|
94
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
95
|
+
renameSync(tmp, file);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"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"}
|