karajan-code 1.34.2 → 1.34.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.34.2",
3
+ "version": "1.34.3",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -162,23 +162,23 @@ async function checkBecariaSecrets() {
162
162
  const { detectRepo } = await import("../becaria/repo.js");
163
163
  const repo = await detectRepo();
164
164
  if (!repo) return null;
165
+
165
166
  const secretsRes = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
166
167
  if (secretsRes.exitCode !== 0) return null;
168
+
167
169
  const names = new Set(secretsRes.stdout.split("\n").map((s) => s.trim()));
168
170
  const hasAppId = names.has("BECARIA_APP_ID");
169
171
  const hasKey = names.has("BECARIA_APP_PRIVATE_KEY");
170
172
  const secretsOk = hasAppId && hasKey;
173
+ const missing = [!hasAppId && "BECARIA_APP_ID", !hasKey && "BECARIA_APP_PRIVATE_KEY"].filter(Boolean).join(" ");
171
174
  return {
172
175
  name: "becaria:secrets",
173
176
  label: "BecarIA: GitHub secrets",
174
177
  ok: secretsOk,
175
- detail: secretsOk
176
- ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found"
177
- : `Missing: ${[!hasAppId && "BECARIA_APP_ID", !hasKey && "BECARIA_APP_PRIVATE_KEY"].filter(Boolean).join(" ")}`,
178
+ detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${missing}`,
178
179
  fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
179
180
  };
180
181
  } catch {
181
- // Skip secrets check if we can't access the API
182
182
  return null;
183
183
  }
184
184
  }
@@ -205,33 +205,15 @@ async function checkBecariaInfra(config) {
205
205
  }
206
206
 
207
207
  async function checkRtk() {
208
+ const NOT_FOUND_DETAIL = "Not found — install for 60-90% token savings: brew install rtk";
209
+ let detail = NOT_FOUND_DETAIL;
208
210
  try {
209
211
  const res = await runCommand("rtk", ["--version"]);
210
212
  if (res.exitCode === 0) {
211
- return {
212
- name: "rtk",
213
- label: "RTK (Rust Token Killer)",
214
- ok: true,
215
- detail: `${res.stdout.trim()} — token savings active`,
216
- fix: null
217
- };
213
+ detail = `${res.stdout.trim()} — token savings active`;
218
214
  }
219
- return {
220
- name: "rtk",
221
- label: "RTK (Rust Token Killer)",
222
- ok: true,
223
- detail: "Not found — install for 60-90% token savings: brew install rtk",
224
- fix: null
225
- };
226
- } catch {
227
- return {
228
- name: "rtk",
229
- label: "RTK (Rust Token Killer)",
230
- ok: true,
231
- detail: "Not found — install for 60-90% token savings: brew install rtk",
232
- fix: null
233
- };
234
- }
215
+ } catch { /* not installed */ }
216
+ return { name: "rtk", label: "RTK (Rust Token Killer)", ok: true, detail, fix: null };
235
217
  }
236
218
 
237
219
  async function checkRuleFiles(config) {
@@ -155,50 +155,49 @@ async function buildReport(dir, sessionId) {
155
155
  return report;
156
156
  }
157
157
 
158
- function printTextReport(report) {
159
- let budgetText = "N/A";
160
- if (typeof report.budget_consumed?.consumed_usd === "number") {
161
- const limitSuffix = typeof report.budget_consumed?.limit_usd === "number"
162
- ? ` / $${report.budget_consumed.limit_usd.toFixed(2)}`
163
- : "";
164
- budgetText = `$${report.budget_consumed.consumed_usd.toFixed(2)}${limitSuffix}`;
165
- }
158
+ function formatBudgetText(budget) {
159
+ if (typeof budget?.consumed_usd !== "number") return "N/A";
160
+ const limitSuffix = typeof budget.limit_usd === "number" ? ` / $${budget.limit_usd.toFixed(2)}` : "";
161
+ return `$${budget.consumed_usd.toFixed(2)}${limitSuffix}`;
162
+ }
166
163
 
167
- const planText = report.plan_executed.length > 0 ? report.plan_executed.join(" -> ") : "N/A";
168
- const iterationText =
169
- report.iterations.length > 0
170
- ? report.iterations
171
- .map(
172
- (item) =>
173
- `#${item.iteration} coder=${item.coder_runs} reviewer_attempts=${item.reviewer_attempts} approved=${item.reviewer_approved}`
174
- )
175
- .join("\n")
176
- : "N/A";
177
- const commitsText =
178
- report.commits_generated.ids.length > 0
179
- ? `${report.commits_generated.count} (${report.commits_generated.ids.join(", ")})`
180
- : String(report.commits_generated.count);
164
+ function formatIterationsText(iterations) {
165
+ if (iterations.length === 0) return "N/A";
166
+ return iterations
167
+ .map((item) => `#${item.iteration} coder=${item.coder_runs} reviewer_attempts=${item.reviewer_attempts} approved=${item.reviewer_approved}`)
168
+ .join("\n");
169
+ }
170
+
171
+ function formatCommitsText(commits) {
172
+ return commits.ids.length > 0
173
+ ? `${commits.count} (${commits.ids.join(", ")})`
174
+ : String(commits.count);
175
+ }
176
+
177
+ function printPgCardLine(report) {
178
+ if (!report.pg_task_id) return;
179
+ const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
180
+ console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
181
+ }
182
+
183
+ function printTextReport(report) {
184
+ const sonar = report.sonar_issues_resolved;
181
185
 
182
186
  console.log(`Session: ${report.session_id}`);
183
- if (report.pg_task_id) {
184
- const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
185
- console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
186
- }
187
+ printPgCardLine(report);
187
188
  console.log(`Status: ${report.status}`);
188
189
  console.log("Task Description:");
189
190
  console.log(report.task_description || "N/A");
190
191
  console.log("Plan Executed:");
191
- console.log(planText);
192
+ console.log(report.plan_executed.length > 0 ? report.plan_executed.join(" -> ") : "N/A");
192
193
  console.log("Iterations (Coder/Reviewer):");
193
- console.log(iterationText);
194
+ console.log(formatIterationsText(report.iterations));
194
195
  console.log("Sonar Issues Resolved:");
195
- console.log(
196
- `initial=${report.sonar_issues_resolved.initial_open_issues ?? "N/A"} final=${report.sonar_issues_resolved.final_open_issues ?? "N/A"} resolved=${report.sonar_issues_resolved.resolved}`
197
- );
196
+ console.log(`initial=${sonar.initial_open_issues ?? "N/A"} final=${sonar.final_open_issues ?? "N/A"} resolved=${sonar.resolved}`);
198
197
  console.log("Budget Consumed:");
199
- console.log(budgetText);
198
+ console.log(formatBudgetText(report.budget_consumed));
200
199
  console.log("Commits Generated:");
201
- console.log(commitsText);
200
+ console.log(formatCommitsText(report.commits_generated));
202
201
  }
203
202
 
204
203
  function formatDuration(ms) {
@@ -329,10 +328,7 @@ async function resolveTraceOptions(currency) {
329
328
 
330
329
  function printTraceReport(report, currency, exchangeRate) {
331
330
  console.log(`Session: ${report.session_id}`);
332
- if (report.pg_task_id) {
333
- const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
334
- console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
335
- }
331
+ printPgCardLine(report);
336
332
  console.log(`Status: ${report.status}`);
337
333
  console.log(`Task: ${report.task_description || "N/A"}`);
338
334
  console.log("");
package/src/config.js CHANGED
@@ -453,27 +453,39 @@ export function isModelCompatible(agent, model) {
453
453
  return true;
454
454
  }
455
455
 
456
+ // Roles that inherit provider/model from the coder when not explicitly configured
457
+ const CODER_INHERITED_ROLES = new Set([
458
+ "planner", "refactorer", "solomon", "researcher", "tester", "security",
459
+ "impeccable", "triage", "discover", "architect", "audit", "hu_reviewer", "hu-reviewer"
460
+ ]);
461
+
462
+ function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
463
+ if (roleConfig.provider) return roleConfig.provider;
464
+ if (role === "coder") return legacyCoder;
465
+ if (role === "reviewer") return legacyReviewer;
466
+ if (CODER_INHERITED_ROLES.has(role)) return roles.coder?.provider || legacyCoder;
467
+ return null;
468
+ }
469
+
470
+ function resolveModel(roleConfig, role, config) {
471
+ if (roleConfig.model) return { model: roleConfig.model, inherited: false };
472
+ if (role === "coder") return { model: config?.coder_options?.model ?? null, inherited: false };
473
+ if (role === "reviewer") return { model: config?.reviewer_options?.model ?? null, inherited: false };
474
+ if (CODER_INHERITED_ROLES.has(role)) {
475
+ const model = config?.coder_options?.model ?? null;
476
+ return { model, inherited: !!model };
477
+ }
478
+ return { model: null, inherited: false };
479
+ }
480
+
456
481
  export function resolveRole(config, role) {
457
482
  const roles = config?.roles || {};
458
483
  const roleConfig = roles[role] || {};
459
484
  const legacyCoder = config?.coder || null;
460
485
  const legacyReviewer = config?.reviewer || null;
461
486
 
462
- let provider = roleConfig.provider ?? null;
463
- if (!provider && role === "coder") provider = legacyCoder;
464
- if (!provider && role === "reviewer") provider = legacyReviewer;
465
- if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "audit" || role === "hu_reviewer" || role === "hu-reviewer")) {
466
- provider = roles.coder?.provider || legacyCoder;
467
- }
468
-
469
- let model = roleConfig.model ?? null;
470
- let modelIsInherited = false;
471
- if (!model && role === "coder") model = config?.coder_options?.model ?? null;
472
- if (!model && role === "reviewer") model = config?.reviewer_options?.model ?? null;
473
- if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "hu_reviewer" || role === "hu-reviewer")) {
474
- model = config?.coder_options?.model ?? null;
475
- modelIsInherited = !!model;
476
- }
487
+ const provider = resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer);
488
+ let { model, inherited: modelIsInherited } = resolveModel(roleConfig, role, config);
477
489
 
