opencara 0.18.3 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +564 -194
- package/package.json +2 -5
package/dist/index.js
CHANGED
|
@@ -7,8 +7,7 @@ import { Command as Command5 } from "commander";
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { execFile } from "child_process";
|
|
9
9
|
import crypto2 from "crypto";
|
|
10
|
-
import * as
|
|
11
|
-
import * as path6 from "path";
|
|
10
|
+
import * as path8 from "path";
|
|
12
11
|
|
|
13
12
|
// ../shared/dist/types.js
|
|
14
13
|
function isDedupRole(role) {
|
|
@@ -22,9 +21,9 @@ function isRepoAllowed(repoConfig, targetOwner, targetRepo, agentOwner) {
|
|
|
22
21
|
return true;
|
|
23
22
|
const fullRepo = `${targetOwner}/${targetRepo}`;
|
|
24
23
|
switch (repoConfig.mode) {
|
|
25
|
-
case "
|
|
24
|
+
case "public":
|
|
26
25
|
return true;
|
|
27
|
-
case "
|
|
26
|
+
case "private":
|
|
28
27
|
return agentOwner === targetOwner;
|
|
29
28
|
case "whitelist":
|
|
30
29
|
return (repoConfig.list ?? []).includes(fullRepo);
|
|
@@ -159,6 +158,20 @@ function parseStringArray(value) {
|
|
|
159
158
|
return [];
|
|
160
159
|
return value.filter((v) => typeof v === "string");
|
|
161
160
|
}
|
|
161
|
+
var DEFAULT_MODEL_DIVERSITY_GRACE_MS = 3e4;
|
|
162
|
+
function parseDurationSeconds(value, defaultMs) {
|
|
163
|
+
if (typeof value === "number")
|
|
164
|
+
return value === 0 ? 0 : clamp(value, 0, 300) * 1e3;
|
|
165
|
+
if (typeof value !== "string")
|
|
166
|
+
return defaultMs;
|
|
167
|
+
if (value === "0" || value === "0s")
|
|
168
|
+
return 0;
|
|
169
|
+
const match = value.match(/^(\d+)s$/);
|
|
170
|
+
if (!match)
|
|
171
|
+
return defaultMs;
|
|
172
|
+
const seconds = parseInt(match[1], 10);
|
|
173
|
+
return clamp(seconds, 0, 300) * 1e3;
|
|
174
|
+
}
|
|
162
175
|
var DEFAULT_TRIGGER = {
|
|
163
176
|
on: ["opened"],
|
|
164
177
|
comment: "/opencara review",
|
|
@@ -169,13 +182,14 @@ var DEFAULT_FEATURE_CONFIG = {
|
|
|
169
182
|
agentCount: 1,
|
|
170
183
|
timeout: "10m",
|
|
171
184
|
preferredModels: [],
|
|
172
|
-
preferredTools: []
|
|
185
|
+
preferredTools: [],
|
|
186
|
+
modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
|
|
173
187
|
};
|
|
174
188
|
var DEFAULT_REVIEW_SECTION = {
|
|
175
189
|
...DEFAULT_FEATURE_CONFIG,
|
|
176
190
|
trigger: DEFAULT_TRIGGER,
|
|
177
191
|
reviewer: { whitelist: [], blacklist: [] },
|
|
178
|
-
summarizer: { whitelist: [], blacklist: [], preferred: [] }
|
|
192
|
+
summarizer: { whitelist: [], blacklist: [], preferred: [], preferredModels: [] }
|
|
179
193
|
};
|
|
180
194
|
function toGithubEntity(name) {
|
|
181
195
|
return { github: name };
|
|
@@ -184,27 +198,30 @@ function parseSummarizerSection(raw) {
|
|
|
184
198
|
const defaults = {
|
|
185
199
|
whitelist: [],
|
|
186
200
|
blacklist: [],
|
|
187
|
-
preferred: []
|
|
201
|
+
preferred: [],
|
|
202
|
+
preferredModels: []
|
|
188
203
|
};
|
|
189
204
|
if (typeof raw === "string") {
|
|
190
205
|
return { ...defaults, preferred: [toGithubEntity(raw)] };
|
|
191
206
|
}
|
|
192
207
|
if (!isObject(raw))
|
|
193
208
|
return defaults;
|
|
209
|
+
const preferredModels = parseStringArray(raw.preferred_models);
|
|
194
210
|
if (raw.only !== void 0) {
|
|
195
211
|
if (typeof raw.only === "string") {
|
|
196
|
-
return { ...defaults, whitelist: [toGithubEntity(raw.only)] };
|
|
212
|
+
return { ...defaults, whitelist: [toGithubEntity(raw.only)], preferredModels };
|
|
197
213
|
}
|
|
198
214
|
if (Array.isArray(raw.only)) {
|
|
199
215
|
const entries = raw.only.filter((v) => typeof v === "string").map((v) => toGithubEntity(v));
|
|
200
|
-
return { ...defaults, whitelist: entries };
|
|
216
|
+
return { ...defaults, whitelist: entries, preferredModels };
|
|
201
217
|
}
|
|
202
|
-
return defaults;
|
|
218
|
+
return { ...defaults, preferredModels };
|
|
203
219
|
}
|
|
204
220
|
return {
|
|
205
221
|
whitelist: parseEntityList(raw.whitelist),
|
|
206
222
|
blacklist: parseEntityList(raw.blacklist),
|
|
207
|
-
preferred: parseEntityList(raw.preferred)
|
|
223
|
+
preferred: parseEntityList(raw.preferred),
|
|
224
|
+
preferredModels
|
|
208
225
|
};
|
|
209
226
|
}
|
|
210
227
|
function parseAgentSlots(value) {
|
|
@@ -235,6 +252,7 @@ function parseFeatureFields(raw, defaults) {
|
|
|
235
252
|
timeout: parseTimeout(raw.timeout ?? defaults.timeout),
|
|
236
253
|
preferredModels: parseStringArray(raw.preferred_models ?? defaults.preferredModels),
|
|
237
254
|
preferredTools: parseStringArray(raw.preferred_tools ?? defaults.preferredTools),
|
|
255
|
+
modelDiversityGraceMs: parseDurationSeconds(raw.model_diversity_grace, defaults.modelDiversityGraceMs),
|
|
238
256
|
...agentSlots ? { agents: agentSlots } : {}
|
|
239
257
|
};
|
|
240
258
|
}
|
|
@@ -261,7 +279,8 @@ var DEFAULT_DEDUP_FEATURE = {
|
|
|
261
279
|
agentCount: 1,
|
|
262
280
|
timeout: "10m",
|
|
263
281
|
preferredModels: [],
|
|
264
|
-
preferredTools: []
|
|
282
|
+
preferredTools: [],
|
|
283
|
+
modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
|
|
265
284
|
};
|
|
266
285
|
function parseDedupTarget(raw) {
|
|
267
286
|
const base = parseFeatureFields(raw, DEFAULT_DEDUP_FEATURE);
|
|
@@ -291,7 +310,8 @@ var DEFAULT_TRIAGE_FEATURE = {
|
|
|
291
310
|
agentCount: 1,
|
|
292
311
|
timeout: "10m",
|
|
293
312
|
preferredModels: [],
|
|
294
|
-
preferredTools: []
|
|
313
|
+
preferredTools: [],
|
|
314
|
+
modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
|
|
295
315
|
};
|
|
296
316
|
function parseTriageSection(raw) {
|
|
297
317
|
const base = parseFeatureFields(raw, DEFAULT_TRIAGE_FEATURE);
|
|
@@ -360,6 +380,7 @@ function parseLegacyReviewConfig(raw) {
|
|
|
360
380
|
timeout: parseTimeout(raw.timeout),
|
|
361
381
|
preferredModels: parseStringArray(agentsRaw.preferred_models),
|
|
362
382
|
preferredTools: parseStringArray(agentsRaw.preferred_tools),
|
|
383
|
+
modelDiversityGraceMs: parseDurationSeconds(raw.model_diversity_grace ?? agentsRaw.model_diversity_grace, DEFAULT_MODEL_DIVERSITY_GRACE_MS),
|
|
363
384
|
trigger: {
|
|
364
385
|
on: Array.isArray(triggerRaw.on) ? triggerRaw.on.filter((v) => typeof v === "string") : DEFAULT_TRIGGER.on,
|
|
365
386
|
comment: typeof triggerRaw.comment === "string" ? triggerRaw.comment : DEFAULT_TRIGGER.comment,
|
|
@@ -387,8 +408,12 @@ function ensureConfigDir() {
|
|
|
387
408
|
}
|
|
388
409
|
var DEFAULT_MAX_DIFF_SIZE_KB = 100;
|
|
389
410
|
var DEFAULT_MAX_CONSECUTIVE_ERRORS = 10;
|
|
390
|
-
var VALID_REPO_MODES = ["
|
|
411
|
+
var VALID_REPO_MODES = ["public", "private", "whitelist", "blacklist"];
|
|
391
412
|
var REPO_PATTERN = /^[^/]+\/[^/]+$/;
|
|
413
|
+
var REPO_MODE_ALIASES = {
|
|
414
|
+
all: "public",
|
|
415
|
+
own: "private"
|
|
416
|
+
};
|
|
392
417
|
var RepoConfigError = class extends Error {
|
|
393
418
|
constructor(message) {
|
|
394
419
|
super(message);
|
|
@@ -412,10 +437,17 @@ function parseRepoConfig(obj, index, field = "repos") {
|
|
|
412
437
|
throw new RepoConfigError(`agents[${index}].${field} must be an object`);
|
|
413
438
|
}
|
|
414
439
|
const reposObj = raw;
|
|
415
|
-
|
|
440
|
+
let mode = reposObj.mode;
|
|
416
441
|
if (mode === void 0) {
|
|
417
442
|
throw new RepoConfigError(`agents[${index}].${field}.mode is required`);
|
|
418
443
|
}
|
|
444
|
+
if (typeof mode === "string" && Object.hasOwn(REPO_MODE_ALIASES, mode)) {
|
|
445
|
+
const resolved = REPO_MODE_ALIASES[mode];
|
|
446
|
+
console.warn(
|
|
447
|
+
`\u26A0 Config warning: agents[${index}].${field}.mode "${mode}" is deprecated, use "${resolved}" instead`
|
|
448
|
+
);
|
|
449
|
+
mode = resolved;
|
|
450
|
+
}
|
|
419
451
|
if (typeof mode !== "string" || !VALID_REPO_MODES.includes(mode)) {
|
|
420
452
|
throw new RepoConfigError(
|
|
421
453
|
`agents[${index}].${field}.mode must be one of: ${VALID_REPO_MODES.join(", ")}`
|
|
@@ -509,6 +541,15 @@ function parseAgents(data) {
|
|
|
509
541
|
);
|
|
510
542
|
}
|
|
511
543
|
if (typeof obj.codebase_dir === "string") agent.codebase_dir = obj.codebase_dir;
|
|
544
|
+
if (typeof obj.instances === "number") {
|
|
545
|
+
if (!Number.isInteger(obj.instances) || obj.instances < 1) {
|
|
546
|
+
console.warn(
|
|
547
|
+
`\u26A0 Config warning: agents[${i}].instances must be a positive integer, got ${obj.instances}. Value ignored.`
|
|
548
|
+
);
|
|
549
|
+
} else {
|
|
550
|
+
agent.instances = obj.instances;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
512
553
|
const repoConfig = parseRepoConfig(obj, i);
|
|
513
554
|
if (repoConfig) agent.repos = repoConfig;
|
|
514
555
|
const synthesizeRepoConfig = parseRepoConfig(obj, i, "synthesize_repos");
|
|
@@ -568,6 +609,7 @@ function loadConfig() {
|
|
|
568
609
|
maxDiffSizeKb: DEFAULT_MAX_DIFF_SIZE_KB,
|
|
569
610
|
maxConsecutiveErrors: DEFAULT_MAX_CONSECUTIVE_ERRORS,
|
|
570
611
|
codebaseDir: null,
|
|
612
|
+
codebaseTtl: null,
|
|
571
613
|
agentCommand: null,
|
|
572
614
|
agents: null,
|
|
573
615
|
usageLimits: {
|
|
@@ -611,6 +653,7 @@ function loadConfig() {
|
|
|
611
653
|
maxDiffSizeKb: overrides.maxDiffSizeKb ?? (typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB),
|
|
612
654
|
maxConsecutiveErrors: overrides.maxConsecutiveErrors ?? (typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS),
|
|
613
655
|
codebaseDir: typeof data.codebase_dir === "string" ? data.codebase_dir : null,
|
|
656
|
+
codebaseTtl: typeof data.codebase_ttl === "string" ? data.codebase_ttl : null,
|
|
614
657
|
agentCommand: typeof data.agent_command === "string" ? data.agent_command : null,
|
|
615
658
|
agents: parseAgents(data),
|
|
616
659
|
usageLimits: {
|
|
@@ -629,10 +672,10 @@ function resolveCodebaseDir(agentDir, globalDir) {
|
|
|
629
672
|
return path.resolve(raw);
|
|
630
673
|
}
|
|
631
674
|
|
|
632
|
-
// src/
|
|
633
|
-
import { execFileSync } from "child_process";
|
|
634
|
-
import * as
|
|
635
|
-
import * as
|
|
675
|
+
// src/repo-cache.ts
|
|
676
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
677
|
+
import * as fs3 from "fs";
|
|
678
|
+
import * as path3 from "path";
|
|
636
679
|
|
|
637
680
|
// src/sanitize.ts
|
|
638
681
|
var GITHUB_TOKEN_PATTERN = /\b(ghp_[A-Za-z0-9_]{1,255}|gho_[A-Za-z0-9_]{1,255}|ghs_[A-Za-z0-9_]{1,255}|ghr_[A-Za-z0-9_]{1,255}|github_pat_[A-Za-z0-9_]{1,255})\b/g;
|
|
@@ -643,47 +686,10 @@ function sanitizeTokens(input) {
|
|
|
643
686
|
}
|
|
644
687
|
|
|
645
688
|
// src/codebase.ts
|
|
689
|
+
import { execFileSync } from "child_process";
|
|
690
|
+
import * as fs2 from "fs";
|
|
691
|
+
import * as path2 from "path";
|
|
646
692
|
var VALID_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
647
|
-
var GH_CREDENTIAL_HELPER = "!gh auth git-credential";
|
|
648
|
-
function cloneOrUpdate(owner, repo, prNumber, baseDir, taskId) {
|
|
649
|
-
validatePathSegment(owner, "owner");
|
|
650
|
-
validatePathSegment(repo, "repo");
|
|
651
|
-
if (taskId) {
|
|
652
|
-
validatePathSegment(taskId, "taskId");
|
|
653
|
-
}
|
|
654
|
-
const repoDir = taskId ? path2.join(baseDir, owner, repo, taskId) : path2.join(baseDir, owner, repo);
|
|
655
|
-
const ghAvailable = isGhAvailable();
|
|
656
|
-
let cloned = false;
|
|
657
|
-
if (!fs2.existsSync(path2.join(repoDir, ".git"))) {
|
|
658
|
-
if (ghAvailable) {
|
|
659
|
-
ghClone(owner, repo, repoDir);
|
|
660
|
-
} else {
|
|
661
|
-
fs2.mkdirSync(repoDir, { recursive: true });
|
|
662
|
-
const cloneUrl = buildCloneUrl(owner, repo);
|
|
663
|
-
git(["clone", "--depth", "1", cloneUrl, repoDir]);
|
|
664
|
-
}
|
|
665
|
-
cloned = true;
|
|
666
|
-
}
|
|
667
|
-
const credArgs = ghAvailable ? ["-c", `credential.helper=${GH_CREDENTIAL_HELPER}`] : [];
|
|
668
|
-
git(
|
|
669
|
-
[...credArgs, "fetch", "--force", "--depth", "1", "origin", `pull/${prNumber}/head`],
|
|
670
|
-
repoDir
|
|
671
|
-
);
|
|
672
|
-
git(["checkout", "FETCH_HEAD"], repoDir);
|
|
673
|
-
return { localPath: repoDir, cloned };
|
|
674
|
-
}
|
|
675
|
-
function cleanupTaskDir(dirPath) {
|
|
676
|
-
if (!path2.isAbsolute(dirPath) || dirPath.split(path2.sep).filter(Boolean).length < 3) {
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
try {
|
|
680
|
-
fs2.rmSync(dirPath, { recursive: true, force: true });
|
|
681
|
-
} catch (err) {
|
|
682
|
-
if (err?.code !== "ENOENT") {
|
|
683
|
-
console.warn(`[cleanup] Failed to remove ${dirPath}: ${err.message}`);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
693
|
function validatePathSegment(segment, name) {
|
|
688
694
|
if (!VALID_NAME_PATTERN.test(segment) || segment === "." || segment === "..") {
|
|
689
695
|
throw new Error(`Invalid ${name}: '${segment}' contains disallowed characters`);
|
|
@@ -704,24 +710,111 @@ function isGhAvailable() {
|
|
|
704
710
|
return false;
|
|
705
711
|
}
|
|
706
712
|
}
|
|
707
|
-
|
|
713
|
+
|
|
714
|
+
// src/repo-cache.ts
|
|
715
|
+
var GH_CREDENTIAL_HELPER = "!gh auth git-credential";
|
|
716
|
+
var GIT_TIMEOUT_MS = 12e4;
|
|
717
|
+
var repoLocks = /* @__PURE__ */ new Map();
|
|
718
|
+
async function withRepoLock(repoKey, fn) {
|
|
719
|
+
const existing = repoLocks.get(repoKey);
|
|
720
|
+
let release;
|
|
721
|
+
const gate = new Promise((resolve2) => {
|
|
722
|
+
release = resolve2;
|
|
723
|
+
});
|
|
724
|
+
repoLocks.set(repoKey, gate);
|
|
708
725
|
try {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
throw new Error(sanitizeTokens(message));
|
|
726
|
+
if (existing) await existing;
|
|
727
|
+
return await fn();
|
|
728
|
+
} finally {
|
|
729
|
+
release();
|
|
730
|
+
if (repoLocks.get(repoKey) === gate) {
|
|
731
|
+
repoLocks.delete(repoKey);
|
|
732
|
+
}
|
|
717
733
|
}
|
|
718
734
|
}
|
|
719
|
-
function
|
|
735
|
+
function ensureBareClone(owner, repo, baseDir, ghAvailable) {
|
|
736
|
+
validatePathSegment(owner, "owner");
|
|
737
|
+
validatePathSegment(repo, "repo");
|
|
738
|
+
const bareRepoPath = path3.join(baseDir, owner, `${repo}.git`);
|
|
739
|
+
if (fs3.existsSync(path3.join(bareRepoPath, "HEAD"))) {
|
|
740
|
+
return { bareRepoPath, cloned: false };
|
|
741
|
+
}
|
|
742
|
+
fs3.mkdirSync(path3.join(baseDir, owner), { recursive: true });
|
|
743
|
+
if (ghAvailable) {
|
|
744
|
+
gitExec("gh", [
|
|
745
|
+
"repo",
|
|
746
|
+
"clone",
|
|
747
|
+
`${owner}/${repo}`,
|
|
748
|
+
bareRepoPath,
|
|
749
|
+
"--",
|
|
750
|
+
"--bare",
|
|
751
|
+
"--filter=blob:none"
|
|
752
|
+
]);
|
|
753
|
+
} else {
|
|
754
|
+
const cloneUrl = buildCloneUrl(owner, repo);
|
|
755
|
+
gitExec("git", ["clone", "--bare", "--filter=blob:none", cloneUrl, bareRepoPath]);
|
|
756
|
+
}
|
|
757
|
+
return { bareRepoPath, cloned: true };
|
|
758
|
+
}
|
|
759
|
+
function fetchPRRef(bareRepoPath, prNumber, ghAvailable) {
|
|
760
|
+
const credArgs = ghAvailable ? ["-c", `credential.helper=${GH_CREDENTIAL_HELPER}`] : [];
|
|
761
|
+
gitExec(
|
|
762
|
+
"git",
|
|
763
|
+
[...credArgs, "fetch", "--force", "origin", `pull/${prNumber}/head`],
|
|
764
|
+
bareRepoPath
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
function addWorktree(bareRepoPath, taskId) {
|
|
768
|
+
validatePathSegment(taskId, "taskId");
|
|
769
|
+
const repoName = path3.basename(bareRepoPath, ".git");
|
|
770
|
+
const worktreeBase = path3.join(path3.dirname(bareRepoPath), `${repoName}-worktrees`);
|
|
771
|
+
const worktreePath = path3.join(worktreeBase, taskId);
|
|
772
|
+
fs3.mkdirSync(worktreeBase, { recursive: true });
|
|
773
|
+
gitExec("git", ["worktree", "add", "--detach", worktreePath, "FETCH_HEAD"], bareRepoPath);
|
|
774
|
+
return worktreePath;
|
|
775
|
+
}
|
|
776
|
+
function removeWorktree(bareRepoPath, worktreePath) {
|
|
777
|
+
try {
|
|
778
|
+
gitExec("git", ["worktree", "remove", "--force", worktreePath], bareRepoPath);
|
|
779
|
+
} catch {
|
|
780
|
+
try {
|
|
781
|
+
fs3.rmSync(worktreePath, { recursive: true, force: true });
|
|
782
|
+
gitExec("git", ["worktree", "prune"], bareRepoPath);
|
|
783
|
+
} catch {
|
|
784
|
+
console.warn(`[repo-cache] Failed to clean up worktree: ${worktreePath}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function repoKeyFromBarePath(bareRepoPath) {
|
|
789
|
+
const repoName = path3.basename(bareRepoPath, ".git");
|
|
790
|
+
const owner = path3.basename(path3.dirname(bareRepoPath));
|
|
791
|
+
return `${owner}/${repoName}`;
|
|
792
|
+
}
|
|
793
|
+
async function checkoutWorktree(owner, repo, prNumber, baseDir, taskId) {
|
|
794
|
+
validatePathSegment(owner, "owner");
|
|
795
|
+
validatePathSegment(repo, "repo");
|
|
796
|
+
validatePathSegment(taskId, "taskId");
|
|
797
|
+
const repoKey = `${owner}/${repo}`;
|
|
798
|
+
const ghAvailable = isGhAvailable();
|
|
799
|
+
return withRepoLock(repoKey, () => {
|
|
800
|
+
const { bareRepoPath, cloned } = ensureBareClone(owner, repo, baseDir, ghAvailable);
|
|
801
|
+
fetchPRRef(bareRepoPath, prNumber, ghAvailable);
|
|
802
|
+
const worktreePath = addWorktree(bareRepoPath, taskId);
|
|
803
|
+
return { worktreePath, bareRepoPath, cloned };
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
async function cleanupWorktree(bareRepoPath, worktreePath) {
|
|
807
|
+
const repoKey = repoKeyFromBarePath(bareRepoPath);
|
|
808
|
+
await withRepoLock(repoKey, () => {
|
|
809
|
+
removeWorktree(bareRepoPath, worktreePath);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
function gitExec(command, args, cwd) {
|
|
720
813
|
try {
|
|
721
|
-
return
|
|
814
|
+
return execFileSync2(command, args, {
|
|
722
815
|
cwd,
|
|
723
816
|
encoding: "utf-8",
|
|
724
|
-
timeout:
|
|
817
|
+
timeout: GIT_TIMEOUT_MS,
|
|
725
818
|
stdio: ["ignore", "pipe", "pipe"]
|
|
726
819
|
});
|
|
727
820
|
} catch (err) {
|
|
@@ -730,20 +823,170 @@ function git(args, cwd) {
|
|
|
730
823
|
}
|
|
731
824
|
}
|
|
732
825
|
|
|
826
|
+
// src/codebase-cleanup.ts
|
|
827
|
+
import * as fs4 from "fs";
|
|
828
|
+
import * as path4 from "path";
|
|
829
|
+
var DEFAULT_CODEBASE_TTL_MS = 30 * 60 * 1e3;
|
|
830
|
+
function parseTtl(value) {
|
|
831
|
+
const trimmed = value.trim();
|
|
832
|
+
if (trimmed === "0") return 0;
|
|
833
|
+
const match = trimmed.match(/^(\d+)\s*(ms|s|m|h|d)$/);
|
|
834
|
+
if (match) {
|
|
835
|
+
const num = parseInt(match[1], 10);
|
|
836
|
+
switch (match[2]) {
|
|
837
|
+
case "ms":
|
|
838
|
+
return num;
|
|
839
|
+
case "s":
|
|
840
|
+
return num * 1e3;
|
|
841
|
+
case "m":
|
|
842
|
+
return num * 60 * 1e3;
|
|
843
|
+
case "h":
|
|
844
|
+
return num * 60 * 60 * 1e3;
|
|
845
|
+
case "d":
|
|
846
|
+
return num * 24 * 60 * 60 * 1e3;
|
|
847
|
+
default:
|
|
848
|
+
throw new Error(`Unreachable: unhandled unit "${match[2]}"`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (/^\d+$/.test(trimmed)) {
|
|
852
|
+
return parseInt(trimmed, 10) * 1e3;
|
|
853
|
+
}
|
|
854
|
+
throw new Error(`Invalid codebase_ttl: "${value}". Use "0", "30m", "2h", "24h", "1d", etc.`);
|
|
855
|
+
}
|
|
856
|
+
var CodebaseCleanupTracker = class {
|
|
857
|
+
pending = [];
|
|
858
|
+
ttlMs;
|
|
859
|
+
constructor(ttlMs) {
|
|
860
|
+
this.ttlMs = ttlMs;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Record a completed task's worktree for deferred cleanup.
|
|
864
|
+
*/
|
|
865
|
+
track(bareRepoPath, worktreePath) {
|
|
866
|
+
this.pending.push({
|
|
867
|
+
bareRepoPath,
|
|
868
|
+
worktreePath,
|
|
869
|
+
completedAt: Date.now()
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Check for and remove any worktrees that have exceeded the TTL.
|
|
874
|
+
* Returns the number of directories cleaned up.
|
|
875
|
+
*
|
|
876
|
+
* The removeFn callback performs the actual git worktree removal.
|
|
877
|
+
*/
|
|
878
|
+
async sweep(removeFn) {
|
|
879
|
+
const now = Date.now();
|
|
880
|
+
const expired = [];
|
|
881
|
+
const remaining = [];
|
|
882
|
+
for (const entry of this.pending) {
|
|
883
|
+
if (now - entry.completedAt >= this.ttlMs) {
|
|
884
|
+
expired.push(entry);
|
|
885
|
+
} else {
|
|
886
|
+
remaining.push(entry);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
this.pending = remaining;
|
|
890
|
+
let cleaned = 0;
|
|
891
|
+
for (const entry of expired) {
|
|
892
|
+
try {
|
|
893
|
+
await removeFn(entry.bareRepoPath, entry.worktreePath);
|
|
894
|
+
cleaned++;
|
|
895
|
+
} catch {
|
|
896
|
+
this.pending.push(entry);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return cleaned;
|
|
900
|
+
}
|
|
901
|
+
/** Number of entries pending cleanup. */
|
|
902
|
+
get size() {
|
|
903
|
+
return this.pending.length;
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
function scanAndCleanStaleWorktrees(baseDir, ttlMs) {
|
|
907
|
+
if (!fs4.existsSync(baseDir)) return 0;
|
|
908
|
+
const now = Date.now();
|
|
909
|
+
let cleaned = 0;
|
|
910
|
+
let ownerDirs;
|
|
911
|
+
try {
|
|
912
|
+
ownerDirs = fs4.readdirSync(baseDir);
|
|
913
|
+
} catch {
|
|
914
|
+
return 0;
|
|
915
|
+
}
|
|
916
|
+
for (const ownerName of ownerDirs) {
|
|
917
|
+
const ownerPath = path4.join(baseDir, ownerName);
|
|
918
|
+
let stat;
|
|
919
|
+
try {
|
|
920
|
+
stat = fs4.statSync(ownerPath);
|
|
921
|
+
} catch {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (!stat.isDirectory()) continue;
|
|
925
|
+
let entries;
|
|
926
|
+
try {
|
|
927
|
+
entries = fs4.readdirSync(ownerPath);
|
|
928
|
+
} catch {
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
for (const entry of entries) {
|
|
932
|
+
if (!entry.endsWith("-worktrees")) continue;
|
|
933
|
+
const worktreeBasePath = path4.join(ownerPath, entry);
|
|
934
|
+
let worktreeStat;
|
|
935
|
+
try {
|
|
936
|
+
worktreeStat = fs4.statSync(worktreeBasePath);
|
|
937
|
+
} catch {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (!worktreeStat.isDirectory()) continue;
|
|
941
|
+
let taskDirs;
|
|
942
|
+
try {
|
|
943
|
+
taskDirs = fs4.readdirSync(worktreeBasePath);
|
|
944
|
+
} catch {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
for (const taskId of taskDirs) {
|
|
948
|
+
const taskPath = path4.join(worktreeBasePath, taskId);
|
|
949
|
+
let taskStat;
|
|
950
|
+
try {
|
|
951
|
+
taskStat = fs4.statSync(taskPath);
|
|
952
|
+
} catch {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (!taskStat.isDirectory()) continue;
|
|
956
|
+
const age = now - taskStat.mtimeMs;
|
|
957
|
+
if (age >= ttlMs) {
|
|
958
|
+
try {
|
|
959
|
+
fs4.rmSync(taskPath, { recursive: true, force: true });
|
|
960
|
+
const repoName = entry.replace(/-worktrees$/, "");
|
|
961
|
+
const metadataPath = path4.join(ownerPath, `${repoName}.git`, "worktrees", taskId);
|
|
962
|
+
try {
|
|
963
|
+
fs4.rmSync(metadataPath, { recursive: true, force: true });
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
cleaned++;
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return cleaned;
|
|
974
|
+
}
|
|
975
|
+
|
|
733
976
|
// src/auth.ts
|
|
734
|
-
import * as
|
|
735
|
-
import * as
|
|
977
|
+
import * as fs5 from "fs";
|
|
978
|
+
import * as path5 from "path";
|
|
736
979
|
import * as os2 from "os";
|
|
737
980
|
import * as crypto from "crypto";
|
|
738
|
-
var AUTH_DIR =
|
|
981
|
+
var AUTH_DIR = path5.join(os2.homedir(), ".opencara");
|
|
739
982
|
function getAuthFilePath() {
|
|
740
983
|
const envPath = process.env.OPENCARA_AUTH_FILE?.trim();
|
|
741
|
-
return envPath ||
|
|
984
|
+
return envPath || path5.join(AUTH_DIR, "auth.json");
|
|
742
985
|
}
|
|
743
986
|
function loadAuth() {
|
|
744
987
|
const filePath = getAuthFilePath();
|
|
745
988
|
try {
|
|
746
|
-
const raw =
|
|
989
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
747
990
|
const data = JSON.parse(raw);
|
|
748
991
|
if (typeof data.access_token === "string" && typeof data.expires_at === "number" && typeof data.github_username === "string" && typeof data.github_user_id === "number" && // refresh_token is optional — tolerate non-refreshable tokens, but validate type when present
|
|
749
992
|
(data.refresh_token === void 0 || typeof data.refresh_token === "string")) {
|
|
@@ -756,15 +999,15 @@ function loadAuth() {
|
|
|
756
999
|
}
|
|
757
1000
|
function saveAuth(auth) {
|
|
758
1001
|
const filePath = getAuthFilePath();
|
|
759
|
-
const dir =
|
|
760
|
-
|
|
761
|
-
const tmpPath =
|
|
1002
|
+
const dir = path5.dirname(filePath);
|
|
1003
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1004
|
+
const tmpPath = path5.join(dir, `.auth-${crypto.randomBytes(8).toString("hex")}.tmp`);
|
|
762
1005
|
try {
|
|
763
|
-
|
|
764
|
-
|
|
1006
|
+
fs5.writeFileSync(tmpPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1007
|
+
fs5.renameSync(tmpPath, filePath);
|
|
765
1008
|
} catch (err) {
|
|
766
1009
|
try {
|
|
767
|
-
|
|
1010
|
+
fs5.unlinkSync(tmpPath);
|
|
768
1011
|
} catch {
|
|
769
1012
|
}
|
|
770
1013
|
throw err;
|
|
@@ -773,7 +1016,7 @@ function saveAuth(auth) {
|
|
|
773
1016
|
function deleteAuth() {
|
|
774
1017
|
const filePath = getAuthFilePath();
|
|
775
1018
|
try {
|
|
776
|
-
|
|
1019
|
+
fs5.unlinkSync(filePath);
|
|
777
1020
|
} catch (err) {
|
|
778
1021
|
if (err.code !== "ENOENT") {
|
|
779
1022
|
throw err;
|
|
@@ -1043,27 +1286,27 @@ var ApiClient = class {
|
|
|
1043
1286
|
clearTimeout(timer);
|
|
1044
1287
|
}
|
|
1045
1288
|
}
|
|
1046
|
-
async get(
|
|
1047
|
-
this.log(`GET ${
|
|
1048
|
-
const res = await this.timedFetch(`${this.baseUrl}${
|
|
1289
|
+
async get(path9) {
|
|
1290
|
+
this.log(`GET ${path9}`);
|
|
1291
|
+
const res = await this.timedFetch(`${this.baseUrl}${path9}`, {
|
|
1049
1292
|
method: "GET",
|
|
1050
1293
|
headers: this.headers()
|
|
1051
1294
|
});
|
|
1052
|
-
return this.handleResponse(res,
|
|
1295
|
+
return this.handleResponse(res, path9, "GET");
|
|
1053
1296
|
}
|
|
1054
|
-
async post(
|
|
1055
|
-
this.log(`POST ${
|
|
1056
|
-
const res = await this.timedFetch(`${this.baseUrl}${
|
|
1297
|
+
async post(path9, body) {
|
|
1298
|
+
this.log(`POST ${path9}`);
|
|
1299
|
+
const res = await this.timedFetch(`${this.baseUrl}${path9}`, {
|
|
1057
1300
|
method: "POST",
|
|
1058
1301
|
headers: this.headers(),
|
|
1059
1302
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1060
1303
|
});
|
|
1061
|
-
return this.handleResponse(res,
|
|
1304
|
+
return this.handleResponse(res, path9, "POST", body);
|
|
1062
1305
|
}
|
|
1063
|
-
async handleResponse(res,
|
|
1306
|
+
async handleResponse(res, path9, method, body) {
|
|
1064
1307
|
if (!res.ok) {
|
|
1065
1308
|
const { message, errorCode, minimumVersion } = await this.parseErrorBody(res);
|
|
1066
|
-
this.log(`${res.status} ${message} (${
|
|
1309
|
+
this.log(`${res.status} ${message} (${path9})`);
|
|
1067
1310
|
if (res.status === 426) {
|
|
1068
1311
|
throw new UpgradeRequiredError(this.cliVersion ?? "unknown", minimumVersion);
|
|
1069
1312
|
}
|
|
@@ -1072,12 +1315,12 @@ var ApiClient = class {
|
|
|
1072
1315
|
try {
|
|
1073
1316
|
this.authToken = await this.onTokenRefresh();
|
|
1074
1317
|
this.log("Token refreshed, retrying request");
|
|
1075
|
-
const retryRes = await this.timedFetch(`${this.baseUrl}${
|
|
1318
|
+
const retryRes = await this.timedFetch(`${this.baseUrl}${path9}`, {
|
|
1076
1319
|
method,
|
|
1077
1320
|
headers: this.headers(),
|
|
1078
1321
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1079
1322
|
});
|
|
1080
|
-
return this.handleRetryResponse(retryRes,
|
|
1323
|
+
return this.handleRetryResponse(retryRes, path9);
|
|
1081
1324
|
} catch (refreshErr) {
|
|
1082
1325
|
this.log(`Token refresh failed: ${refreshErr.message}`);
|
|
1083
1326
|
throw new HttpError(res.status, message, errorCode);
|
|
@@ -1085,20 +1328,20 @@ var ApiClient = class {
|
|
|
1085
1328
|
}
|
|
1086
1329
|
throw new HttpError(res.status, message, errorCode);
|
|
1087
1330
|
}
|
|
1088
|
-
this.log(`${res.status} OK (${
|
|
1331
|
+
this.log(`${res.status} OK (${path9})`);
|
|
1089
1332
|
return await res.json();
|
|
1090
1333
|
}
|
|
1091
1334
|
/** Handle response for a retry after token refresh — no second refresh attempt. */
|
|
1092
|
-
async handleRetryResponse(res,
|
|
1335
|
+
async handleRetryResponse(res, path9) {
|
|
1093
1336
|
if (!res.ok) {
|
|
1094
1337
|
const { message, errorCode, minimumVersion } = await this.parseErrorBody(res);
|
|
1095
|
-
this.log(`${res.status} ${message} (${
|
|
1338
|
+
this.log(`${res.status} ${message} (${path9}) [retry]`);
|
|
1096
1339
|
if (res.status === 426) {
|
|
1097
1340
|
throw new UpgradeRequiredError(this.cliVersion ?? "unknown", minimumVersion);
|
|
1098
1341
|
}
|
|
1099
1342
|
throw new HttpError(res.status, message, errorCode);
|
|
1100
1343
|
}
|
|
1101
|
-
this.log(`${res.status} OK (${
|
|
1344
|
+
this.log(`${res.status} OK (${path9}) [retry]`);
|
|
1102
1345
|
return await res.json();
|
|
1103
1346
|
}
|
|
1104
1347
|
};
|
|
@@ -1153,9 +1396,9 @@ function sleep(ms, signal) {
|
|
|
1153
1396
|
}
|
|
1154
1397
|
|
|
1155
1398
|
// src/tool-executor.ts
|
|
1156
|
-
import { spawn, execFileSync as
|
|
1157
|
-
import * as
|
|
1158
|
-
import * as
|
|
1399
|
+
import { spawn, execFileSync as execFileSync3 } from "child_process";
|
|
1400
|
+
import * as fs6 from "fs";
|
|
1401
|
+
import * as path6 from "path";
|
|
1159
1402
|
var ToolTimeoutError = class extends Error {
|
|
1160
1403
|
constructor(message) {
|
|
1161
1404
|
super(message);
|
|
@@ -1167,9 +1410,9 @@ var MIN_PARTIAL_RESULT_LENGTH = 50;
|
|
|
1167
1410
|
var MAX_STDERR_LENGTH = 1e3;
|
|
1168
1411
|
function validateCommandBinary(commandTemplate) {
|
|
1169
1412
|
const { command } = parseCommandTemplate(commandTemplate);
|
|
1170
|
-
if (
|
|
1413
|
+
if (path6.isAbsolute(command)) {
|
|
1171
1414
|
try {
|
|
1172
|
-
|
|
1415
|
+
fs6.accessSync(command, fs6.constants.X_OK);
|
|
1173
1416
|
return true;
|
|
1174
1417
|
} catch {
|
|
1175
1418
|
return false;
|
|
@@ -1178,9 +1421,9 @@ function validateCommandBinary(commandTemplate) {
|
|
|
1178
1421
|
try {
|
|
1179
1422
|
const isWindows = process.platform === "win32";
|
|
1180
1423
|
if (isWindows) {
|
|
1181
|
-
|
|
1424
|
+
execFileSync3("where", [command], { stdio: "pipe" });
|
|
1182
1425
|
} else {
|
|
1183
|
-
|
|
1426
|
+
execFileSync3("sh", ["-c", 'command -v -- "$1"', "_", command], { stdio: "pipe" });
|
|
1184
1427
|
}
|
|
1185
1428
|
return true;
|
|
1186
1429
|
} catch {
|
|
@@ -1537,7 +1780,10 @@ ${userMessage}`;
|
|
|
1537
1780
|
verdict,
|
|
1538
1781
|
tokensUsed: result.tokensUsed + inputTokens,
|
|
1539
1782
|
tokensEstimated: !result.tokensParsed,
|
|
1540
|
-
tokenDetail
|
|
1783
|
+
tokenDetail,
|
|
1784
|
+
toolStdout: result.stdout,
|
|
1785
|
+
toolStderr: result.stderr,
|
|
1786
|
+
promptLength: fullPrompt.length
|
|
1541
1787
|
};
|
|
1542
1788
|
} finally {
|
|
1543
1789
|
clearTimeout(abortTimer);
|
|
@@ -1716,7 +1962,10 @@ ${userMessage}`;
|
|
|
1716
1962
|
tokensUsed: result.tokensUsed + inputTokens,
|
|
1717
1963
|
tokensEstimated: !result.tokensParsed,
|
|
1718
1964
|
tokenDetail,
|
|
1719
|
-
flaggedReviews
|
|
1965
|
+
flaggedReviews,
|
|
1966
|
+
toolStdout: result.stdout,
|
|
1967
|
+
toolStderr: result.stderr,
|
|
1968
|
+
promptLength: fullPrompt.length
|
|
1720
1969
|
};
|
|
1721
1970
|
} finally {
|
|
1722
1971
|
clearTimeout(abortTimer);
|
|
@@ -1914,9 +2163,9 @@ function formatPostReviewStats(session) {
|
|
|
1914
2163
|
}
|
|
1915
2164
|
|
|
1916
2165
|
// src/usage-tracker.ts
|
|
1917
|
-
import * as
|
|
1918
|
-
import * as
|
|
1919
|
-
var USAGE_FILE =
|
|
2166
|
+
import * as fs7 from "fs";
|
|
2167
|
+
import * as path7 from "path";
|
|
2168
|
+
var USAGE_FILE = path7.join(CONFIG_DIR, "usage.json");
|
|
1920
2169
|
var MAX_HISTORY_DAYS = 30;
|
|
1921
2170
|
var WARNING_THRESHOLD = 0.8;
|
|
1922
2171
|
function todayKey() {
|
|
@@ -1939,8 +2188,8 @@ var UsageTracker = class {
|
|
|
1939
2188
|
}
|
|
1940
2189
|
load() {
|
|
1941
2190
|
try {
|
|
1942
|
-
if (
|
|
1943
|
-
const raw =
|
|
2191
|
+
if (fs7.existsSync(this.filePath)) {
|
|
2192
|
+
const raw = fs7.readFileSync(this.filePath, "utf-8");
|
|
1944
2193
|
const parsed = JSON.parse(raw);
|
|
1945
2194
|
if (parsed && Array.isArray(parsed.days)) {
|
|
1946
2195
|
return parsed;
|
|
@@ -1952,7 +2201,7 @@ var UsageTracker = class {
|
|
|
1952
2201
|
}
|
|
1953
2202
|
save() {
|
|
1954
2203
|
ensureConfigDir();
|
|
1955
|
-
|
|
2204
|
+
fs7.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), {
|
|
1956
2205
|
encoding: "utf-8",
|
|
1957
2206
|
mode: 384
|
|
1958
2207
|
});
|
|
@@ -2156,6 +2405,36 @@ function createLogger(label) {
|
|
|
2156
2405
|
logWarn: (msg) => console.warn(`${pc.dim(`[${timestamp()}]`)}${labelStr} ${pc.yellow(sanitizeTokens(msg))}`)
|
|
2157
2406
|
};
|
|
2158
2407
|
}
|
|
2408
|
+
var VERBOSE_TRUNCATE_LIMIT = 2e3;
|
|
2409
|
+
var CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
2410
|
+
function logVerboseToolOutput(logger, label, stdout, stderr, promptLength, limit = VERBOSE_TRUNCATE_LIMIT) {
|
|
2411
|
+
const estimatedTokens = Math.ceil(promptLength / CHARS_PER_TOKEN_ESTIMATE);
|
|
2412
|
+
logger.log(
|
|
2413
|
+
`${icons.info} [verbose] ${label} \u2014 prompt: ${promptLength} chars (~${estimatedTokens} tokens)`
|
|
2414
|
+
);
|
|
2415
|
+
if (stdout) {
|
|
2416
|
+
const truncated = stdout.length > limit ? stdout.slice(0, limit) + `
|
|
2417
|
+
... (truncated at ${limit} chars)` : stdout;
|
|
2418
|
+
logger.log(
|
|
2419
|
+
`${icons.info} [verbose] ${label} stdout (${stdout.length} chars):
|
|
2420
|
+
---
|
|
2421
|
+
${truncated}
|
|
2422
|
+
---`
|
|
2423
|
+
);
|
|
2424
|
+
} else {
|
|
2425
|
+
logger.log(`${icons.info} [verbose] ${label} stdout: (empty)`);
|
|
2426
|
+
}
|
|
2427
|
+
if (stderr) {
|
|
2428
|
+
const truncated = stderr.length > limit ? stderr.slice(0, limit) + `
|
|
2429
|
+
... (truncated at ${limit} chars)` : stderr;
|
|
2430
|
+
logger.log(
|
|
2431
|
+
`${icons.info} [verbose] ${label} stderr (${stderr.length} chars):
|
|
2432
|
+
---
|
|
2433
|
+
${truncated}
|
|
2434
|
+
---`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2159
2438
|
function createAgentSession() {
|
|
2160
2439
|
return {
|
|
2161
2440
|
startTime: Date.now(),
|
|
@@ -2926,7 +3205,9 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
2926
3205
|
repoConfig,
|
|
2927
3206
|
roles,
|
|
2928
3207
|
synthesizeRepos,
|
|
2929
|
-
signal
|
|
3208
|
+
signal,
|
|
3209
|
+
cleanupTracker,
|
|
3210
|
+
verbose
|
|
2930
3211
|
} = options;
|
|
2931
3212
|
const { log, logError, logWarn } = logger;
|
|
2932
3213
|
log(`${icons.polling} Polling every ${pollIntervalMs / 1e3}s...`);
|
|
@@ -2962,6 +3243,14 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
2962
3243
|
const task = eligibleTasks.find(
|
|
2963
3244
|
(t) => (diffFailCounts.get(t.task_id) ?? 0) < MAX_DIFF_FETCH_ATTEMPTS
|
|
2964
3245
|
);
|
|
3246
|
+
if (cleanupTracker) {
|
|
3247
|
+
const swept = await cleanupTracker.sweep(cleanupWorktree);
|
|
3248
|
+
if (swept > 0) {
|
|
3249
|
+
log(
|
|
3250
|
+
`${icons.info} Cleaned up ${swept} stale codebase director${swept === 1 ? "y" : "ies"}`
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
2965
3254
|
if (task) {
|
|
2966
3255
|
const result = await handleTask(
|
|
2967
3256
|
client,
|
|
@@ -2973,7 +3262,9 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
2973
3262
|
logger,
|
|
2974
3263
|
agentSession,
|
|
2975
3264
|
routerRelay,
|
|
2976
|
-
signal
|
|
3265
|
+
signal,
|
|
3266
|
+
cleanupTracker,
|
|
3267
|
+
verbose
|
|
2977
3268
|
);
|
|
2978
3269
|
if (result.diffFetchFailed) {
|
|
2979
3270
|
agentSession.errorsEncountered++;
|
|
@@ -3031,7 +3322,7 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
3031
3322
|
await sleep2(pollIntervalMs, signal);
|
|
3032
3323
|
}
|
|
3033
3324
|
}
|
|
3034
|
-
async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal) {
|
|
3325
|
+
async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal, cleanupTracker, verbose) {
|
|
3035
3326
|
const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt, role } = task;
|
|
3036
3327
|
const { log, logError, logWarn } = logger;
|
|
3037
3328
|
const isIssueTask = pr_number === 0;
|
|
@@ -3068,6 +3359,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3068
3359
|
let diffContent = "";
|
|
3069
3360
|
let taskReviewDeps = reviewDeps;
|
|
3070
3361
|
let taskCheckoutPath = null;
|
|
3362
|
+
let taskBareRepoPath = null;
|
|
3071
3363
|
let contextBlock;
|
|
3072
3364
|
if (isIssueTask) {
|
|
3073
3365
|
log(" Issue-based task \u2014 skipping diff fetch");
|
|
@@ -3091,33 +3383,20 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3091
3383
|
);
|
|
3092
3384
|
return { diffFetchFailed: true };
|
|
3093
3385
|
}
|
|
3094
|
-
|
|
3386
|
+
{
|
|
3387
|
+
const codebaseDir = reviewDeps.codebaseDir || path8.join(CONFIG_DIR, "repos");
|
|
3095
3388
|
try {
|
|
3096
|
-
const result =
|
|
3097
|
-
log(` Codebase ${result.cloned ? "cloned" : "
|
|
3098
|
-
taskCheckoutPath = result.
|
|
3099
|
-
|
|
3389
|
+
const result = await checkoutWorktree(owner, repo, pr_number, codebaseDir, task_id);
|
|
3390
|
+
log(` Codebase ${result.cloned ? "cloned" : "cached"} \u2192 worktree: ${result.worktreePath}`);
|
|
3391
|
+
taskCheckoutPath = result.worktreePath;
|
|
3392
|
+
taskBareRepoPath = result.bareRepoPath;
|
|
3393
|
+
taskReviewDeps = { ...reviewDeps, codebaseDir: result.worktreePath };
|
|
3100
3394
|
} catch (err) {
|
|
3101
3395
|
logWarn(
|
|
3102
|
-
` Warning:
|
|
3396
|
+
` Warning: worktree checkout failed: ${err.message}. Continuing with diff-only review.`
|
|
3103
3397
|
);
|
|
3104
3398
|
taskReviewDeps = { ...reviewDeps, codebaseDir: null };
|
|
3105
3399
|
}
|
|
3106
|
-
} else {
|
|
3107
|
-
try {
|
|
3108
|
-
validatePathSegment(owner, "owner");
|
|
3109
|
-
validatePathSegment(repo, "repo");
|
|
3110
|
-
validatePathSegment(task_id, "task_id");
|
|
3111
|
-
const repoScopedDir = path6.join(CONFIG_DIR, "repos", owner, repo, task_id);
|
|
3112
|
-
fs6.mkdirSync(repoScopedDir, { recursive: true });
|
|
3113
|
-
taskCheckoutPath = repoScopedDir;
|
|
3114
|
-
taskReviewDeps = { ...reviewDeps, codebaseDir: repoScopedDir };
|
|
3115
|
-
log(` Working directory: ${repoScopedDir}`);
|
|
3116
|
-
} catch (err) {
|
|
3117
|
-
logWarn(
|
|
3118
|
-
` Warning: failed to create working directory: ${err.message}. Continuing without scoped cwd.`
|
|
3119
|
-
);
|
|
3120
|
-
}
|
|
3121
3400
|
}
|
|
3122
3401
|
try {
|
|
3123
3402
|
const prContext = await fetchPRContext(owner, repo, pr_number, {
|
|
@@ -3219,7 +3498,8 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3219
3498
|
agentInfo,
|
|
3220
3499
|
routerRelay,
|
|
3221
3500
|
signal,
|
|
3222
|
-
contextBlock
|
|
3501
|
+
contextBlock,
|
|
3502
|
+
verbose
|
|
3223
3503
|
);
|
|
3224
3504
|
} else {
|
|
3225
3505
|
await executeReviewTask(
|
|
@@ -3238,7 +3518,8 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3238
3518
|
agentInfo,
|
|
3239
3519
|
routerRelay,
|
|
3240
3520
|
signal,
|
|
3241
|
-
contextBlock
|
|
3521
|
+
contextBlock,
|
|
3522
|
+
verbose
|
|
3242
3523
|
);
|
|
3243
3524
|
}
|
|
3244
3525
|
agentSession.tasksCompleted++;
|
|
@@ -3252,8 +3533,12 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3252
3533
|
await safeError(client, task_id, agentId, err.message, logger);
|
|
3253
3534
|
}
|
|
3254
3535
|
} finally {
|
|
3255
|
-
if (taskCheckoutPath) {
|
|
3256
|
-
|
|
3536
|
+
if (taskCheckoutPath && taskBareRepoPath) {
|
|
3537
|
+
if (cleanupTracker) {
|
|
3538
|
+
cleanupTracker.track(taskBareRepoPath, taskCheckoutPath);
|
|
3539
|
+
} else {
|
|
3540
|
+
await cleanupWorktree(taskBareRepoPath, taskCheckoutPath);
|
|
3541
|
+
}
|
|
3257
3542
|
}
|
|
3258
3543
|
}
|
|
3259
3544
|
return {};
|
|
@@ -3288,7 +3573,7 @@ async function safeError(client, taskId, agentId, error, logger) {
|
|
|
3288
3573
|
);
|
|
3289
3574
|
}
|
|
3290
3575
|
}
|
|
3291
|
-
async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock) {
|
|
3576
|
+
async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock, verbose) {
|
|
3292
3577
|
if (consumptionDeps.usageLimits?.maxTokensPerReview != null && consumptionDeps.usageTracker) {
|
|
3293
3578
|
const estimatedInput = estimateTokens(diffContent + prompt + (contextBlock ?? ""));
|
|
3294
3579
|
const perReviewCheck = consumptionDeps.usageTracker.checkPerReviewLimit(
|
|
@@ -3354,6 +3639,15 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
|
|
|
3354
3639
|
totalTokens: result.tokensUsed,
|
|
3355
3640
|
estimated: result.tokensEstimated
|
|
3356
3641
|
};
|
|
3642
|
+
if (verbose) {
|
|
3643
|
+
logVerboseToolOutput(
|
|
3644
|
+
logger,
|
|
3645
|
+
"Review",
|
|
3646
|
+
result.toolStdout,
|
|
3647
|
+
result.toolStderr,
|
|
3648
|
+
result.promptLength
|
|
3649
|
+
);
|
|
3650
|
+
}
|
|
3357
3651
|
}
|
|
3358
3652
|
const reviewMeta = {
|
|
3359
3653
|
model: agentInfo.model,
|
|
@@ -3383,7 +3677,7 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
|
|
|
3383
3677
|
logger.log(` ${icons.success} Review submitted (${tokensUsed.toLocaleString()} tokens)`);
|
|
3384
3678
|
logger.log(formatPostReviewStats(consumptionDeps.session));
|
|
3385
3679
|
}
|
|
3386
|
-
async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviews, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock) {
|
|
3680
|
+
async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviews, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock, verbose) {
|
|
3387
3681
|
const meta = { model: agentInfo.model, tool: agentInfo.tool };
|
|
3388
3682
|
if (reviews.length === 0) {
|
|
3389
3683
|
let reviewText;
|
|
@@ -3441,6 +3735,15 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
|
|
|
3441
3735
|
totalTokens: result.tokensUsed,
|
|
3442
3736
|
estimated: result.tokensEstimated
|
|
3443
3737
|
};
|
|
3738
|
+
if (verbose) {
|
|
3739
|
+
logVerboseToolOutput(
|
|
3740
|
+
logger,
|
|
3741
|
+
"Summary (single-agent)",
|
|
3742
|
+
result.toolStdout,
|
|
3743
|
+
result.toolStderr,
|
|
3744
|
+
result.promptLength
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3444
3747
|
}
|
|
3445
3748
|
const headerSingle = buildMetadataHeader(verdict ?? "comment", meta);
|
|
3446
3749
|
const sanitizedReview = sanitizeTokens(headerSingle + reviewText);
|
|
@@ -3534,6 +3837,15 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
|
|
|
3534
3837
|
totalTokens: result.tokensUsed,
|
|
3535
3838
|
estimated: result.tokensEstimated
|
|
3536
3839
|
};
|
|
3840
|
+
if (verbose) {
|
|
3841
|
+
logVerboseToolOutput(
|
|
3842
|
+
logger,
|
|
3843
|
+
"Summary",
|
|
3844
|
+
result.toolStdout,
|
|
3845
|
+
result.toolStderr,
|
|
3846
|
+
result.promptLength
|
|
3847
|
+
);
|
|
3848
|
+
}
|
|
3537
3849
|
}
|
|
3538
3850
|
if (flaggedReviews.length > 0) {
|
|
3539
3851
|
logger.logWarn(
|
|
@@ -3592,7 +3904,7 @@ function sleep2(ms, signal) {
|
|
|
3592
3904
|
async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
|
|
3593
3905
|
const client = new ApiClient(platformUrl, {
|
|
3594
3906
|
authToken: options?.authToken,
|
|
3595
|
-
cliVersion: "0.18.
|
|
3907
|
+
cliVersion: "0.18.5",
|
|
3596
3908
|
versionOverride: options?.versionOverride,
|
|
3597
3909
|
onTokenRefresh: options?.onTokenRefresh
|
|
3598
3910
|
});
|
|
@@ -3617,6 +3929,9 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
|
|
|
3617
3929
|
if (options?.versionOverride) {
|
|
3618
3930
|
log(`${icons.info} Version override active: ${options.versionOverride}`);
|
|
3619
3931
|
}
|
|
3932
|
+
if (options?.verbose) {
|
|
3933
|
+
log(`${icons.info} Verbose mode enabled \u2014 tool stdout/stderr will be logged`);
|
|
3934
|
+
}
|
|
3620
3935
|
if (!reviewDeps) {
|
|
3621
3936
|
logError(`${icons.error} No review command configured. Set command in config.toml`);
|
|
3622
3937
|
return;
|
|
@@ -3630,6 +3945,16 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
|
|
|
3630
3945
|
logWarn(`${icons.warn} Command test failed (${result.error}). Reviews may fail.`);
|
|
3631
3946
|
}
|
|
3632
3947
|
}
|
|
3948
|
+
const ttlMs = options?.codebaseTtl != null ? parseTtl(options.codebaseTtl) : 0;
|
|
3949
|
+
const codebaseDir = reviewDeps.codebaseDir || path8.join(CONFIG_DIR, "repos");
|
|
3950
|
+
const scanTtl = Math.max(ttlMs, DEFAULT_CODEBASE_TTL_MS);
|
|
3951
|
+
const staleCount = scanAndCleanStaleWorktrees(codebaseDir, scanTtl);
|
|
3952
|
+
if (staleCount > 0) {
|
|
3953
|
+
log(
|
|
3954
|
+
`${icons.info} Cleaned up ${staleCount} stale codebase director${staleCount === 1 ? "y" : "ies"} on startup`
|
|
3955
|
+
);
|
|
3956
|
+
}
|
|
3957
|
+
const cleanupTracker = ttlMs > 0 ? new CodebaseCleanupTracker(ttlMs) : void 0;
|
|
3633
3958
|
const abortController = new AbortController();
|
|
3634
3959
|
process.on("SIGINT", () => {
|
|
3635
3960
|
abortController.abort();
|
|
@@ -3645,8 +3970,18 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
|
|
|
3645
3970
|
repoConfig: options?.repoConfig,
|
|
3646
3971
|
roles: options?.roles,
|
|
3647
3972
|
synthesizeRepos: options?.synthesizeRepos,
|
|
3648
|
-
signal: abortController.signal
|
|
3973
|
+
signal: abortController.signal,
|
|
3974
|
+
cleanupTracker,
|
|
3975
|
+
verbose: options?.verbose
|
|
3649
3976
|
});
|
|
3977
|
+
if (cleanupTracker && cleanupTracker.size > 0) {
|
|
3978
|
+
const finalSwept = await cleanupTracker.sweep(cleanupWorktree);
|
|
3979
|
+
if (finalSwept > 0) {
|
|
3980
|
+
log(
|
|
3981
|
+
`${icons.info} Cleaned up ${finalSwept} codebase director${finalSwept === 1 ? "y" : "ies"} on shutdown`
|
|
3982
|
+
);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3650
3985
|
if (deps.usageTracker) {
|
|
3651
3986
|
log(deps.usageTracker.formatSummary(deps.usageLimits ?? usageLimits));
|
|
3652
3987
|
}
|
|
@@ -3718,13 +4053,13 @@ async function startAgentRouter() {
|
|
|
3718
4053
|
authToken: oauthToken,
|
|
3719
4054
|
onTokenRefresh: () => getValidToken(config.platformUrl),
|
|
3720
4055
|
usageLimits: config.usageLimits,
|
|
3721
|
-
versionOverride
|
|
4056
|
+
versionOverride,
|
|
4057
|
+
codebaseTtl: config.codebaseTtl
|
|
3722
4058
|
}
|
|
3723
4059
|
);
|
|
3724
4060
|
router.stop();
|
|
3725
4061
|
}
|
|
3726
|
-
function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versionOverride) {
|
|
3727
|
-
const agentId = crypto2.randomUUID();
|
|
4062
|
+
function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versionOverride, verbose, instancesOverride) {
|
|
3728
4063
|
let commandTemplate;
|
|
3729
4064
|
let agentConfig;
|
|
3730
4065
|
if (config.agents && config.agents.length > agentIndex) {
|
|
@@ -3744,58 +4079,76 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versi
|
|
|
3744
4079
|
);
|
|
3745
4080
|
return null;
|
|
3746
4081
|
}
|
|
4082
|
+
const instanceCount = instancesOverride ?? agentConfig?.instances ?? 1;
|
|
3747
4083
|
const codebaseDir = resolveCodebaseDir(agentConfig?.codebase_dir, config.codebaseDir);
|
|
3748
4084
|
const reviewDeps = {
|
|
3749
4085
|
commandTemplate,
|
|
3750
4086
|
maxDiffSizeKb: config.maxDiffSizeKb,
|
|
3751
4087
|
codebaseDir
|
|
3752
4088
|
};
|
|
3753
|
-
const isRouter = agentConfig?.router === true;
|
|
3754
|
-
let routerRelay;
|
|
3755
|
-
if (isRouter) {
|
|
3756
|
-
routerRelay = new RouterRelay();
|
|
3757
|
-
routerRelay.start();
|
|
3758
|
-
}
|
|
3759
|
-
const session = createSessionTracker();
|
|
3760
|
-
const usageTracker = new UsageTracker();
|
|
3761
4089
|
const model = agentConfig?.model ?? "unknown";
|
|
3762
4090
|
const tool = agentConfig?.tool ?? "unknown";
|
|
3763
4091
|
const thinking = agentConfig?.thinking;
|
|
3764
4092
|
const roles = agentConfig ? computeRoles(agentConfig) : void 0;
|
|
3765
|
-
const
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
routerRelay
|
|
3775
|
-
|
|
3776
|
-
repoConfig: agentConfig?.repos,
|
|
3777
|
-
roles,
|
|
3778
|
-
synthesizeRepos: agentConfig?.synthesize_repos,
|
|
3779
|
-
label,
|
|
3780
|
-
authToken: oauthToken,
|
|
3781
|
-
onTokenRefresh: () => getValidToken(config.platformUrl),
|
|
3782
|
-
usageLimits: config.usageLimits,
|
|
3783
|
-
versionOverride
|
|
4093
|
+
const session = createSessionTracker();
|
|
4094
|
+
const usageTracker = new UsageTracker();
|
|
4095
|
+
const promises = [];
|
|
4096
|
+
for (let inst = 0; inst < instanceCount; inst++) {
|
|
4097
|
+
const agentId = crypto2.randomUUID();
|
|
4098
|
+
const instanceLabel = instanceCount > 1 ? `${label}#${inst + 1}` : label;
|
|
4099
|
+
const isRouter = agentConfig?.router === true;
|
|
4100
|
+
let routerRelay;
|
|
4101
|
+
if (isRouter) {
|
|
4102
|
+
routerRelay = new RouterRelay();
|
|
4103
|
+
routerRelay.start();
|
|
3784
4104
|
}
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
4105
|
+
const agentPromise = startAgent(
|
|
4106
|
+
agentId,
|
|
4107
|
+
config.platformUrl,
|
|
4108
|
+
{ model, tool, thinking },
|
|
4109
|
+
reviewDeps,
|
|
4110
|
+
{ agentId, session, usageTracker, usageLimits: config.usageLimits },
|
|
4111
|
+
{
|
|
4112
|
+
pollIntervalMs,
|
|
4113
|
+
maxConsecutiveErrors: config.maxConsecutiveErrors,
|
|
4114
|
+
routerRelay,
|
|
4115
|
+
reviewOnly: agentConfig?.review_only,
|
|
4116
|
+
repoConfig: agentConfig?.repos,
|
|
4117
|
+
roles,
|
|
4118
|
+
synthesizeRepos: agentConfig?.synthesize_repos,
|
|
4119
|
+
label: instanceLabel,
|
|
4120
|
+
authToken: oauthToken,
|
|
4121
|
+
onTokenRefresh: () => getValidToken(config.platformUrl),
|
|
4122
|
+
usageLimits: config.usageLimits,
|
|
4123
|
+
versionOverride,
|
|
4124
|
+
codebaseTtl: config.codebaseTtl,
|
|
4125
|
+
verbose
|
|
4126
|
+
}
|
|
4127
|
+
).finally(() => {
|
|
4128
|
+
routerRelay?.stop();
|
|
4129
|
+
});
|
|
4130
|
+
promises.push(agentPromise);
|
|
4131
|
+
}
|
|
4132
|
+
return promises;
|
|
3789
4133
|
}
|
|
3790
4134
|
var agentCommand = new Command("agent").description("Manage review agents");
|
|
3791
4135
|
agentCommand.command("start").description("Start agents in polling mode").option("--poll-interval <seconds>", "Poll interval in seconds", "10").option("--agent <index>", "Agent index from config.toml (0-based)", "0").option("--all", "Start all configured agents concurrently").option(
|
|
3792
4136
|
"--version-override <value>",
|
|
3793
4137
|
"Cloudflare Workers version override (e.g. opencara-server=abc123)"
|
|
3794
|
-
).action(
|
|
4138
|
+
).option("-v, --verbose", "Log tool stdout/stderr after each review/summary for debugging").option("--instances <count>", "Number of concurrent instances per agent (overrides config)").action(
|
|
3795
4139
|
async (opts) => {
|
|
3796
4140
|
const config = loadConfig();
|
|
3797
4141
|
const pollIntervalMs = parseInt(opts.pollInterval, 10) * 1e3;
|
|
3798
4142
|
const versionOverride = opts.versionOverride || process.env.OPENCARA_VERSION_OVERRIDE || null;
|
|
4143
|
+
let instancesOverride;
|
|
4144
|
+
if (opts.instances !== void 0) {
|
|
4145
|
+
if (!/^[1-9]\d*$/.test(opts.instances)) {
|
|
4146
|
+
console.error("--instances must be a positive integer");
|
|
4147
|
+
process.exit(1);
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
instancesOverride = parseInt(opts.instances, 10);
|
|
4151
|
+
}
|
|
3799
4152
|
let oauthToken;
|
|
3800
4153
|
try {
|
|
3801
4154
|
oauthToken = await getValidToken(config.platformUrl);
|
|
@@ -3817,13 +4170,21 @@ agentCommand.command("start").description("Start agents in polling mode").option
|
|
|
3817
4170
|
process.exit(1);
|
|
3818
4171
|
return;
|
|
3819
4172
|
}
|
|
3820
|
-
console.log(`Starting ${config.agents.length} agent(s)...`);
|
|
4173
|
+
console.log(`Starting ${config.agents.length} agent config(s)...`);
|
|
3821
4174
|
const promises = [];
|
|
3822
4175
|
let startFailed = false;
|
|
3823
4176
|
for (let i = 0; i < config.agents.length; i++) {
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
4177
|
+
const agentPromises = startAgentByIndex(
|
|
4178
|
+
config,
|
|
4179
|
+
i,
|
|
4180
|
+
pollIntervalMs,
|
|
4181
|
+
oauthToken,
|
|
4182
|
+
versionOverride,
|
|
4183
|
+
opts.verbose,
|
|
4184
|
+
instancesOverride
|
|
4185
|
+
);
|
|
4186
|
+
if (agentPromises) {
|
|
4187
|
+
promises.push(...agentPromises);
|
|
3827
4188
|
} else {
|
|
3828
4189
|
startFailed = true;
|
|
3829
4190
|
}
|
|
@@ -3838,7 +4199,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
|
|
|
3838
4199
|
"One or more agents could not start (see warnings above). Continuing with the rest."
|
|
3839
4200
|
);
|
|
3840
4201
|
}
|
|
3841
|
-
console.log(`${promises.length} agent(s) running. Press Ctrl+C to stop all.
|
|
4202
|
+
console.log(`${promises.length} agent instance(s) running. Press Ctrl+C to stop all.
|
|
3842
4203
|
`);
|
|
3843
4204
|
const results = await Promise.allSettled(promises);
|
|
3844
4205
|
const failures = results.filter((r) => r.status === "rejected");
|
|
@@ -3858,18 +4219,27 @@ agentCommand.command("start").description("Start agents in polling mode").option
|
|
|
3858
4219
|
process.exit(1);
|
|
3859
4220
|
return;
|
|
3860
4221
|
}
|
|
3861
|
-
const
|
|
4222
|
+
const agentPromises = startAgentByIndex(
|
|
3862
4223
|
config,
|
|
3863
4224
|
agentIndex,
|
|
3864
4225
|
pollIntervalMs,
|
|
3865
4226
|
oauthToken,
|
|
3866
|
-
versionOverride
|
|
4227
|
+
versionOverride,
|
|
4228
|
+
opts.verbose,
|
|
4229
|
+
instancesOverride
|
|
3867
4230
|
);
|
|
3868
|
-
if (!
|
|
4231
|
+
if (!agentPromises) {
|
|
3869
4232
|
process.exit(1);
|
|
3870
4233
|
return;
|
|
3871
4234
|
}
|
|
3872
|
-
await
|
|
4235
|
+
const results = await Promise.allSettled(agentPromises);
|
|
4236
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
4237
|
+
if (failures.length > 0) {
|
|
4238
|
+
for (const f of failures) {
|
|
4239
|
+
console.error(`Agent instance failed: ${f.reason}`);
|
|
4240
|
+
}
|
|
4241
|
+
process.exit(1);
|
|
4242
|
+
}
|
|
3873
4243
|
}
|
|
3874
4244
|
}
|
|
3875
4245
|
);
|
|
@@ -4005,8 +4375,8 @@ var PER_PAGE = 100;
|
|
|
4005
4375
|
var OPEN_MARKER = "<!-- opencara-dedup-index:open -->";
|
|
4006
4376
|
var RECENT_MARKER = "<!-- opencara-dedup-index:recent -->";
|
|
4007
4377
|
var ARCHIVED_MARKER = "<!-- opencara-dedup-index:archived -->";
|
|
4008
|
-
async function fetchRepoFile(owner, repo,
|
|
4009
|
-
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${
|
|
4378
|
+
async function fetchRepoFile(owner, repo, path9, token, fetchFn = fetch) {
|
|
4379
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path9}`;
|
|
4010
4380
|
const res = await fetchFn(url, {
|
|
4011
4381
|
headers: {
|
|
4012
4382
|
Authorization: `Bearer ${token}`,
|
|
@@ -4014,7 +4384,7 @@ async function fetchRepoFile(owner, repo, path7, token, fetchFn = fetch) {
|
|
|
4014
4384
|
}
|
|
4015
4385
|
});
|
|
4016
4386
|
if (res.status === 404) return null;
|
|
4017
|
-
if (!res.ok) throw new Error(`GitHub API error: ${res.status} fetching ${
|
|
4387
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status} fetching ${path9}`);
|
|
4018
4388
|
return res.text();
|
|
4019
4389
|
}
|
|
4020
4390
|
async function fetchAllPRs(owner, repo, token, fetchFn = fetch, log) {
|
|
@@ -4572,7 +4942,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
|
|
|
4572
4942
|
});
|
|
4573
4943
|
|
|
4574
4944
|
// src/index.ts
|
|
4575
|
-
var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.
|
|
4945
|
+
var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.5");
|
|
4576
4946
|
program.addCommand(agentCommand);
|
|
4577
4947
|
program.addCommand(authCommand());
|
|
4578
4948
|
program.addCommand(dedupCommand());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencara",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
4
4
|
"description": "Distributed AI code review agent — poll, review, and submit PR reviews using your own AI tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,13 +30,10 @@
|
|
|
30
30
|
"node": ">=20"
|
|
31
31
|
},
|
|
32
32
|
"bin": {
|
|
33
|
-
"opencara": "dist/index.js"
|
|
34
|
-
"opencara-codex-agent": "bin/opencara-codex-agent",
|
|
35
|
-
"opencara-gemini-agent": "bin/opencara-gemini-agent"
|
|
33
|
+
"opencara": "dist/index.js"
|
|
36
34
|
},
|
|
37
35
|
"files": [
|
|
38
36
|
"dist",
|
|
39
|
-
"bin",
|
|
40
37
|
"README.md"
|
|
41
38
|
],
|
|
42
39
|
"scripts": {
|