opencara 0.18.4 → 0.18.6
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 +621 -198
- package/package.json +1 -1
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) {
|
|
@@ -17,15 +16,24 @@ function isDedupRole(role) {
|
|
|
17
16
|
function isTriageRole(role) {
|
|
18
17
|
return role === "pr_triage" || role === "issue_triage";
|
|
19
18
|
}
|
|
20
|
-
function isRepoAllowed(repoConfig, targetOwner, targetRepo, agentOwner) {
|
|
19
|
+
function isRepoAllowed(repoConfig, targetOwner, targetRepo, agentOwner, userOrgs) {
|
|
21
20
|
if (!repoConfig)
|
|
22
21
|
return true;
|
|
23
22
|
const fullRepo = `${targetOwner}/${targetRepo}`;
|
|
24
23
|
switch (repoConfig.mode) {
|
|
25
|
-
case "
|
|
24
|
+
case "public":
|
|
25
|
+
return true;
|
|
26
|
+
case "private": {
|
|
27
|
+
const normalizedTarget = targetOwner.toLowerCase();
|
|
28
|
+
const normalizedOwner = agentOwner?.toLowerCase();
|
|
29
|
+
const hasAccess = normalizedOwner === normalizedTarget || userOrgs != null && userOrgs.has(normalizedTarget);
|
|
30
|
+
if (!hasAccess)
|
|
31
|
+
return false;
|
|
32
|
+
if (repoConfig.list && repoConfig.list.length > 0) {
|
|
33
|
+
return repoConfig.list.includes(fullRepo);
|
|
34
|
+
}
|
|
26
35
|
return true;
|
|
27
|
-
|
|
28
|
-
return agentOwner === targetOwner;
|
|
36
|
+
}
|
|
29
37
|
case "whitelist":
|
|
30
38
|
return (repoConfig.list ?? []).includes(fullRepo);
|
|
31
39
|
case "blacklist":
|
|
@@ -159,6 +167,20 @@ function parseStringArray(value) {
|
|
|
159
167
|
return [];
|
|
160
168
|
return value.filter((v) => typeof v === "string");
|
|
161
169
|
}
|
|
170
|
+
var DEFAULT_MODEL_DIVERSITY_GRACE_MS = 3e4;
|
|
171
|
+
function parseDurationSeconds(value, defaultMs) {
|
|
172
|
+
if (typeof value === "number")
|
|
173
|
+
return value === 0 ? 0 : clamp(value, 0, 300) * 1e3;
|
|
174
|
+
if (typeof value !== "string")
|
|
175
|
+
return defaultMs;
|
|
176
|
+
if (value === "0" || value === "0s")
|
|
177
|
+
return 0;
|
|
178
|
+
const match = value.match(/^(\d+)s$/);
|
|
179
|
+
if (!match)
|
|
180
|
+
return defaultMs;
|
|
181
|
+
const seconds = parseInt(match[1], 10);
|
|
182
|
+
return clamp(seconds, 0, 300) * 1e3;
|
|
183
|
+
}
|
|
162
184
|
var DEFAULT_TRIGGER = {
|
|
163
185
|
on: ["opened"],
|
|
164
186
|
comment: "/opencara review",
|
|
@@ -169,13 +191,14 @@ var DEFAULT_FEATURE_CONFIG = {
|
|
|
169
191
|
agentCount: 1,
|
|
170
192
|
timeout: "10m",
|
|
171
193
|
preferredModels: [],
|
|
172
|
-
preferredTools: []
|
|
194
|
+
preferredTools: [],
|
|
195
|
+
modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
|
|
173
196
|
};
|
|
174
197
|
var DEFAULT_REVIEW_SECTION = {
|
|
175
198
|
...DEFAULT_FEATURE_CONFIG,
|
|
176
199
|
trigger: DEFAULT_TRIGGER,
|
|
177
200
|
reviewer: { whitelist: [], blacklist: [] },
|
|
178
|
-
summarizer: { whitelist: [], blacklist: [], preferred: [] }
|
|
201
|
+
summarizer: { whitelist: [], blacklist: [], preferred: [], preferredModels: [] }
|
|
179
202
|
};
|
|
180
203
|
function toGithubEntity(name) {
|
|
181
204
|
return { github: name };
|
|
@@ -184,27 +207,30 @@ function parseSummarizerSection(raw) {
|
|
|
184
207
|
const defaults = {
|
|
185
208
|
whitelist: [],
|
|
186
209
|
blacklist: [],
|
|
187
|
-
preferred: []
|
|
210
|
+
preferred: [],
|
|
211
|
+
preferredModels: []
|
|
188
212
|
};
|
|
189
213
|
if (typeof raw === "string") {
|
|
190
214
|
return { ...defaults, preferred: [toGithubEntity(raw)] };
|
|
191
215
|
}
|
|
192
216
|
if (!isObject(raw))
|
|
193
217
|
return defaults;
|
|
218
|
+
const preferredModels = parseStringArray(raw.preferred_models);
|
|
194
219
|
if (raw.only !== void 0) {
|
|
195
220
|
if (typeof raw.only === "string") {
|
|
196
|
-
return { ...defaults, whitelist: [toGithubEntity(raw.only)] };
|
|
221
|
+
return { ...defaults, whitelist: [toGithubEntity(raw.only)], preferredModels };
|
|
197
222
|
}
|
|
198
223
|
if (Array.isArray(raw.only)) {
|
|
199
224
|
const entries = raw.only.filter((v) => typeof v === "string").map((v) => toGithubEntity(v));
|
|
200
|
-
return { ...defaults, whitelist: entries };
|
|
225
|
+
return { ...defaults, whitelist: entries, preferredModels };
|
|
201
226
|
}
|
|
202
|
-
return defaults;
|
|
227
|
+
return { ...defaults, preferredModels };
|
|
203
228
|
}
|
|
204
229
|
return {
|
|
205
230
|
whitelist: parseEntityList(raw.whitelist),
|
|
206
231
|
blacklist: parseEntityList(raw.blacklist),
|
|
207
|
-
preferred: parseEntityList(raw.preferred)
|
|
232
|
+
preferred: parseEntityList(raw.preferred),
|
|
233
|
+
preferredModels
|
|
208
234
|
};
|
|
209
235
|
}
|
|
210
236
|
function parseAgentSlots(value) {
|
|
@@ -235,6 +261,7 @@ function parseFeatureFields(raw, defaults) {
|
|
|
235
261
|
timeout: parseTimeout(raw.timeout ?? defaults.timeout),
|
|
236
262
|
preferredModels: parseStringArray(raw.preferred_models ?? defaults.preferredModels),
|
|
237
263
|
preferredTools: parseStringArray(raw.preferred_tools ?? defaults.preferredTools),
|
|
264
|
+
modelDiversityGraceMs: parseDurationSeconds(raw.model_diversity_grace, defaults.modelDiversityGraceMs),
|
|
238
265
|
...agentSlots ? { agents: agentSlots } : {}
|
|
239
266
|
};
|
|
240
267
|
}
|
|
@@ -261,7 +288,8 @@ var DEFAULT_DEDUP_FEATURE = {
|
|
|
261
288
|
agentCount: 1,
|
|
262
289
|
timeout: "10m",
|
|
263
290
|
preferredModels: [],
|
|
264
|
-
preferredTools: []
|
|
291
|
+
preferredTools: [],
|
|
292
|
+
modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
|
|
265
293
|
};
|
|
266
294
|
function parseDedupTarget(raw) {
|
|
267
295
|
const base = parseFeatureFields(raw, DEFAULT_DEDUP_FEATURE);
|
|
@@ -291,7 +319,8 @@ var DEFAULT_TRIAGE_FEATURE = {
|
|
|
291
319
|
agentCount: 1,
|
|
292
320
|
timeout: "10m",
|
|
293
321
|
preferredModels: [],
|
|
294
|
-
preferredTools: []
|
|
322
|
+
preferredTools: [],
|
|
323
|
+
modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
|
|
295
324
|
};
|
|
296
325
|
function parseTriageSection(raw) {
|
|
297
326
|
const base = parseFeatureFields(raw, DEFAULT_TRIAGE_FEATURE);
|
|
@@ -360,6 +389,7 @@ function parseLegacyReviewConfig(raw) {
|
|
|
360
389
|
timeout: parseTimeout(raw.timeout),
|
|
361
390
|
preferredModels: parseStringArray(agentsRaw.preferred_models),
|
|
362
391
|
preferredTools: parseStringArray(agentsRaw.preferred_tools),
|
|
392
|
+
modelDiversityGraceMs: parseDurationSeconds(raw.model_diversity_grace ?? agentsRaw.model_diversity_grace, DEFAULT_MODEL_DIVERSITY_GRACE_MS),
|
|
363
393
|
trigger: {
|
|
364
394
|
on: Array.isArray(triggerRaw.on) ? triggerRaw.on.filter((v) => typeof v === "string") : DEFAULT_TRIGGER.on,
|
|
365
395
|
comment: typeof triggerRaw.comment === "string" ? triggerRaw.comment : DEFAULT_TRIGGER.comment,
|
|
@@ -387,8 +417,12 @@ function ensureConfigDir() {
|
|
|
387
417
|
}
|
|
388
418
|
var DEFAULT_MAX_DIFF_SIZE_KB = 100;
|
|
389
419
|
var DEFAULT_MAX_CONSECUTIVE_ERRORS = 10;
|
|
390
|
-
var VALID_REPO_MODES = ["
|
|
420
|
+
var VALID_REPO_MODES = ["public", "private", "whitelist", "blacklist"];
|
|
391
421
|
var REPO_PATTERN = /^[^/]+\/[^/]+$/;
|
|
422
|
+
var REPO_MODE_ALIASES = {
|
|
423
|
+
all: "public",
|
|
424
|
+
own: "private"
|
|
425
|
+
};
|
|
392
426
|
var RepoConfigError = class extends Error {
|
|
393
427
|
constructor(message) {
|
|
394
428
|
super(message);
|
|
@@ -412,10 +446,17 @@ function parseRepoConfig(obj, index, field = "repos") {
|
|
|
412
446
|
throw new RepoConfigError(`agents[${index}].${field} must be an object`);
|
|
413
447
|
}
|
|
414
448
|
const reposObj = raw;
|
|
415
|
-
|
|
449
|
+
let mode = reposObj.mode;
|
|
416
450
|
if (mode === void 0) {
|
|
417
451
|
throw new RepoConfigError(`agents[${index}].${field}.mode is required`);
|
|
418
452
|
}
|
|
453
|
+
if (typeof mode === "string" && Object.hasOwn(REPO_MODE_ALIASES, mode)) {
|
|
454
|
+
const resolved = REPO_MODE_ALIASES[mode];
|
|
455
|
+
console.warn(
|
|
456
|
+
`\u26A0 Config warning: agents[${index}].${field}.mode "${mode}" is deprecated, use "${resolved}" instead`
|
|
457
|
+
);
|
|
458
|
+
mode = resolved;
|
|
459
|
+
}
|
|
419
460
|
if (typeof mode !== "string" || !VALID_REPO_MODES.includes(mode)) {
|
|
420
461
|
throw new RepoConfigError(
|
|
421
462
|
`agents[${index}].${field}.mode must be one of: ${VALID_REPO_MODES.join(", ")}`
|
|
@@ -509,6 +550,15 @@ function parseAgents(data) {
|
|
|
509
550
|
);
|
|
510
551
|
}
|
|
511
552
|
if (typeof obj.codebase_dir === "string") agent.codebase_dir = obj.codebase_dir;
|
|
553
|
+
if (typeof obj.instances === "number") {
|
|
554
|
+
if (!Number.isInteger(obj.instances) || obj.instances < 1) {
|
|
555
|
+
console.warn(
|
|
556
|
+
`\u26A0 Config warning: agents[${i}].instances must be a positive integer, got ${obj.instances}. Value ignored.`
|
|
557
|
+
);
|
|
558
|
+
} else {
|
|
559
|
+
agent.instances = obj.instances;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
512
562
|
const repoConfig = parseRepoConfig(obj, i);
|
|
513
563
|
if (repoConfig) agent.repos = repoConfig;
|
|
514
564
|
const synthesizeRepoConfig = parseRepoConfig(obj, i, "synthesize_repos");
|
|
@@ -568,6 +618,7 @@ function loadConfig() {
|
|
|
568
618
|
maxDiffSizeKb: DEFAULT_MAX_DIFF_SIZE_KB,
|
|
569
619
|
maxConsecutiveErrors: DEFAULT_MAX_CONSECUTIVE_ERRORS,
|
|
570
620
|
codebaseDir: null,
|
|
621
|
+
codebaseTtl: null,
|
|
571
622
|
agentCommand: null,
|
|
572
623
|
agents: null,
|
|
573
624
|
usageLimits: {
|
|
@@ -611,6 +662,7 @@ function loadConfig() {
|
|
|
611
662
|
maxDiffSizeKb: overrides.maxDiffSizeKb ?? (typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB),
|
|
612
663
|
maxConsecutiveErrors: overrides.maxConsecutiveErrors ?? (typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS),
|
|
613
664
|
codebaseDir: typeof data.codebase_dir === "string" ? data.codebase_dir : null,
|
|
665
|
+
codebaseTtl: typeof data.codebase_ttl === "string" ? data.codebase_ttl : null,
|
|
614
666
|
agentCommand: typeof data.agent_command === "string" ? data.agent_command : null,
|
|
615
667
|
agents: parseAgents(data),
|
|
616
668
|
usageLimits: {
|
|
@@ -629,10 +681,10 @@ function resolveCodebaseDir(agentDir, globalDir) {
|
|
|
629
681
|
return path.resolve(raw);
|
|
630
682
|
}
|
|
631
683
|
|
|
632
|
-
// src/
|
|
633
|
-
import { execFileSync } from "child_process";
|
|
634
|
-
import * as
|
|
635
|
-
import * as
|
|
684
|
+
// src/repo-cache.ts
|
|
685
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
686
|
+
import * as fs3 from "fs";
|
|
687
|
+
import * as path3 from "path";
|
|
636
688
|
|
|
637
689
|
// src/sanitize.ts
|
|
638
690
|
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 +695,10 @@ function sanitizeTokens(input) {
|
|
|
643
695
|
}
|
|
644
696
|
|
|
645
697
|
// src/codebase.ts
|
|
698
|
+
import { execFileSync } from "child_process";
|
|
699
|
+
import * as fs2 from "fs";
|
|
700
|
+
import * as path2 from "path";
|
|
646
701
|
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
702
|
function validatePathSegment(segment, name) {
|
|
688
703
|
if (!VALID_NAME_PATTERN.test(segment) || segment === "." || segment === "..") {
|
|
689
704
|
throw new Error(`Invalid ${name}: '${segment}' contains disallowed characters`);
|
|
@@ -704,24 +719,111 @@ function isGhAvailable() {
|
|
|
704
719
|
return false;
|
|
705
720
|
}
|
|
706
721
|
}
|
|
707
|
-
|
|
722
|
+
|
|
723
|
+
// src/repo-cache.ts
|
|
724
|
+
var GH_CREDENTIAL_HELPER = "!gh auth git-credential";
|
|
725
|
+
var GIT_TIMEOUT_MS = 12e4;
|
|
726
|
+
var repoLocks = /* @__PURE__ */ new Map();
|
|
727
|
+
async function withRepoLock(repoKey, fn) {
|
|
728
|
+
const existing = repoLocks.get(repoKey);
|
|
729
|
+
let release;
|
|
730
|
+
const gate = new Promise((resolve2) => {
|
|
731
|
+
release = resolve2;
|
|
732
|
+
});
|
|
733
|
+
repoLocks.set(repoKey, gate);
|
|
708
734
|
try {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
735
|
+
if (existing) await existing;
|
|
736
|
+
return await fn();
|
|
737
|
+
} finally {
|
|
738
|
+
release();
|
|
739
|
+
if (repoLocks.get(repoKey) === gate) {
|
|
740
|
+
repoLocks.delete(repoKey);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function ensureBareClone(owner, repo, baseDir, ghAvailable) {
|
|
745
|
+
validatePathSegment(owner, "owner");
|
|
746
|
+
validatePathSegment(repo, "repo");
|
|
747
|
+
const bareRepoPath = path3.join(baseDir, owner, `${repo}.git`);
|
|
748
|
+
if (fs3.existsSync(path3.join(bareRepoPath, "HEAD"))) {
|
|
749
|
+
return { bareRepoPath, cloned: false };
|
|
750
|
+
}
|
|
751
|
+
fs3.mkdirSync(path3.join(baseDir, owner), { recursive: true });
|
|
752
|
+
if (ghAvailable) {
|
|
753
|
+
gitExec("gh", [
|
|
754
|
+
"repo",
|
|
755
|
+
"clone",
|
|
756
|
+
`${owner}/${repo}`,
|
|
757
|
+
bareRepoPath,
|
|
758
|
+
"--",
|
|
759
|
+
"--bare",
|
|
760
|
+
"--filter=blob:none"
|
|
761
|
+
]);
|
|
762
|
+
} else {
|
|
763
|
+
const cloneUrl = buildCloneUrl(owner, repo);
|
|
764
|
+
gitExec("git", ["clone", "--bare", "--filter=blob:none", cloneUrl, bareRepoPath]);
|
|
717
765
|
}
|
|
766
|
+
return { bareRepoPath, cloned: true };
|
|
767
|
+
}
|
|
768
|
+
function fetchPRRef(bareRepoPath, prNumber, ghAvailable) {
|
|
769
|
+
const credArgs = ghAvailable ? ["-c", `credential.helper=${GH_CREDENTIAL_HELPER}`] : [];
|
|
770
|
+
gitExec(
|
|
771
|
+
"git",
|
|
772
|
+
[...credArgs, "fetch", "--force", "origin", `pull/${prNumber}/head`],
|
|
773
|
+
bareRepoPath
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
function addWorktree(bareRepoPath, taskId) {
|
|
777
|
+
validatePathSegment(taskId, "taskId");
|
|
778
|
+
const repoName = path3.basename(bareRepoPath, ".git");
|
|
779
|
+
const worktreeBase = path3.join(path3.dirname(bareRepoPath), `${repoName}-worktrees`);
|
|
780
|
+
const worktreePath = path3.join(worktreeBase, taskId);
|
|
781
|
+
fs3.mkdirSync(worktreeBase, { recursive: true });
|
|
782
|
+
gitExec("git", ["worktree", "add", "--detach", worktreePath, "FETCH_HEAD"], bareRepoPath);
|
|
783
|
+
return worktreePath;
|
|
784
|
+
}
|
|
785
|
+
function removeWorktree(bareRepoPath, worktreePath) {
|
|
786
|
+
try {
|
|
787
|
+
gitExec("git", ["worktree", "remove", "--force", worktreePath], bareRepoPath);
|
|
788
|
+
} catch {
|
|
789
|
+
try {
|
|
790
|
+
fs3.rmSync(worktreePath, { recursive: true, force: true });
|
|
791
|
+
gitExec("git", ["worktree", "prune"], bareRepoPath);
|
|
792
|
+
} catch {
|
|
793
|
+
console.warn(`[repo-cache] Failed to clean up worktree: ${worktreePath}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function repoKeyFromBarePath(bareRepoPath) {
|
|
798
|
+
const repoName = path3.basename(bareRepoPath, ".git");
|
|
799
|
+
const owner = path3.basename(path3.dirname(bareRepoPath));
|
|
800
|
+
return `${owner}/${repoName}`;
|
|
801
|
+
}
|
|
802
|
+
async function checkoutWorktree(owner, repo, prNumber, baseDir, taskId) {
|
|
803
|
+
validatePathSegment(owner, "owner");
|
|
804
|
+
validatePathSegment(repo, "repo");
|
|
805
|
+
validatePathSegment(taskId, "taskId");
|
|
806
|
+
const repoKey = `${owner}/${repo}`;
|
|
807
|
+
const ghAvailable = isGhAvailable();
|
|
808
|
+
return withRepoLock(repoKey, () => {
|
|
809
|
+
const { bareRepoPath, cloned } = ensureBareClone(owner, repo, baseDir, ghAvailable);
|
|
810
|
+
fetchPRRef(bareRepoPath, prNumber, ghAvailable);
|
|
811
|
+
const worktreePath = addWorktree(bareRepoPath, taskId);
|
|
812
|
+
return { worktreePath, bareRepoPath, cloned };
|
|
813
|
+
});
|
|
718
814
|
}
|
|
719
|
-
function
|
|
815
|
+
async function cleanupWorktree(bareRepoPath, worktreePath) {
|
|
816
|
+
const repoKey = repoKeyFromBarePath(bareRepoPath);
|
|
817
|
+
await withRepoLock(repoKey, () => {
|
|
818
|
+
removeWorktree(bareRepoPath, worktreePath);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
function gitExec(command, args, cwd) {
|
|
720
822
|
try {
|
|
721
|
-
return
|
|
823
|
+
return execFileSync2(command, args, {
|
|
722
824
|
cwd,
|
|
723
825
|
encoding: "utf-8",
|
|
724
|
-
timeout:
|
|
826
|
+
timeout: GIT_TIMEOUT_MS,
|
|
725
827
|
stdio: ["ignore", "pipe", "pipe"]
|
|
726
828
|
});
|
|
727
829
|
} catch (err) {
|
|
@@ -730,20 +832,170 @@ function git(args, cwd) {
|
|
|
730
832
|
}
|
|
731
833
|
}
|
|
732
834
|
|
|
835
|
+
// src/codebase-cleanup.ts
|
|
836
|
+
import * as fs4 from "fs";
|
|
837
|
+
import * as path4 from "path";
|
|
838
|
+
var DEFAULT_CODEBASE_TTL_MS = 30 * 60 * 1e3;
|
|
839
|
+
function parseTtl(value) {
|
|
840
|
+
const trimmed = value.trim();
|
|
841
|
+
if (trimmed === "0") return 0;
|
|
842
|
+
const match = trimmed.match(/^(\d+)\s*(ms|s|m|h|d)$/);
|
|
843
|
+
if (match) {
|
|
844
|
+
const num = parseInt(match[1], 10);
|
|
845
|
+
switch (match[2]) {
|
|
846
|
+
case "ms":
|
|
847
|
+
return num;
|
|
848
|
+
case "s":
|
|
849
|
+
return num * 1e3;
|
|
850
|
+
case "m":
|
|
851
|
+
return num * 60 * 1e3;
|
|
852
|
+
case "h":
|
|
853
|
+
return num * 60 * 60 * 1e3;
|
|
854
|
+
case "d":
|
|
855
|
+
return num * 24 * 60 * 60 * 1e3;
|
|
856
|
+
default:
|
|
857
|
+
throw new Error(`Unreachable: unhandled unit "${match[2]}"`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (/^\d+$/.test(trimmed)) {
|
|
861
|
+
return parseInt(trimmed, 10) * 1e3;
|
|
862
|
+
}
|
|
863
|
+
throw new Error(`Invalid codebase_ttl: "${value}". Use "0", "30m", "2h", "24h", "1d", etc.`);
|
|
864
|
+
}
|
|
865
|
+
var CodebaseCleanupTracker = class {
|
|
866
|
+
pending = [];
|
|
867
|
+
ttlMs;
|
|
868
|
+
constructor(ttlMs) {
|
|
869
|
+
this.ttlMs = ttlMs;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Record a completed task's worktree for deferred cleanup.
|
|
873
|
+
*/
|
|
874
|
+
track(bareRepoPath, worktreePath) {
|
|
875
|
+
this.pending.push({
|
|
876
|
+
bareRepoPath,
|
|
877
|
+
worktreePath,
|
|
878
|
+
completedAt: Date.now()
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Check for and remove any worktrees that have exceeded the TTL.
|
|
883
|
+
* Returns the number of directories cleaned up.
|
|
884
|
+
*
|
|
885
|
+
* The removeFn callback performs the actual git worktree removal.
|
|
886
|
+
*/
|
|
887
|
+
async sweep(removeFn) {
|
|
888
|
+
const now = Date.now();
|
|
889
|
+
const expired = [];
|
|
890
|
+
const remaining = [];
|
|
891
|
+
for (const entry of this.pending) {
|
|
892
|
+
if (now - entry.completedAt >= this.ttlMs) {
|
|
893
|
+
expired.push(entry);
|
|
894
|
+
} else {
|
|
895
|
+
remaining.push(entry);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
this.pending = remaining;
|
|
899
|
+
let cleaned = 0;
|
|
900
|
+
for (const entry of expired) {
|
|
901
|
+
try {
|
|
902
|
+
await removeFn(entry.bareRepoPath, entry.worktreePath);
|
|
903
|
+
cleaned++;
|
|
904
|
+
} catch {
|
|
905
|
+
this.pending.push(entry);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return cleaned;
|
|
909
|
+
}
|
|
910
|
+
/** Number of entries pending cleanup. */
|
|
911
|
+
get size() {
|
|
912
|
+
return this.pending.length;
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
function scanAndCleanStaleWorktrees(baseDir, ttlMs) {
|
|
916
|
+
if (!fs4.existsSync(baseDir)) return 0;
|
|
917
|
+
const now = Date.now();
|
|
918
|
+
let cleaned = 0;
|
|
919
|
+
let ownerDirs;
|
|
920
|
+
try {
|
|
921
|
+
ownerDirs = fs4.readdirSync(baseDir);
|
|
922
|
+
} catch {
|
|
923
|
+
return 0;
|
|
924
|
+
}
|
|
925
|
+
for (const ownerName of ownerDirs) {
|
|
926
|
+
const ownerPath = path4.join(baseDir, ownerName);
|
|
927
|
+
let stat;
|
|
928
|
+
try {
|
|
929
|
+
stat = fs4.statSync(ownerPath);
|
|
930
|
+
} catch {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (!stat.isDirectory()) continue;
|
|
934
|
+
let entries;
|
|
935
|
+
try {
|
|
936
|
+
entries = fs4.readdirSync(ownerPath);
|
|
937
|
+
} catch {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
for (const entry of entries) {
|
|
941
|
+
if (!entry.endsWith("-worktrees")) continue;
|
|
942
|
+
const worktreeBasePath = path4.join(ownerPath, entry);
|
|
943
|
+
let worktreeStat;
|
|
944
|
+
try {
|
|
945
|
+
worktreeStat = fs4.statSync(worktreeBasePath);
|
|
946
|
+
} catch {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (!worktreeStat.isDirectory()) continue;
|
|
950
|
+
let taskDirs;
|
|
951
|
+
try {
|
|
952
|
+
taskDirs = fs4.readdirSync(worktreeBasePath);
|
|
953
|
+
} catch {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
for (const taskId of taskDirs) {
|
|
957
|
+
const taskPath = path4.join(worktreeBasePath, taskId);
|
|
958
|
+
let taskStat;
|
|
959
|
+
try {
|
|
960
|
+
taskStat = fs4.statSync(taskPath);
|
|
961
|
+
} catch {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (!taskStat.isDirectory()) continue;
|
|
965
|
+
const age = now - taskStat.mtimeMs;
|
|
966
|
+
if (age >= ttlMs) {
|
|
967
|
+
try {
|
|
968
|
+
fs4.rmSync(taskPath, { recursive: true, force: true });
|
|
969
|
+
const repoName = entry.replace(/-worktrees$/, "");
|
|
970
|
+
const metadataPath = path4.join(ownerPath, `${repoName}.git`, "worktrees", taskId);
|
|
971
|
+
try {
|
|
972
|
+
fs4.rmSync(metadataPath, { recursive: true, force: true });
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
cleaned++;
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return cleaned;
|
|
983
|
+
}
|
|
984
|
+
|
|
733
985
|
// src/auth.ts
|
|
734
|
-
import * as
|
|
735
|
-
import * as
|
|
986
|
+
import * as fs5 from "fs";
|
|
987
|
+
import * as path5 from "path";
|
|
736
988
|
import * as os2 from "os";
|
|
737
989
|
import * as crypto from "crypto";
|
|
738
|
-
var AUTH_DIR =
|
|
990
|
+
var AUTH_DIR = path5.join(os2.homedir(), ".opencara");
|
|
739
991
|
function getAuthFilePath() {
|
|
740
992
|
const envPath = process.env.OPENCARA_AUTH_FILE?.trim();
|
|
741
|
-
return envPath ||
|
|
993
|
+
return envPath || path5.join(AUTH_DIR, "auth.json");
|
|
742
994
|
}
|
|
743
995
|
function loadAuth() {
|
|
744
996
|
const filePath = getAuthFilePath();
|
|
745
997
|
try {
|
|
746
|
-
const raw =
|
|
998
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
747
999
|
const data = JSON.parse(raw);
|
|
748
1000
|
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
1001
|
(data.refresh_token === void 0 || typeof data.refresh_token === "string")) {
|
|
@@ -756,15 +1008,15 @@ function loadAuth() {
|
|
|
756
1008
|
}
|
|
757
1009
|
function saveAuth(auth) {
|
|
758
1010
|
const filePath = getAuthFilePath();
|
|
759
|
-
const dir =
|
|
760
|
-
|
|
761
|
-
const tmpPath =
|
|
1011
|
+
const dir = path5.dirname(filePath);
|
|
1012
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1013
|
+
const tmpPath = path5.join(dir, `.auth-${crypto.randomBytes(8).toString("hex")}.tmp`);
|
|
762
1014
|
try {
|
|
763
|
-
|
|
764
|
-
|
|
1015
|
+
fs5.writeFileSync(tmpPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1016
|
+
fs5.renameSync(tmpPath, filePath);
|
|
765
1017
|
} catch (err) {
|
|
766
1018
|
try {
|
|
767
|
-
|
|
1019
|
+
fs5.unlinkSync(tmpPath);
|
|
768
1020
|
} catch {
|
|
769
1021
|
}
|
|
770
1022
|
throw err;
|
|
@@ -773,7 +1025,7 @@ function saveAuth(auth) {
|
|
|
773
1025
|
function deleteAuth() {
|
|
774
1026
|
const filePath = getAuthFilePath();
|
|
775
1027
|
try {
|
|
776
|
-
|
|
1028
|
+
fs5.unlinkSync(filePath);
|
|
777
1029
|
} catch (err) {
|
|
778
1030
|
if (err.code !== "ENOENT") {
|
|
779
1031
|
throw err;
|
|
@@ -946,6 +1198,30 @@ async function resolveUser(token, fetchFn = fetch) {
|
|
|
946
1198
|
}
|
|
947
1199
|
return { login: data.login, id: data.id };
|
|
948
1200
|
}
|
|
1201
|
+
async function fetchUserOrgs(token, fetchFn = fetch) {
|
|
1202
|
+
try {
|
|
1203
|
+
const res = await fetchFn("https://api.github.com/user/orgs?per_page=100", {
|
|
1204
|
+
headers: {
|
|
1205
|
+
Authorization: `Bearer ${token}`,
|
|
1206
|
+
Accept: "application/vnd.github+json",
|
|
1207
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
if (!res.ok) {
|
|
1211
|
+
return /* @__PURE__ */ new Set();
|
|
1212
|
+
}
|
|
1213
|
+
const data = await res.json();
|
|
1214
|
+
const orgs = /* @__PURE__ */ new Set();
|
|
1215
|
+
for (const org of data) {
|
|
1216
|
+
if (typeof org.login === "string") {
|
|
1217
|
+
orgs.add(org.login.toLowerCase());
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return orgs;
|
|
1221
|
+
} catch {
|
|
1222
|
+
return /* @__PURE__ */ new Set();
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
949
1225
|
|
|
950
1226
|
// src/http.ts
|
|
951
1227
|
var HttpError = class extends Error {
|
|
@@ -1043,27 +1319,27 @@ var ApiClient = class {
|
|
|
1043
1319
|
clearTimeout(timer);
|
|
1044
1320
|
}
|
|
1045
1321
|
}
|
|
1046
|
-
async get(
|
|
1047
|
-
this.log(`GET ${
|
|
1048
|
-
const res = await this.timedFetch(`${this.baseUrl}${
|
|
1322
|
+
async get(path9) {
|
|
1323
|
+
this.log(`GET ${path9}`);
|
|
1324
|
+
const res = await this.timedFetch(`${this.baseUrl}${path9}`, {
|
|
1049
1325
|
method: "GET",
|
|
1050
1326
|
headers: this.headers()
|
|
1051
1327
|
});
|
|
1052
|
-
return this.handleResponse(res,
|
|
1328
|
+
return this.handleResponse(res, path9, "GET");
|
|
1053
1329
|
}
|
|
1054
|
-
async post(
|
|
1055
|
-
this.log(`POST ${
|
|
1056
|
-
const res = await this.timedFetch(`${this.baseUrl}${
|
|
1330
|
+
async post(path9, body) {
|
|
1331
|
+
this.log(`POST ${path9}`);
|
|
1332
|
+
const res = await this.timedFetch(`${this.baseUrl}${path9}`, {
|
|
1057
1333
|
method: "POST",
|
|
1058
1334
|
headers: this.headers(),
|
|
1059
1335
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1060
1336
|
});
|
|
1061
|
-
return this.handleResponse(res,
|
|
1337
|
+
return this.handleResponse(res, path9, "POST", body);
|
|
1062
1338
|
}
|
|
1063
|
-
async handleResponse(res,
|
|
1339
|
+
async handleResponse(res, path9, method, body) {
|
|
1064
1340
|
if (!res.ok) {
|
|
1065
1341
|
const { message, errorCode, minimumVersion } = await this.parseErrorBody(res);
|
|
1066
|
-
this.log(`${res.status} ${message} (${
|
|
1342
|
+
this.log(`${res.status} ${message} (${path9})`);
|
|
1067
1343
|
if (res.status === 426) {
|
|
1068
1344
|
throw new UpgradeRequiredError(this.cliVersion ?? "unknown", minimumVersion);
|
|
1069
1345
|
}
|
|
@@ -1072,12 +1348,12 @@ var ApiClient = class {
|
|
|
1072
1348
|
try {
|
|
1073
1349
|
this.authToken = await this.onTokenRefresh();
|
|
1074
1350
|
this.log("Token refreshed, retrying request");
|
|
1075
|
-
const retryRes = await this.timedFetch(`${this.baseUrl}${
|
|
1351
|
+
const retryRes = await this.timedFetch(`${this.baseUrl}${path9}`, {
|
|
1076
1352
|
method,
|
|
1077
1353
|
headers: this.headers(),
|
|
1078
1354
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1079
1355
|
});
|
|
1080
|
-
return this.handleRetryResponse(retryRes,
|
|
1356
|
+
return this.handleRetryResponse(retryRes, path9);
|
|
1081
1357
|
} catch (refreshErr) {
|
|
1082
1358
|
this.log(`Token refresh failed: ${refreshErr.message}`);
|
|
1083
1359
|
throw new HttpError(res.status, message, errorCode);
|
|
@@ -1085,20 +1361,20 @@ var ApiClient = class {
|
|
|
1085
1361
|
}
|
|
1086
1362
|
throw new HttpError(res.status, message, errorCode);
|
|
1087
1363
|
}
|
|
1088
|
-
this.log(`${res.status} OK (${
|
|
1364
|
+
this.log(`${res.status} OK (${path9})`);
|
|
1089
1365
|
return await res.json();
|
|
1090
1366
|
}
|
|
1091
1367
|
/** Handle response for a retry after token refresh — no second refresh attempt. */
|
|
1092
|
-
async handleRetryResponse(res,
|
|
1368
|
+
async handleRetryResponse(res, path9) {
|
|
1093
1369
|
if (!res.ok) {
|
|
1094
1370
|
const { message, errorCode, minimumVersion } = await this.parseErrorBody(res);
|
|
1095
|
-
this.log(`${res.status} ${message} (${
|
|
1371
|
+
this.log(`${res.status} ${message} (${path9}) [retry]`);
|
|
1096
1372
|
if (res.status === 426) {
|
|
1097
1373
|
throw new UpgradeRequiredError(this.cliVersion ?? "unknown", minimumVersion);
|
|
1098
1374
|
}
|
|
1099
1375
|
throw new HttpError(res.status, message, errorCode);
|
|
1100
1376
|
}
|
|
1101
|
-
this.log(`${res.status} OK (${
|
|
1377
|
+
this.log(`${res.status} OK (${path9}) [retry]`);
|
|
1102
1378
|
return await res.json();
|
|
1103
1379
|
}
|
|
1104
1380
|
};
|
|
@@ -1153,9 +1429,9 @@ function sleep(ms, signal) {
|
|
|
1153
1429
|
}
|
|
1154
1430
|
|
|
1155
1431
|
// src/tool-executor.ts
|
|
1156
|
-
import { spawn, execFileSync as
|
|
1157
|
-
import * as
|
|
1158
|
-
import * as
|
|
1432
|
+
import { spawn, execFileSync as execFileSync3 } from "child_process";
|
|
1433
|
+
import * as fs6 from "fs";
|
|
1434
|
+
import * as path6 from "path";
|
|
1159
1435
|
var ToolTimeoutError = class extends Error {
|
|
1160
1436
|
constructor(message) {
|
|
1161
1437
|
super(message);
|
|
@@ -1167,9 +1443,9 @@ var MIN_PARTIAL_RESULT_LENGTH = 50;
|
|
|
1167
1443
|
var MAX_STDERR_LENGTH = 1e3;
|
|
1168
1444
|
function validateCommandBinary(commandTemplate) {
|
|
1169
1445
|
const { command } = parseCommandTemplate(commandTemplate);
|
|
1170
|
-
if (
|
|
1446
|
+
if (path6.isAbsolute(command)) {
|
|
1171
1447
|
try {
|
|
1172
|
-
|
|
1448
|
+
fs6.accessSync(command, fs6.constants.X_OK);
|
|
1173
1449
|
return true;
|
|
1174
1450
|
} catch {
|
|
1175
1451
|
return false;
|
|
@@ -1178,9 +1454,9 @@ function validateCommandBinary(commandTemplate) {
|
|
|
1178
1454
|
try {
|
|
1179
1455
|
const isWindows = process.platform === "win32";
|
|
1180
1456
|
if (isWindows) {
|
|
1181
|
-
|
|
1457
|
+
execFileSync3("where", [command], { stdio: "pipe" });
|
|
1182
1458
|
} else {
|
|
1183
|
-
|
|
1459
|
+
execFileSync3("sh", ["-c", 'command -v -- "$1"', "_", command], { stdio: "pipe" });
|
|
1184
1460
|
}
|
|
1185
1461
|
return true;
|
|
1186
1462
|
} catch {
|
|
@@ -1537,7 +1813,10 @@ ${userMessage}`;
|
|
|
1537
1813
|
verdict,
|
|
1538
1814
|
tokensUsed: result.tokensUsed + inputTokens,
|
|
1539
1815
|
tokensEstimated: !result.tokensParsed,
|
|
1540
|
-
tokenDetail
|
|
1816
|
+
tokenDetail,
|
|
1817
|
+
toolStdout: result.stdout,
|
|
1818
|
+
toolStderr: result.stderr,
|
|
1819
|
+
promptLength: fullPrompt.length
|
|
1541
1820
|
};
|
|
1542
1821
|
} finally {
|
|
1543
1822
|
clearTimeout(abortTimer);
|
|
@@ -1716,7 +1995,10 @@ ${userMessage}`;
|
|
|
1716
1995
|
tokensUsed: result.tokensUsed + inputTokens,
|
|
1717
1996
|
tokensEstimated: !result.tokensParsed,
|
|
1718
1997
|
tokenDetail,
|
|
1719
|
-
flaggedReviews
|
|
1998
|
+
flaggedReviews,
|
|
1999
|
+
toolStdout: result.stdout,
|
|
2000
|
+
toolStderr: result.stderr,
|
|
2001
|
+
promptLength: fullPrompt.length
|
|
1720
2002
|
};
|
|
1721
2003
|
} finally {
|
|
1722
2004
|
clearTimeout(abortTimer);
|
|
@@ -1914,9 +2196,9 @@ function formatPostReviewStats(session) {
|
|
|
1914
2196
|
}
|
|
1915
2197
|
|
|
1916
2198
|
// src/usage-tracker.ts
|
|
1917
|
-
import * as
|
|
1918
|
-
import * as
|
|
1919
|
-
var USAGE_FILE =
|
|
2199
|
+
import * as fs7 from "fs";
|
|
2200
|
+
import * as path7 from "path";
|
|
2201
|
+
var USAGE_FILE = path7.join(CONFIG_DIR, "usage.json");
|
|
1920
2202
|
var MAX_HISTORY_DAYS = 30;
|
|
1921
2203
|
var WARNING_THRESHOLD = 0.8;
|
|
1922
2204
|
function todayKey() {
|
|
@@ -1939,8 +2221,8 @@ var UsageTracker = class {
|
|
|
1939
2221
|
}
|
|
1940
2222
|
load() {
|
|
1941
2223
|
try {
|
|
1942
|
-
if (
|
|
1943
|
-
const raw =
|
|
2224
|
+
if (fs7.existsSync(this.filePath)) {
|
|
2225
|
+
const raw = fs7.readFileSync(this.filePath, "utf-8");
|
|
1944
2226
|
const parsed = JSON.parse(raw);
|
|
1945
2227
|
if (parsed && Array.isArray(parsed.days)) {
|
|
1946
2228
|
return parsed;
|
|
@@ -1952,7 +2234,7 @@ var UsageTracker = class {
|
|
|
1952
2234
|
}
|
|
1953
2235
|
save() {
|
|
1954
2236
|
ensureConfigDir();
|
|
1955
|
-
|
|
2237
|
+
fs7.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), {
|
|
1956
2238
|
encoding: "utf-8",
|
|
1957
2239
|
mode: 384
|
|
1958
2240
|
});
|
|
@@ -2156,6 +2438,36 @@ function createLogger(label) {
|
|
|
2156
2438
|
logWarn: (msg) => console.warn(`${pc.dim(`[${timestamp()}]`)}${labelStr} ${pc.yellow(sanitizeTokens(msg))}`)
|
|
2157
2439
|
};
|
|
2158
2440
|
}
|
|
2441
|
+
var VERBOSE_TRUNCATE_LIMIT = 2e3;
|
|
2442
|
+
var CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
2443
|
+
function logVerboseToolOutput(logger, label, stdout, stderr, promptLength, limit = VERBOSE_TRUNCATE_LIMIT) {
|
|
2444
|
+
const estimatedTokens = Math.ceil(promptLength / CHARS_PER_TOKEN_ESTIMATE);
|
|
2445
|
+
logger.log(
|
|
2446
|
+
`${icons.info} [verbose] ${label} \u2014 prompt: ${promptLength} chars (~${estimatedTokens} tokens)`
|
|
2447
|
+
);
|
|
2448
|
+
if (stdout) {
|
|
2449
|
+
const truncated = stdout.length > limit ? stdout.slice(0, limit) + `
|
|
2450
|
+
... (truncated at ${limit} chars)` : stdout;
|
|
2451
|
+
logger.log(
|
|
2452
|
+
`${icons.info} [verbose] ${label} stdout (${stdout.length} chars):
|
|
2453
|
+
---
|
|
2454
|
+
${truncated}
|
|
2455
|
+
---`
|
|
2456
|
+
);
|
|
2457
|
+
} else {
|
|
2458
|
+
logger.log(`${icons.info} [verbose] ${label} stdout: (empty)`);
|
|
2459
|
+
}
|
|
2460
|
+
if (stderr) {
|
|
2461
|
+
const truncated = stderr.length > limit ? stderr.slice(0, limit) + `
|
|
2462
|
+
... (truncated at ${limit} chars)` : stderr;
|
|
2463
|
+
logger.log(
|
|
2464
|
+
`${icons.info} [verbose] ${label} stderr (${stderr.length} chars):
|
|
2465
|
+
---
|
|
2466
|
+
${truncated}
|
|
2467
|
+
---`
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2159
2471
|
function createAgentSession() {
|
|
2160
2472
|
return {
|
|
2161
2473
|
startTime: Date.now(),
|
|
@@ -2926,7 +3238,11 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
2926
3238
|
repoConfig,
|
|
2927
3239
|
roles,
|
|
2928
3240
|
synthesizeRepos,
|
|
2929
|
-
signal
|
|
3241
|
+
signal,
|
|
3242
|
+
cleanupTracker,
|
|
3243
|
+
verbose,
|
|
3244
|
+
agentOwner,
|
|
3245
|
+
userOrgs
|
|
2930
3246
|
} = options;
|
|
2931
3247
|
const { log, logError, logWarn } = logger;
|
|
2932
3248
|
log(`${icons.polling} Polling every ${pollIntervalMs / 1e3}s...`);
|
|
@@ -2958,10 +3274,20 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
2958
3274
|
const pollResponse = await client.post("/api/tasks/poll", pollBody);
|
|
2959
3275
|
consecutiveAuthErrors = 0;
|
|
2960
3276
|
consecutiveErrors = 0;
|
|
2961
|
-
const eligibleTasks = repoConfig ? pollResponse.tasks.filter(
|
|
3277
|
+
const eligibleTasks = repoConfig ? pollResponse.tasks.filter(
|
|
3278
|
+
(t) => isRepoAllowed(repoConfig, t.owner, t.repo, agentOwner, userOrgs)
|
|
3279
|
+
) : pollResponse.tasks;
|
|
2962
3280
|
const task = eligibleTasks.find(
|
|
2963
3281
|
(t) => (diffFailCounts.get(t.task_id) ?? 0) < MAX_DIFF_FETCH_ATTEMPTS
|
|
2964
3282
|
);
|
|
3283
|
+
if (cleanupTracker) {
|
|
3284
|
+
const swept = await cleanupTracker.sweep(cleanupWorktree);
|
|
3285
|
+
if (swept > 0) {
|
|
3286
|
+
log(
|
|
3287
|
+
`${icons.info} Cleaned up ${swept} stale codebase director${swept === 1 ? "y" : "ies"}`
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
2965
3291
|
if (task) {
|
|
2966
3292
|
const result = await handleTask(
|
|
2967
3293
|
client,
|
|
@@ -2973,7 +3299,9 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
2973
3299
|
logger,
|
|
2974
3300
|
agentSession,
|
|
2975
3301
|
routerRelay,
|
|
2976
|
-
signal
|
|
3302
|
+
signal,
|
|
3303
|
+
cleanupTracker,
|
|
3304
|
+
verbose
|
|
2977
3305
|
);
|
|
2978
3306
|
if (result.diffFetchFailed) {
|
|
2979
3307
|
agentSession.errorsEncountered++;
|
|
@@ -3031,7 +3359,7 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
|
|
|
3031
3359
|
await sleep2(pollIntervalMs, signal);
|
|
3032
3360
|
}
|
|
3033
3361
|
}
|
|
3034
|
-
async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal) {
|
|
3362
|
+
async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal, cleanupTracker, verbose) {
|
|
3035
3363
|
const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt, role } = task;
|
|
3036
3364
|
const { log, logError, logWarn } = logger;
|
|
3037
3365
|
const isIssueTask = pr_number === 0;
|
|
@@ -3068,6 +3396,7 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3068
3396
|
let diffContent = "";
|
|
3069
3397
|
let taskReviewDeps = reviewDeps;
|
|
3070
3398
|
let taskCheckoutPath = null;
|
|
3399
|
+
let taskBareRepoPath = null;
|
|
3071
3400
|
let contextBlock;
|
|
3072
3401
|
if (isIssueTask) {
|
|
3073
3402
|
log(" Issue-based task \u2014 skipping diff fetch");
|
|
@@ -3091,33 +3420,20 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3091
3420
|
);
|
|
3092
3421
|
return { diffFetchFailed: true };
|
|
3093
3422
|
}
|
|
3094
|
-
|
|
3423
|
+
{
|
|
3424
|
+
const codebaseDir = reviewDeps.codebaseDir || path8.join(CONFIG_DIR, "repos");
|
|
3095
3425
|
try {
|
|
3096
|
-
const result =
|
|
3097
|
-
log(` Codebase ${result.cloned ? "cloned" : "
|
|
3098
|
-
taskCheckoutPath = result.
|
|
3099
|
-
|
|
3426
|
+
const result = await checkoutWorktree(owner, repo, pr_number, codebaseDir, task_id);
|
|
3427
|
+
log(` Codebase ${result.cloned ? "cloned" : "cached"} \u2192 worktree: ${result.worktreePath}`);
|
|
3428
|
+
taskCheckoutPath = result.worktreePath;
|
|
3429
|
+
taskBareRepoPath = result.bareRepoPath;
|
|
3430
|
+
taskReviewDeps = { ...reviewDeps, codebaseDir: result.worktreePath };
|
|
3100
3431
|
} catch (err) {
|
|
3101
3432
|
logWarn(
|
|
3102
|
-
` Warning:
|
|
3433
|
+
` Warning: worktree checkout failed: ${err.message}. Continuing with diff-only review.`
|
|
3103
3434
|
);
|
|
3104
3435
|
taskReviewDeps = { ...reviewDeps, codebaseDir: null };
|
|
3105
3436
|
}
|
|
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
3437
|
}
|
|
3122
3438
|
try {
|
|
3123
3439
|
const prContext = await fetchPRContext(owner, repo, pr_number, {
|
|
@@ -3219,7 +3535,8 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3219
3535
|
agentInfo,
|
|
3220
3536
|
routerRelay,
|
|
3221
3537
|
signal,
|
|
3222
|
-
contextBlock
|
|
3538
|
+
contextBlock,
|
|
3539
|
+
verbose
|
|
3223
3540
|
);
|
|
3224
3541
|
} else {
|
|
3225
3542
|
await executeReviewTask(
|
|
@@ -3238,7 +3555,8 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3238
3555
|
agentInfo,
|
|
3239
3556
|
routerRelay,
|
|
3240
3557
|
signal,
|
|
3241
|
-
contextBlock
|
|
3558
|
+
contextBlock,
|
|
3559
|
+
verbose
|
|
3242
3560
|
);
|
|
3243
3561
|
}
|
|
3244
3562
|
agentSession.tasksCompleted++;
|
|
@@ -3252,8 +3570,12 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
|
|
|
3252
3570
|
await safeError(client, task_id, agentId, err.message, logger);
|
|
3253
3571
|
}
|
|
3254
3572
|
} finally {
|
|
3255
|
-
if (taskCheckoutPath) {
|
|
3256
|
-
|
|
3573
|
+
if (taskCheckoutPath && taskBareRepoPath) {
|
|
3574
|
+
if (cleanupTracker) {
|
|
3575
|
+
cleanupTracker.track(taskBareRepoPath, taskCheckoutPath);
|
|
3576
|
+
} else {
|
|
3577
|
+
await cleanupWorktree(taskBareRepoPath, taskCheckoutPath);
|
|
3578
|
+
}
|
|
3257
3579
|
}
|
|
3258
3580
|
}
|
|
3259
3581
|
return {};
|
|
@@ -3288,7 +3610,7 @@ async function safeError(client, taskId, agentId, error, logger) {
|
|
|
3288
3610
|
);
|
|
3289
3611
|
}
|
|
3290
3612
|
}
|
|
3291
|
-
async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock) {
|
|
3613
|
+
async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock, verbose) {
|
|
3292
3614
|
if (consumptionDeps.usageLimits?.maxTokensPerReview != null && consumptionDeps.usageTracker) {
|
|
3293
3615
|
const estimatedInput = estimateTokens(diffContent + prompt + (contextBlock ?? ""));
|
|
3294
3616
|
const perReviewCheck = consumptionDeps.usageTracker.checkPerReviewLimit(
|
|
@@ -3354,6 +3676,15 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
|
|
|
3354
3676
|
totalTokens: result.tokensUsed,
|
|
3355
3677
|
estimated: result.tokensEstimated
|
|
3356
3678
|
};
|
|
3679
|
+
if (verbose) {
|
|
3680
|
+
logVerboseToolOutput(
|
|
3681
|
+
logger,
|
|
3682
|
+
"Review",
|
|
3683
|
+
result.toolStdout,
|
|
3684
|
+
result.toolStderr,
|
|
3685
|
+
result.promptLength
|
|
3686
|
+
);
|
|
3687
|
+
}
|
|
3357
3688
|
}
|
|
3358
3689
|
const reviewMeta = {
|
|
3359
3690
|
model: agentInfo.model,
|
|
@@ -3383,7 +3714,7 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
|
|
|
3383
3714
|
logger.log(` ${icons.success} Review submitted (${tokensUsed.toLocaleString()} tokens)`);
|
|
3384
3715
|
logger.log(formatPostReviewStats(consumptionDeps.session));
|
|
3385
3716
|
}
|
|
3386
|
-
async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviews, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock) {
|
|
3717
|
+
async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviews, reviewDeps, consumptionDeps, logger, agentInfo, routerRelay, signal, contextBlock, verbose) {
|
|
3387
3718
|
const meta = { model: agentInfo.model, tool: agentInfo.tool };
|
|
3388
3719
|
if (reviews.length === 0) {
|
|
3389
3720
|
let reviewText;
|
|
@@ -3441,6 +3772,15 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
|
|
|
3441
3772
|
totalTokens: result.tokensUsed,
|
|
3442
3773
|
estimated: result.tokensEstimated
|
|
3443
3774
|
};
|
|
3775
|
+
if (verbose) {
|
|
3776
|
+
logVerboseToolOutput(
|
|
3777
|
+
logger,
|
|
3778
|
+
"Summary (single-agent)",
|
|
3779
|
+
result.toolStdout,
|
|
3780
|
+
result.toolStderr,
|
|
3781
|
+
result.promptLength
|
|
3782
|
+
);
|
|
3783
|
+
}
|
|
3444
3784
|
}
|
|
3445
3785
|
const headerSingle = buildMetadataHeader(verdict ?? "comment", meta);
|
|
3446
3786
|
const sanitizedReview = sanitizeTokens(headerSingle + reviewText);
|
|
@@ -3534,6 +3874,15 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
|
|
|
3534
3874
|
totalTokens: result.tokensUsed,
|
|
3535
3875
|
estimated: result.tokensEstimated
|
|
3536
3876
|
};
|
|
3877
|
+
if (verbose) {
|
|
3878
|
+
logVerboseToolOutput(
|
|
3879
|
+
logger,
|
|
3880
|
+
"Summary",
|
|
3881
|
+
result.toolStdout,
|
|
3882
|
+
result.toolStderr,
|
|
3883
|
+
result.promptLength
|
|
3884
|
+
);
|
|
3885
|
+
}
|
|
3537
3886
|
}
|
|
3538
3887
|
if (flaggedReviews.length > 0) {
|
|
3539
3888
|
logger.logWarn(
|
|
@@ -3592,7 +3941,7 @@ function sleep2(ms, signal) {
|
|
|
3592
3941
|
async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
|
|
3593
3942
|
const client = new ApiClient(platformUrl, {
|
|
3594
3943
|
authToken: options?.authToken,
|
|
3595
|
-
cliVersion: "0.18.
|
|
3944
|
+
cliVersion: "0.18.6",
|
|
3596
3945
|
versionOverride: options?.versionOverride,
|
|
3597
3946
|
onTokenRefresh: options?.onTokenRefresh
|
|
3598
3947
|
});
|
|
@@ -3617,6 +3966,9 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
|
|
|
3617
3966
|
if (options?.versionOverride) {
|
|
3618
3967
|
log(`${icons.info} Version override active: ${options.versionOverride}`);
|
|
3619
3968
|
}
|
|
3969
|
+
if (options?.verbose) {
|
|
3970
|
+
log(`${icons.info} Verbose mode enabled \u2014 tool stdout/stderr will be logged`);
|
|
3971
|
+
}
|
|
3620
3972
|
if (!reviewDeps) {
|
|
3621
3973
|
logError(`${icons.error} No review command configured. Set command in config.toml`);
|
|
3622
3974
|
return;
|
|
@@ -3630,6 +3982,16 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
|
|
|
3630
3982
|
logWarn(`${icons.warn} Command test failed (${result.error}). Reviews may fail.`);
|
|
3631
3983
|
}
|
|
3632
3984
|
}
|
|
3985
|
+
const ttlMs = options?.codebaseTtl != null ? parseTtl(options.codebaseTtl) : 0;
|
|
3986
|
+
const codebaseDir = reviewDeps.codebaseDir || path8.join(CONFIG_DIR, "repos");
|
|
3987
|
+
const scanTtl = Math.max(ttlMs, DEFAULT_CODEBASE_TTL_MS);
|
|
3988
|
+
const staleCount = scanAndCleanStaleWorktrees(codebaseDir, scanTtl);
|
|
3989
|
+
if (staleCount > 0) {
|
|
3990
|
+
log(
|
|
3991
|
+
`${icons.info} Cleaned up ${staleCount} stale codebase director${staleCount === 1 ? "y" : "ies"} on startup`
|
|
3992
|
+
);
|
|
3993
|
+
}
|
|
3994
|
+
const cleanupTracker = ttlMs > 0 ? new CodebaseCleanupTracker(ttlMs) : void 0;
|
|
3633
3995
|
const abortController = new AbortController();
|
|
3634
3996
|
process.on("SIGINT", () => {
|
|
3635
3997
|
abortController.abort();
|
|
@@ -3645,8 +4007,20 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
|
|
|
3645
4007
|
repoConfig: options?.repoConfig,
|
|
3646
4008
|
roles: options?.roles,
|
|
3647
4009
|
synthesizeRepos: options?.synthesizeRepos,
|
|
3648
|
-
signal: abortController.signal
|
|
4010
|
+
signal: abortController.signal,
|
|
4011
|
+
cleanupTracker,
|
|
4012
|
+
verbose: options?.verbose,
|
|
4013
|
+
agentOwner: options?.agentOwner,
|
|
4014
|
+
userOrgs: options?.userOrgs
|
|
3649
4015
|
});
|
|
4016
|
+
if (cleanupTracker && cleanupTracker.size > 0) {
|
|
4017
|
+
const finalSwept = await cleanupTracker.sweep(cleanupWorktree);
|
|
4018
|
+
if (finalSwept > 0) {
|
|
4019
|
+
log(
|
|
4020
|
+
`${icons.info} Cleaned up ${finalSwept} codebase director${finalSwept === 1 ? "y" : "ies"} on shutdown`
|
|
4021
|
+
);
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
3650
4024
|
if (deps.usageTracker) {
|
|
3651
4025
|
log(deps.usageTracker.formatSummary(deps.usageLimits ?? usageLimits));
|
|
3652
4026
|
}
|
|
@@ -3679,9 +4053,12 @@ async function startAgentRouter() {
|
|
|
3679
4053
|
throw err;
|
|
3680
4054
|
}
|
|
3681
4055
|
const storedAuth = loadAuth();
|
|
4056
|
+
const agentOwner = storedAuth?.github_username;
|
|
3682
4057
|
if (storedAuth) {
|
|
3683
4058
|
logger.log(`Authenticated as ${storedAuth.github_username}`);
|
|
3684
4059
|
}
|
|
4060
|
+
const repoConfig = agentConfig?.repos;
|
|
4061
|
+
const userOrgs = repoConfig?.mode === "private" ? await fetchUserOrgs(oauthToken) : /* @__PURE__ */ new Set();
|
|
3685
4062
|
const codebaseDir = resolveCodebaseDir(agentConfig?.codebase_dir, config.codebaseDir);
|
|
3686
4063
|
const reviewDeps = {
|
|
3687
4064
|
commandTemplate: commandTemplate ?? "",
|
|
@@ -3711,20 +4088,22 @@ async function startAgentRouter() {
|
|
|
3711
4088
|
maxConsecutiveErrors: config.maxConsecutiveErrors,
|
|
3712
4089
|
routerRelay: router,
|
|
3713
4090
|
reviewOnly: agentConfig?.review_only,
|
|
3714
|
-
repoConfig
|
|
4091
|
+
repoConfig,
|
|
3715
4092
|
roles,
|
|
3716
4093
|
synthesizeRepos: agentConfig?.synthesize_repos,
|
|
3717
4094
|
label,
|
|
3718
4095
|
authToken: oauthToken,
|
|
3719
4096
|
onTokenRefresh: () => getValidToken(config.platformUrl),
|
|
4097
|
+
agentOwner,
|
|
4098
|
+
userOrgs,
|
|
3720
4099
|
usageLimits: config.usageLimits,
|
|
3721
|
-
versionOverride
|
|
4100
|
+
versionOverride,
|
|
4101
|
+
codebaseTtl: config.codebaseTtl
|
|
3722
4102
|
}
|
|
3723
4103
|
);
|
|
3724
4104
|
router.stop();
|
|
3725
4105
|
}
|
|
3726
|
-
function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versionOverride) {
|
|
3727
|
-
const agentId = crypto2.randomUUID();
|
|
4106
|
+
function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versionOverride, verbose, instancesOverride, agentOwner, userOrgs) {
|
|
3728
4107
|
let commandTemplate;
|
|
3729
4108
|
let agentConfig;
|
|
3730
4109
|
if (config.agents && config.agents.length > agentIndex) {
|
|
@@ -3744,58 +4123,78 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versi
|
|
|
3744
4123
|
);
|
|
3745
4124
|
return null;
|
|
3746
4125
|
}
|
|
4126
|
+
const instanceCount = instancesOverride ?? agentConfig?.instances ?? 1;
|
|
3747
4127
|
const codebaseDir = resolveCodebaseDir(agentConfig?.codebase_dir, config.codebaseDir);
|
|
3748
4128
|
const reviewDeps = {
|
|
3749
4129
|
commandTemplate,
|
|
3750
4130
|
maxDiffSizeKb: config.maxDiffSizeKb,
|
|
3751
4131
|
codebaseDir
|
|
3752
4132
|
};
|
|
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
4133
|
const model = agentConfig?.model ?? "unknown";
|
|
3762
4134
|
const tool = agentConfig?.tool ?? "unknown";
|
|
3763
4135
|
const thinking = agentConfig?.thinking;
|
|
3764
4136
|
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
|
|
4137
|
+
const session = createSessionTracker();
|
|
4138
|
+
const usageTracker = new UsageTracker();
|
|
4139
|
+
const promises = [];
|
|
4140
|
+
for (let inst = 0; inst < instanceCount; inst++) {
|
|
4141
|
+
const agentId = crypto2.randomUUID();
|
|
4142
|
+
const instanceLabel = instanceCount > 1 ? `${label}#${inst + 1}` : label;
|
|
4143
|
+
const isRouter = agentConfig?.router === true;
|
|
4144
|
+
let routerRelay;
|
|
4145
|
+
if (isRouter) {
|
|
4146
|
+
routerRelay = new RouterRelay();
|
|
4147
|
+
routerRelay.start();
|
|
3784
4148
|
}
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
4149
|
+
const agentPromise = startAgent(
|
|
4150
|
+
agentId,
|
|
4151
|
+
config.platformUrl,
|
|
4152
|
+
{ model, tool, thinking },
|
|
4153
|
+
reviewDeps,
|
|
4154
|
+
{ agentId, session, usageTracker, usageLimits: config.usageLimits },
|
|
4155
|
+
{
|
|
4156
|
+
pollIntervalMs,
|
|
4157
|
+
maxConsecutiveErrors: config.maxConsecutiveErrors,
|
|
4158
|
+
routerRelay,
|
|
4159
|
+
reviewOnly: agentConfig?.review_only,
|
|
4160
|
+
repoConfig: agentConfig?.repos,
|
|
4161
|
+
roles,
|
|
4162
|
+
synthesizeRepos: agentConfig?.synthesize_repos,
|
|
4163
|
+
label: instanceLabel,
|
|
4164
|
+
authToken: oauthToken,
|
|
4165
|
+
onTokenRefresh: () => getValidToken(config.platformUrl),
|
|
4166
|
+
usageLimits: config.usageLimits,
|
|
4167
|
+
versionOverride,
|
|
4168
|
+
codebaseTtl: config.codebaseTtl,
|
|
4169
|
+
verbose,
|
|
4170
|
+
agentOwner,
|
|
4171
|
+
userOrgs
|
|
4172
|
+
}
|
|
4173
|
+
).finally(() => {
|
|
4174
|
+
routerRelay?.stop();
|
|
4175
|
+
});
|
|
4176
|
+
promises.push(agentPromise);
|
|
4177
|
+
}
|
|
4178
|
+
return promises;
|
|
3789
4179
|
}
|
|
3790
4180
|
var agentCommand = new Command("agent").description("Manage review agents");
|
|
3791
4181
|
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
4182
|
"--version-override <value>",
|
|
3793
4183
|
"Cloudflare Workers version override (e.g. opencara-server=abc123)"
|
|
3794
|
-
).action(
|
|
4184
|
+
).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
4185
|
async (opts) => {
|
|
3796
4186
|
const config = loadConfig();
|
|
3797
4187
|
const pollIntervalMs = parseInt(opts.pollInterval, 10) * 1e3;
|
|
3798
4188
|
const versionOverride = opts.versionOverride || process.env.OPENCARA_VERSION_OVERRIDE || null;
|
|
4189
|
+
let instancesOverride;
|
|
4190
|
+
if (opts.instances !== void 0) {
|
|
4191
|
+
if (!/^[1-9]\d*$/.test(opts.instances)) {
|
|
4192
|
+
console.error("--instances must be a positive integer");
|
|
4193
|
+
process.exit(1);
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
instancesOverride = parseInt(opts.instances, 10);
|
|
4197
|
+
}
|
|
3799
4198
|
let oauthToken;
|
|
3800
4199
|
try {
|
|
3801
4200
|
oauthToken = await getValidToken(config.platformUrl);
|
|
@@ -3808,22 +4207,35 @@ agentCommand.command("start").description("Start agents in polling mode").option
|
|
|
3808
4207
|
throw err;
|
|
3809
4208
|
}
|
|
3810
4209
|
const storedAuth = loadAuth();
|
|
4210
|
+
const agentOwner = storedAuth?.github_username;
|
|
3811
4211
|
if (storedAuth) {
|
|
3812
4212
|
console.log(`Authenticated as ${storedAuth.github_username}`);
|
|
3813
4213
|
}
|
|
4214
|
+
const needsOrgs = config.agents?.some((a) => a.repos?.mode === "private") ?? false;
|
|
4215
|
+
const userOrgs = needsOrgs ? await fetchUserOrgs(oauthToken) : /* @__PURE__ */ new Set();
|
|
3814
4216
|
if (opts.all) {
|
|
3815
4217
|
if (!config.agents || config.agents.length === 0) {
|
|
3816
4218
|
console.error("No agents configured in ~/.opencara/config.toml");
|
|
3817
4219
|
process.exit(1);
|
|
3818
4220
|
return;
|
|
3819
4221
|
}
|
|
3820
|
-
console.log(`Starting ${config.agents.length} agent(s)...`);
|
|
4222
|
+
console.log(`Starting ${config.agents.length} agent config(s)...`);
|
|
3821
4223
|
const promises = [];
|
|
3822
4224
|
let startFailed = false;
|
|
3823
4225
|
for (let i = 0; i < config.agents.length; i++) {
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
4226
|
+
const agentPromises = startAgentByIndex(
|
|
4227
|
+
config,
|
|
4228
|
+
i,
|
|
4229
|
+
pollIntervalMs,
|
|
4230
|
+
oauthToken,
|
|
4231
|
+
versionOverride,
|
|
4232
|
+
opts.verbose,
|
|
4233
|
+
instancesOverride,
|
|
4234
|
+
agentOwner,
|
|
4235
|
+
userOrgs
|
|
4236
|
+
);
|
|
4237
|
+
if (agentPromises) {
|
|
4238
|
+
promises.push(...agentPromises);
|
|
3827
4239
|
} else {
|
|
3828
4240
|
startFailed = true;
|
|
3829
4241
|
}
|
|
@@ -3838,7 +4250,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
|
|
|
3838
4250
|
"One or more agents could not start (see warnings above). Continuing with the rest."
|
|
3839
4251
|
);
|
|
3840
4252
|
}
|
|
3841
|
-
console.log(`${promises.length} agent(s) running. Press Ctrl+C to stop all.
|
|
4253
|
+
console.log(`${promises.length} agent instance(s) running. Press Ctrl+C to stop all.
|
|
3842
4254
|
`);
|
|
3843
4255
|
const results = await Promise.allSettled(promises);
|
|
3844
4256
|
const failures = results.filter((r) => r.status === "rejected");
|
|
@@ -3858,18 +4270,29 @@ agentCommand.command("start").description("Start agents in polling mode").option
|
|
|
3858
4270
|
process.exit(1);
|
|
3859
4271
|
return;
|
|
3860
4272
|
}
|
|
3861
|
-
const
|
|
4273
|
+
const agentPromises = startAgentByIndex(
|
|
3862
4274
|
config,
|
|
3863
4275
|
agentIndex,
|
|
3864
4276
|
pollIntervalMs,
|
|
3865
4277
|
oauthToken,
|
|
3866
|
-
versionOverride
|
|
4278
|
+
versionOverride,
|
|
4279
|
+
opts.verbose,
|
|
4280
|
+
instancesOverride,
|
|
4281
|
+
agentOwner,
|
|
4282
|
+
userOrgs
|
|
3867
4283
|
);
|
|
3868
|
-
if (!
|
|
4284
|
+
if (!agentPromises) {
|
|
3869
4285
|
process.exit(1);
|
|
3870
4286
|
return;
|
|
3871
4287
|
}
|
|
3872
|
-
await
|
|
4288
|
+
const results = await Promise.allSettled(agentPromises);
|
|
4289
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
4290
|
+
if (failures.length > 0) {
|
|
4291
|
+
for (const f of failures) {
|
|
4292
|
+
console.error(`Agent instance failed: ${f.reason}`);
|
|
4293
|
+
}
|
|
4294
|
+
process.exit(1);
|
|
4295
|
+
}
|
|
3873
4296
|
}
|
|
3874
4297
|
}
|
|
3875
4298
|
);
|
|
@@ -4005,8 +4428,8 @@ var PER_PAGE = 100;
|
|
|
4005
4428
|
var OPEN_MARKER = "<!-- opencara-dedup-index:open -->";
|
|
4006
4429
|
var RECENT_MARKER = "<!-- opencara-dedup-index:recent -->";
|
|
4007
4430
|
var ARCHIVED_MARKER = "<!-- opencara-dedup-index:archived -->";
|
|
4008
|
-
async function fetchRepoFile(owner, repo,
|
|
4009
|
-
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${
|
|
4431
|
+
async function fetchRepoFile(owner, repo, path9, token, fetchFn = fetch) {
|
|
4432
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path9}`;
|
|
4010
4433
|
const res = await fetchFn(url, {
|
|
4011
4434
|
headers: {
|
|
4012
4435
|
Authorization: `Bearer ${token}`,
|
|
@@ -4014,7 +4437,7 @@ async function fetchRepoFile(owner, repo, path7, token, fetchFn = fetch) {
|
|
|
4014
4437
|
}
|
|
4015
4438
|
});
|
|
4016
4439
|
if (res.status === 404) return null;
|
|
4017
|
-
if (!res.ok) throw new Error(`GitHub API error: ${res.status} fetching ${
|
|
4440
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status} fetching ${path9}`);
|
|
4018
4441
|
return res.text();
|
|
4019
4442
|
}
|
|
4020
4443
|
async function fetchAllPRs(owner, repo, token, fetchFn = fetch, log) {
|
|
@@ -4572,7 +4995,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
|
|
|
4572
4995
|
});
|
|
4573
4996
|
|
|
4574
4997
|
// src/index.ts
|
|
4575
|
-
var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.
|
|
4998
|
+
var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.6");
|
|
4576
4999
|
program.addCommand(agentCommand);
|
|
4577
5000
|
program.addCommand(authCommand());
|
|
4578
5001
|
program.addCommand(dedupCommand());
|