478
490
  // Drop inherited model if incompatible with the resolved provider
479
491
  if (modelIsInherited && provider && model && !isModelCompatible(provider, model)) {
@@ -252,6 +252,31 @@ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, ru
252
252
  }
253
253
  }
254
254
 
255
+ const PIPELINE_PROVIDER_ROLES = [
256
+ ["triage", true],
257
+ ["planner", true],
258
+ ["refactorer", true],
259
+ ["researcher", true],
260
+ ["tester", true],
261
+ ["security", true]
262
+ ];
263
+
264
+ function collectRequiredProviders(config) {
265
+ const providers = [
266
+ resolveRole(config, "coder").provider,
267
+ config.reviewer_options?.fallback_reviewer
268
+ ];
269
+ if (config.pipeline?.reviewer?.enabled !== false) {
270
+ providers.push(resolveRole(config, "reviewer").provider);
271
+ }
272
+ for (const [role, requireEnabled] of PIPELINE_PROVIDER_ROLES) {
273
+ if (requireEnabled && config.pipeline?.[role]?.enabled) {
274
+ providers.push(resolveRole(config, role).provider);
275
+ }
276
+ }
277
+ return providers;
278
+ }
279
+
255
280
  export async function handleRunDirect(a, server, extra) {
256
281
  const config = await buildConfig(a);
257
282
  await assertNotOnBaseBranch(config);
@@ -263,20 +288,7 @@ export async function handleRunDirect(a, server, extra) {
263
288
  await cleanupExpiredSessions({ logger });
264
289
  } catch { /* non-blocking */ }
265
290
 
266
- const requiredProviders = [
267
- resolveRole(config, "coder").provider,
268
- config.reviewer_options?.fallback_reviewer
269
- ];
270
- if (config.pipeline?.reviewer?.enabled !== false) {
271
- requiredProviders.push(resolveRole(config, "reviewer").provider);
272
- }
273
- if (config.pipeline?.triage?.enabled) requiredProviders.push(resolveRole(config, "triage").provider);
274
- if (config.pipeline?.planner?.enabled) requiredProviders.push(resolveRole(config, "planner").provider);
275
- if (config.pipeline?.refactorer?.enabled) requiredProviders.push(resolveRole(config, "refactorer").provider);
276
- if (config.pipeline?.researcher?.enabled) requiredProviders.push(resolveRole(config, "researcher").provider);
277
- if (config.pipeline?.tester?.enabled) requiredProviders.push(resolveRole(config, "tester").provider);
278
- if (config.pipeline?.security?.enabled) requiredProviders.push(resolveRole(config, "security").provider);
279
- await assertAgentsAvailable(requiredProviders);
291
+ await assertAgentsAvailable(collectRequiredProviders(config));
280
292
 
281
293
  const projectDir = await resolveProjectDir(server, a.projectDir);
282
294
  const runLog = createRunLog(projectDir);
@@ -343,13 +355,16 @@ export async function handleResumeDirect(a, server, extra) {
343
355
  }
344
356
  }
