opencara 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +157 -83
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -240,6 +240,9 @@ var DEFAULT_TRIAGE_TRIGGER = {
240
240
  events: ["opened"],
241
241
  comment: "/opencara triage"
242
242
  };
243
+ var DEFAULT_ISSUE_REVIEW_TRIGGER = {
244
+ comment: "/opencara review-issue"
245
+ };
243
246
  var DEFAULT_TRIGGER = DEFAULT_REVIEW_TRIGGER;
244
247
  var DEFAULT_FEATURE_CONFIG = {
245
248
  prompt: "Review this pull request for bugs, security issues, and code quality.",
@@ -308,6 +311,24 @@ function parseAgentSlots(value) {
308
311
  }
309
312
  return slots.length > 0 ? slots : void 0;
310
313
  }
314
+ function parseNamedAgents(value) {
315
+ if (!Array.isArray(value))
316
+ return void 0;
317
+ const agents = [];
318
+ for (const item of value) {
319
+ if (!isObject(item))
320
+ continue;
321
+ if (typeof item.id !== "string" || typeof item.prompt !== "string")
322
+ continue;
323
+ const agent = { id: item.id, prompt: item.prompt };
324
+ if (typeof item.model === "string")
325
+ agent.model = item.model;
326
+ if (typeof item.tool === "string")
327
+ agent.tool = item.tool;
328
+ agents.push(agent);
329
+ }
330
+ return agents.length > 0 ? agents : void 0;
331
+ }
311
332
  function parseFeatureFields(raw, defaults) {
312
333
  const agentSlots = parseAgentSlots(raw.agents);
313
334
  return {
@@ -408,12 +429,16 @@ var DEFAULT_IMPLEMENT_FEATURE = {
408
429
  modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
409
430
  };
410
431
  function parseImplementSection(raw) {
411
- const base = parseFeatureFields(raw, DEFAULT_IMPLEMENT_FEATURE);
432
+ const { agents: _slots, ...base } = parseFeatureFields(raw, DEFAULT_IMPLEMENT_FEATURE);
412
433
  const triggerRaw = isObject(raw.trigger) ? raw.trigger : void 0;
434
+ const namedAgents = parseNamedAgents(raw.agents);
435
+ const agentField = typeof raw.agent_field === "string" ? raw.agent_field : void 0;
413
436
  return {
414
437
  ...base,
415
438
  enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
416
- trigger: parseTriggerSection(triggerRaw, DEFAULT_IMPLEMENT_TRIGGER)
439
+ trigger: parseTriggerSection(triggerRaw, DEFAULT_IMPLEMENT_TRIGGER),
440
+ ...namedAgents ? { agents: namedAgents } : {},
441
+ ...agentField ? { agent_field: agentField } : {}
417
442
  };
418
443
  }
419
444
  var DEFAULT_FIX_FEATURE = {
@@ -425,12 +450,33 @@ var DEFAULT_FIX_FEATURE = {
425
450
  modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
426
451
  };
427
452
  function parseFixSection(raw) {
428
- const base = parseFeatureFields(raw, DEFAULT_FIX_FEATURE);
453
+ const { agents: _slots, ...base } = parseFeatureFields(raw, DEFAULT_FIX_FEATURE);
454
+ const triggerRaw = isObject(raw.trigger) ? raw.trigger : void 0;
455
+ const namedAgents = parseNamedAgents(raw.agents);
456
+ const agentField = typeof raw.agent_field === "string" ? raw.agent_field : void 0;
457
+ return {
458
+ ...base,
459
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
460
+ trigger: parseTriggerSection(triggerRaw, DEFAULT_FIX_TRIGGER),
461
+ ...namedAgents ? { agents: namedAgents } : {},
462
+ ...agentField ? { agent_field: agentField } : {}
463
+ };
464
+ }
465
+ var DEFAULT_ISSUE_REVIEW_FEATURE = {
466
+ prompt: "Review this issue for clarity, completeness, and actionability.",
467
+ agentCount: 2,
468
+ timeout: "5m",
469
+ preferredModels: [],
470
+ preferredTools: [],
471
+ modelDiversityGraceMs: DEFAULT_MODEL_DIVERSITY_GRACE_MS
472
+ };
473
+ function parseIssueReviewSection(raw) {
474
+ const base = parseFeatureFields(raw, DEFAULT_ISSUE_REVIEW_FEATURE);
429
475
  const triggerRaw = isObject(raw.trigger) ? raw.trigger : void 0;
430
476
  return {
431
477
  ...base,
432
478
  enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
433
- trigger: parseTriggerSection(triggerRaw, DEFAULT_FIX_TRIGGER)
479
+ trigger: parseTriggerSection(triggerRaw, DEFAULT_ISSUE_REVIEW_TRIGGER)
434
480
  };
435
481
  }
436
482
  function parseOpenCaraConfig(toml) {
@@ -473,6 +519,9 @@ function parseOpenCaraConfig(toml) {
473
519
  if (isObject(raw.fix)) {
474
520
  config.fix = parseFixSection(raw.fix);
475
521
  }
522
+ if (isObject(raw.issue_review)) {
523
+ config.issue_review = parseIssueReviewSection(raw.issue_review);
524
+ }
476
525
  return config;
477
526
  }
478
527
  function parseLegacyReviewConfig(raw) {
@@ -2125,15 +2174,10 @@ ${reviewSections}`);
2125
2174
  }
2126
2175
  var TRIAGE_SYSTEM_PROMPT = `You are a triage agent for a software project. Your job is to analyze a GitHub issue and produce a structured triage report.
2127
2176
 
2128
- The project is a monorepo with the following packages:
2129
- - server \u2014 Hono server on Cloudflare Workers (webhook receiver, REST task API, GitHub integration)
2130
- - cli \u2014 Agent CLI npm package (HTTP polling, local review execution, router mode)
2131
- - shared \u2014 Shared TypeScript types (REST API contracts, review config parser)
2132
-
2133
2177
  ## Instructions
2134
2178
 
2135
2179
  1. **Categorize** the issue into one of: bug, feature, improvement, question, docs, chore
2136
- 2. **Identify the module** most relevant to this issue: server, cli, shared (or omit if unclear)
2180
+ 2. **Identify the module** most relevant to this issue (use the most appropriate component, package, or area name from the repository \u2014 or omit if unclear)
2137
2181
  3. **Assess priority**: critical (service down / data loss), high (blocks users), medium (important but not urgent), low (nice to have)
2138
2182
  4. **Estimate size**: XS (< 1hr), S (1-4hr), M (4hr-2d), L (2-5d), XL (> 5d)
2139
2183
  5. **Suggest labels** relevant to the issue (e.g., "bug", "enhancement", "docs", module names, etc.)
@@ -2148,7 +2192,7 @@ Respond with ONLY a JSON object (no markdown fences, no preamble, no explanation
2148
2192
  \`\`\`
2149
2193
  {
2150
2194
  "category": "bug" | "feature" | "improvement" | "question" | "docs" | "chore",
2151
- "module": "server" | "cli" | "shared",
2195
+ "module": "<string \u2014 component, package, or area name from the repository>",
2152
2196
  "priority": "critical" | "high" | "medium" | "low",
2153
2197
  "size": "XS" | "S" | "M" | "L" | "XL",
2154
2198
  "labels": ["label1", "label2"],
@@ -4325,6 +4369,34 @@ var DEFAULT_RECHECK_INTERVAL = 50;
4325
4369
  var DEFAULT_POLL_INTERVAL_MS = 1e4;
4326
4370
  var MAX_CONSECUTIVE_AUTH_ERRORS = 3;
4327
4371
  var MAX_POLL_BACKOFF_MS = 3e5;
4372
+ var SHUTDOWN_GRACE_MS = 5e3;
4373
+ function registerShutdownHandlers(controller, log, graceMs = SHUTDOWN_GRACE_MS) {
4374
+ let shutdownInitiated = false;
4375
+ let forceTimer;
4376
+ const onSignal = (signal) => {
4377
+ if (shutdownInitiated) {
4378
+ log(`${icons.stop} Received ${signal} again \u2014 forcing exit`);
4379
+ process.exit(1);
4380
+ }
4381
+ shutdownInitiated = true;
4382
+ log(`${icons.stop} Received ${signal} \u2014 shutting down gracefully...`);
4383
+ controller.abort();
4384
+ forceTimer = setTimeout(() => {
4385
+ log(`${icons.stop} Shutdown timed out after ${graceMs / 1e3}s \u2014 forcing exit`);
4386
+ process.exit(1);
4387
+ }, graceMs);
4388
+ forceTimer.unref();
4389
+ };
4390
+ const onSigint = () => onSignal("SIGINT");
4391
+ const onSigterm = () => onSignal("SIGTERM");
4392
+ process.on("SIGINT", onSigint);
4393
+ process.on("SIGTERM", onSigterm);
4394
+ return () => {
4395
+ process.removeListener("SIGINT", onSigint);
4396
+ process.removeListener("SIGTERM", onSigterm);
4397
+ if (forceTimer) clearTimeout(forceTimer);
4398
+ };
4399
+ }
4328
4400
  var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
4329
4401
  function toApiDiffUrl(webUrl) {
4330
4402
  const match = webUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\.diff)?$/);
@@ -5318,7 +5390,7 @@ function sleep2(ms, signal) {
5318
5390
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
5319
5391
  const client = new ApiClient(platformUrl, {
5320
5392
  authToken: options?.authToken,
5321
- cliVersion: "0.20.0",
5393
+ cliVersion: "0.21.0",
5322
5394
  versionOverride: options?.versionOverride,
5323
5395
  onTokenRefresh: options?.onTokenRefresh
5324
5396
  });
@@ -5370,44 +5442,43 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
5370
5442
  }
5371
5443
  const cleanupTracker = ttlMs > 0 ? new CodebaseCleanupTracker(ttlMs) : void 0;
5372
5444
  const abortController = new AbortController();
5373
- process.on("SIGINT", () => {
5374
- abortController.abort();
5375
- });
5376
- process.on("SIGTERM", () => {
5377
- abortController.abort();
5378
- });
5379
- await pollLoop(client, agentId, reviewDeps, deps, agentInfo, logger, agentSession, {
5380
- pollIntervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
5381
- maxConsecutiveErrors: options?.maxConsecutiveErrors ?? DEFAULT_MAX_CONSECUTIVE_ERRORS,
5382
- routerRelay: options?.routerRelay,
5383
- reviewOnly: options?.reviewOnly,
5384
- repoConfig: options?.repoConfig,
5385
- roles: options?.roles,
5386
- synthesizeRepos: options?.synthesizeRepos,
5387
- signal: abortController.signal,
5388
- cleanupTracker,
5389
- verbose: options?.verbose,
5390
- agentOwner: options?.agentOwner,
5391
- userOrgs: options?.userOrgs
5392
- });
5393
- if (cleanupTracker && cleanupTracker.size > 0) {
5394
- const finalSwept = await cleanupTracker.sweep(cleanupWorktree);
5395
- if (finalSwept > 0) {
5445
+ const removeShutdownHandlers = registerShutdownHandlers(abortController, log);
5446
+ try {
5447
+ await pollLoop(client, agentId, reviewDeps, deps, agentInfo, logger, agentSession, {
5448
+ pollIntervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
5449
+ maxConsecutiveErrors: options?.maxConsecutiveErrors ?? DEFAULT_MAX_CONSECUTIVE_ERRORS,
5450
+ routerRelay: options?.routerRelay,
5451
+ reviewOnly: options?.reviewOnly,
5452
+ repoConfig: options?.repoConfig,
5453
+ roles: options?.roles,
5454
+ synthesizeRepos: options?.synthesizeRepos,
5455
+ signal: abortController.signal,
5456
+ cleanupTracker,
5457
+ verbose: options?.verbose,
5458
+ agentOwner: options?.agentOwner,
5459
+ userOrgs: options?.userOrgs
5460
+ });
5461
+ if (cleanupTracker && cleanupTracker.size > 0) {
5462
+ const finalSwept = await cleanupTracker.sweep(cleanupWorktree);
5463
+ if (finalSwept > 0) {
5464
+ log(
5465
+ `${icons.info} Cleaned up ${finalSwept} codebase director${finalSwept === 1 ? "y" : "ies"} on shutdown`
5466
+ );
5467
+ }
5468
+ }
5469
+ if (deps.usageTracker) {
5396
5470
  log(
5397
- `${icons.info} Cleaned up ${finalSwept} codebase director${finalSwept === 1 ? "y" : "ies"} on shutdown`
5471
+ deps.usageTracker.formatSummary(
5472
+ deps.usageLimits ?? usageLimits,
5473
+ deps.agentLimits,
5474
+ deps.agentId
5475
+ )
5398
5476
  );
5399
5477
  }
5478
+ log(formatExitSummary(agentSession));
5479
+ } finally {
5480
+ removeShutdownHandlers();
5400
5481
  }
5401
- if (deps.usageTracker) {
5402
- log(
5403
- deps.usageTracker.formatSummary(
5404
- deps.usageLimits ?? usageLimits,
5405
- deps.agentLimits,
5406
- deps.agentId
5407
- )
5408
- );
5409
- }
5410
- log(formatExitSummary(agentSession));
5411
5482
  }
5412
5483
  async function batchPollLoop(client, agentStates, options) {
5413
5484
  const {
@@ -5605,7 +5676,7 @@ async function startBatchAgents(config, agents, pollIntervalMs, oauthToken, opti
5605
5676
  const { versionOverride, verbose, instancesOverride, agentOwner, userOrgs } = options;
5606
5677
  const client = new ApiClient(config.platformUrl, {
5607
5678
  authToken: oauthToken,
5608
- cliVersion: "0.20.0",
5679
+ cliVersion: "0.21.0",
5609
5680
  versionOverride,
5610
5681
  onTokenRefresh: () => getValidToken(config.platformUrl, { configPath: config.authFile })
5611
5682
  });
@@ -5726,45 +5797,48 @@ async function startBatchAgents(config, agents, pollIntervalMs, oauthToken, opti
5726
5797
  }
5727
5798
  }
5728
5799
  const abortController = new AbortController();
5729
- process.on("SIGINT", () => abortController.abort());
5730
- process.on("SIGTERM", () => abortController.abort());
5800
+ const removeShutdownHandlers = registerShutdownHandlers(abortController, log);
5731
5801
  log(`${agentStates.length} agent instance(s) running in batch mode. Press Ctrl+C to stop.
5732
5802
  `);
5733
- await batchPollLoop(client, agentStates, {
5734
- pollIntervalMs,
5735
- maxConsecutiveErrors: config.maxConsecutiveErrors,
5736
- signal: abortController.signal,
5737
- accessibleRepos,
5738
- githubToken: oauthToken
5739
- });
5740
- await Promise.allSettled(
5741
- agentStates.map(async (state) => {
5742
- state.routerRelay?.stop();
5743
- if (state.cleanupTracker && state.cleanupTracker.size > 0) {
5744
- const swept = await state.cleanupTracker.sweep(cleanupWorktree);
5745
- if (swept > 0) {
5803
+ try {
5804
+ await batchPollLoop(client, agentStates, {
5805
+ pollIntervalMs,
5806
+ maxConsecutiveErrors: config.maxConsecutiveErrors,
5807
+ signal: abortController.signal,
5808
+ accessibleRepos,
5809
+ githubToken: oauthToken
5810
+ });
5811
+ await Promise.allSettled(
5812
+ agentStates.map(async (state) => {
5813
+ state.routerRelay?.stop();
5814
+ if (state.cleanupTracker && state.cleanupTracker.size > 0) {
5815
+ const swept = await state.cleanupTracker.sweep(cleanupWorktree);
5816
+ if (swept > 0) {
5817
+ state.logger.log(
5818
+ `${icons.info} Cleaned up ${swept} codebase director${swept === 1 ? "y" : "ies"} on shutdown`
5819
+ );
5820
+ }
5821
+ }
5822
+ if (state.consumptionDeps.usageTracker) {
5823
+ const limits = state.consumptionDeps.usageLimits ?? {
5824
+ maxTasksPerDay: null,
5825
+ maxTokensPerDay: null,
5826
+ maxTokensPerReview: null
5827
+ };
5746
5828
  state.logger.log(
5747
- `${icons.info} Cleaned up ${swept} codebase director${swept === 1 ? "y" : "ies"} on shutdown`
5829
+ state.consumptionDeps.usageTracker.formatSummary(
5830
+ limits,
5831
+ state.consumptionDeps.agentLimits,
5832
+ state.consumptionDeps.agentId
5833
+ )
5748
5834
  );
5749
5835
  }
5750
- }
5751
- if (state.consumptionDeps.usageTracker) {
5752
- const limits = state.consumptionDeps.usageLimits ?? {
5753
- maxTasksPerDay: null,
5754
- maxTokensPerDay: null,
5755
- maxTokensPerReview: null
5756
- };
5757
- state.logger.log(
5758
- state.consumptionDeps.usageTracker.formatSummary(
5759
- limits,
5760
- state.consumptionDeps.agentLimits,
5761
- state.consumptionDeps.agentId
5762
- )
5763
- );
5764
- }
5765
- state.logger.log(formatExitSummary(state.agentSession));
5766
- })
5767
- );
5836
+ state.logger.log(formatExitSummary(state.agentSession));
5837
+ })
5838
+ );
5839
+ } finally {
5840
+ removeShutdownHandlers();
5841
+ }
5768
5842
  }
5769
5843
  async function startAgentRouter() {
5770
5844
  const config = loadConfig();
@@ -5945,7 +6019,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
5945
6019
  }
5946
6020
  config = loadConfig();
5947
6021
  }
5948
- console.log(formatVersionBanner("0.20.0", "787d0af"));
6022
+ console.log(formatVersionBanner("0.21.0", "a24bc6d"));
5949
6023
  if (config.agents && config.agents.length > 0) {
5950
6024
  const toolEntries = config.agents.map((a) => ({
5951
6025
  tool: a.tool,
@@ -6768,7 +6842,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
6768
6842
  });
6769
6843
 
6770
6844
  // src/index.ts
6771
- var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.20.0"} (${"787d0af"})`);
6845
+ var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version(`${"0.21.0"} (${"a24bc6d"})`);
6772
6846
  program.addCommand(agentCommand);
6773
6847
  program.addCommand(authCommand());
6774
6848
  program.addCommand(dedupCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
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",