345
357
 
358
+ const EVENT_LOG_LEVELS = {
359
+ "agent:stall": "warning",
360
+ "agent:heartbeat": "info"
361
+ };
362
+
346
363
  function buildDirectEmitter(server, runLog, extra) {
347
364
  const emitter = new EventEmitter();
348
365
  emitter.on("progress", (event) => {
349
366
  try {
350
- let level = "debug";
351
- if (event.type === "agent:stall") level = "warning";
352
- else if (event.type === "agent:heartbeat") level = "info";
367
+ const level = EVENT_LOG_LEVELS[event.type] || "debug";
353
368
  server.sendLoggingMessage({ level, logger: "karajan", data: event });
354
369
  } catch { /* best-effort */ }
355
370
  if (runLog) runLog.logEvent(event);
@@ -476,7 +476,7 @@ async function checkBudgetExceeded({ budgetTracker, config, session, emitter, ev
476
476
  }
477
477
 
478
478
  async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger }) {
479
- if (!stageResult?.action || stageResult.action !== "standby") {
479
+ if (stageResult?.action !== "standby") {
480
480
  return { handled: false };
481
481
  }
482
482
 
@@ -511,6 +511,47 @@ async function handleStandbyResult({ stageResult, session, emitter, eventBase, i
511
511
  return { handled: true, action: "retry" };
512
512
  }
513
513
 
514
+ function formatCommitList(commits) {
515
+ return commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
516
+ }
517
+
518
+ async function becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment }) {
519
+ const pushResult = await incrementalPush({ gitCtx, task, logger, session });
520
+ if (!pushResult) return;
521
+
522
+ session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
523
+ await saveSession(session);
524
+
525
+ if (!repo) return;
526
+ const feedback = session.last_reviewer_feedback || "N/A";
527
+ await dispatchComment({
528
+ repo, prNumber: session.becaria_pr_number, agent: "Coder",
529
+ body: `Issues corregidos:\n${feedback}\n\nCommits:\n${formatCommitList(pushResult.commits)}`,
530
+ becariaConfig: config.becaria
531
+ });
532
+ }
533
+
534
+ async function becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment }) {
535
+ const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
536
+ if (!earlyPr) return;
537
+
538
+ session.becaria_pr_number = earlyPr.prNumber;
539
+ session.becaria_pr_url = earlyPr.prUrl;
540
+ session.becaria_commits = earlyPr.commits;
541
+ await saveSession(session);
542
+ emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
543
+ message: `Early PR created: #${earlyPr.prNumber}`,
544
+ detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
545
+ }));
546
+
547
+ if (!repo) return;
548
+ await dispatchComment({
549
+ repo, prNumber: earlyPr.prNumber, agent: "Coder",
550
+ body: `Iteración ${i} completada.\n\nCommits:\n${formatCommitList(earlyPr.commits)}`,
551
+ becariaConfig: config.becaria
552
+ });
553
+ }
554
+
514
555
  async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i }) {
515
556
  if (!becariaEnabled) return;
516
557
 
@@ -520,48 +561,26 @@ async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emi
520
561
  const repo = await detectRepo();
521
562
 
522
563
  if (session.becaria_pr_number) {
523
- const pushResult = await incrementalPush({ gitCtx, task, logger, session });
524
- if (pushResult) {
525
- session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
526
- await saveSession(session);
527
-
528
- if (repo) {
529
- const feedback = session.last_reviewer_feedback || "N/A";
530
- const commitList = pushResult.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
531
- await dispatchComment({
532
- repo, prNumber: session.becaria_pr_number, agent: "Coder",
533
- body: `Issues corregidos:\n${feedback}\n\nCommits:\n${commitList}`,
534
- becariaConfig: config.becaria
535
- });
536
- }
537
- }
564
+ await becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment });
538
565
  } else {
539
- const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
540
- if (earlyPr) {
541
- session.becaria_pr_number = earlyPr.prNumber;
542
- session.becaria_pr_url = earlyPr.prUrl;
543
- session.becaria_commits = earlyPr.commits;
544
- await saveSession(session);
545
- emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
546
- message: `Early PR created: #${earlyPr.prNumber}`,
547
- detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
548
- }));
549
-
550
- if (repo) {
551
- const commitList = earlyPr.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
552
- await dispatchComment({
553
- repo, prNumber: earlyPr.prNumber, agent: "Coder",
554
- body: `Iteración ${i} completada.\n\nCommits:\n${commitList}`,
555
- becariaConfig: config.becaria
556
- });
557
- }
558
- }
566
+ await becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment });
559
567
  }
560
568
  } catch (err) {
561
569
  logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
562
570
  }
563
571
  }
564
572
 
573
+ function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
574
+ for (const alert of alerts) {
575
+ emitProgress(emitter, makeEvent("solomon:alert", { ...eventBase, stage: "solomon" }, {
576
+ status: alert.severity === "critical" ? "fail" : "warn",
577
+ message: alert.message,
578
+ detail: alert.detail
579
+ }));
580
+ logger.warn(`Solomon alert [${alert.rule}]: ${alert.message}`);
581
+ }
582
+ }
583
+
565
584
  async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled, blockingIssues }) {
566
585
  if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
567
586
 
@@ -571,15 +590,7 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
571
590
  const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
572
591
 
573
592
  if (rulesResult.alerts.length > 0) {
574
- for (const alert of rulesResult.alerts) {
575
- emitProgress(emitter, makeEvent("solomon:alert", { ...eventBase, stage: "solomon" }, {
576
- status: alert.severity === "critical" ? "fail" : "warn",
577
- message: alert.message,
578
- detail: alert.detail
579
- }));
580
- logger.warn(`Solomon alert [${alert.rule}]: ${alert.message}`);
581
- }
582
-
593
+ emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
583
594
  const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
584
595
  if (pauseResult) return pauseResult;
585
596
  }
@@ -623,6 +634,27 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
623
634
  return null;
624
635
  }
625
636
 
637
+ function formatBlockingIssues(issues) {
638
+ return issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
639
+ }
640
+
641
+ function formatSuggestions(suggestions) {
642
+ return suggestions?.map((s) => {
643
+ const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
644
+ return `- ${detail}`;
645
+ }).join("\n") || "";
646
+ }
647
+
648
+ function buildReviewCommentBody(review, i) {
649
+ const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
650
+ const blocking = formatBlockingIssues(review.blocking_issues);
651
+ const suggestions = formatSuggestions(review.non_blocking_suggestions);
652
+ let body = `Review iteración ${i}: ${status}`;
653
+ if (blocking) body += `\n\n**Blocking:**\n${blocking}`;
654
+ if (suggestions) body += `\n\n**Suggestions:**\n${suggestions}`;
655
+ return body;
656
+ }
657
+
626
658
  async function handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger }) {
627
659
  if (!becariaEnabled || !session.becaria_pr_number) return;
628
660
 
@@ -633,33 +665,19 @@ async function handleBecariaReviewDispatch({ becariaEnabled, config, session, re
633
665
  if (!repo) return;
634
666
 
635
667
  const bc = config.becaria;
636
- if (review.approved) {
637
- await dispatchReview({
638
- repo, prNumber: session.becaria_pr_number,
639
- event: "APPROVE", body: review.summary || "Approved", agent: "Reviewer", becariaConfig: bc
640
- });
641
- } else {
642
- const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
643
- await dispatchReview({
644
- repo, prNumber: session.becaria_pr_number,
645
- event: "REQUEST_CHANGES",
646
- body: blocking || review.summary || "Changes requested",
647
- agent: "Reviewer", becariaConfig: bc
648
- });
649
- }
668
+ const reviewEvent = review.approved ? "APPROVE" : "REQUEST_CHANGES";
669
+ const reviewBody = review.approved
670
+ ? (review.summary || "Approved")
671
+ : (formatBlockingIssues(review.blocking_issues) || review.summary || "Changes requested");
672
+
673
+ await dispatchReview({
674
+ repo, prNumber: session.becaria_pr_number,
675
+ event: reviewEvent, body: reviewBody, agent: "Reviewer", becariaConfig: bc
676
+ });
650
677
 
651
- const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
652
- const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
653
- const suggestions = review.non_blocking_suggestions?.map((s) => {
654
- const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
655
- return `- ${detail}`;
656
- }).join("\n") || "";
657
- let reviewBody = `Review iteración ${i}: ${status}`;
658
- if (blocking) reviewBody += `\n\n**Blocking:**\n${blocking}`;
659
- if (suggestions) reviewBody += `\n\n**Suggestions:**\n${suggestions}`;
660
678
  await dispatchComment({
661
679
  repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
662
- body: reviewBody, becariaConfig: bc
680
+ body: buildReviewCommentBody(review, i), becariaConfig: bc
663
681
  });
664
682
 
665
683
  logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
@@ -1095,6 +1113,24 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1095
1113
  return { approved: false, sessionId: session.id, reason: "max_iterations" };
1096
1114
  }
1097
1115
 
1116
+ async function tryAutoStartBoard(config, logger, emitter, eventBase) {
1117
+ if (!config.hu_board?.enabled || !config.hu_board?.auto_start) return;
1118
+
1119
+ try {
1120
+ const { startBoard } = await import("./commands/board.js");
1121
+ const boardPort = config.hu_board.port || 4000;
1122
+ const boardResult = await startBoard(boardPort);
1123
+ const status = boardResult.alreadyRunning ? "already running" : "started";
1124
+ logger.info(`HU Board ${status} at ${boardResult.url}`);
1125
+ emitProgress(emitter, makeEvent("board:started", eventBase, {
1126
+ message: `HU Board running at ${boardResult.url}`,
1127
+ detail: { pid: boardResult.pid, port: boardPort }
1128
+ }));
1129
+ } catch (err) {
1130
+ logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
1131
+ }
1132
+ }
1133
+
1098
1134
  async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
1099
1135
  const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
1100
1136
  ctx.askQuestion = askQuestion;
@@ -1127,24 +1163,7 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1127
1163
  }
1128
1164
 
1129
1165
  // --- HU Board auto-start ---
1130
- if (config.hu_board?.enabled && config.hu_board?.auto_start) {
1131
- try {
1132
- const { startBoard } = await import("./commands/board.js");
1133
- const boardPort = config.hu_board.port || 4000;
1134
- const boardResult = await startBoard(boardPort);
1135
- if (boardResult.alreadyRunning) {
1136
- logger.info(`HU Board already running at ${boardResult.url}`);
1137
- } else {
1138
- logger.info(`HU Board started at ${boardResult.url}`);
1139
- }
1140
- emitProgress(emitter, makeEvent("board:started", ctx.eventBase, {
1141
- message: `HU Board running at ${boardResult.url}`,
1142
- detail: { pid: boardResult.pid, port: boardPort }
1143
- }));
1144
- } catch (err) {
1145
- logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
1146
- }
1147
- }
1166
+ await tryAutoStartBoard(config, logger, emitter, ctx.eventBase);
1148
1167
 
1149
1168
  // --- Product Context ---
1150
1169
  const ctxProjectDir = config.projectDir || process.cwd();
@@ -219,9 +219,14 @@ function printSessionGit(git) {
219
219
  }
220
220
  }
221
221
 
222
+ function isBudgetUnavailable(budget) {
223
+ return budget.usage_available === false ||
224
+ (budget.total_tokens === 0 && budget.total_cost_usd === 0 && Object.keys(budget.breakdown_by_role || {}).length > 0);
225
+ }
226
+
222
227
  function printSessionBudget(budget) {
223
228
  if (!budget) return;
224
- if (budget.usage_available === false || (budget.total_tokens === 0 && budget.total_cost_usd === 0 && Object.keys(budget.breakdown_by_role || {}).length > 0)) {
229
+ if (isBudgetUnavailable(budget)) {
225
230
  console.log(` ${ANSI.dim}\ud83d\udcb0 Budget: N/A (provider does not report usage)${ANSI.reset}`);
226
231
  return;
227
232
  }
@@ -381,12 +386,13 @@ const EVENT_HANDLERS = {
381
386
  },
382
387
 
383
388
  "budget:update": (event, icon) => {
384
- const total = Number(event.detail?.total_cost_usd || 0);
385
- const totalTokens = Number(event.detail?.total_tokens || 0);
386
- const max = Number(event.detail?.max_budget_usd);
387
- const pct = Number(event.detail?.pct_used ?? 0);
388
- const warn = Number(event.detail?.warn_threshold_pct ?? 80);
389
- const hasEntries = (event.detail?.entries?.length ?? 0) > 0 || Object.keys(event.detail?.breakdown_by_role || {}).length > 0;
389
+ const d = event.detail || {};
390
+ const total = Number(d.total_cost_usd || 0);
391
+ const totalTokens = Number(d.total_tokens || 0);
392
+ const max = Number(d.max_budget_usd);
393
+ const pct = Number(d.pct_used ?? 0);
394
+ const warn = Number(d.warn_threshold_pct ?? 80);
395
+ const hasEntries = (d.entries?.length ?? 0) > 0 || Object.keys(d.breakdown_by_role || {}).length > 0;
390
396
  if (hasEntries && totalTokens === 0 && total === 0) {
391
397
  console.log(` \u251c\u2500 ${icon} Budget: ${ANSI.dim}N/A (provider does not report usage)${ANSI.reset}`);
392
398
  return;