karajan-code 1.34.2 → 1.34.4
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/README.md +1 -1
- package/docs/README.es.md +1 -1
- package/package.json +1 -1
- package/src/commands/doctor.js +10 -27
- package/src/commands/init.js +2 -1
- package/src/commands/report.js +33 -37
- package/src/config.js +27 -15
- package/src/mcp/server-handlers.js +32 -17
- package/src/orchestrator.js +106 -87
- package/src/utils/agent-detect.js +6 -5
- package/src/utils/display.js +13 -7
- package/src/utils/os-detect.js +45 -0
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ hu-reviewer? → triage → discover? → architect? → planner? → coder →
|
|
|
116
116
|
| **Claude** | `claude` | `npm install -g @anthropic-ai/claude-code` |
|
|
117
117
|
| **Codex** | `codex` | `npm install -g @openai/codex` |
|
|
118
118
|
| **Gemini** | `gemini` | See [Gemini CLI docs](https://github.com/google-gemini/gemini-cli) |
|
|
119
|
-
| **Aider** | `aider` | `
|
|
119
|
+
| **Aider** | `aider` | `pipx install aider-chat` (or `pip3 install aider-chat`) |
|
|
120
120
|
| **OpenCode** | `opencode` | See [OpenCode docs](https://github.com/nicepkg/opencode) |
|
|
121
121
|
|
|
122
122
|
Mix and match. Use Claude as coder and Codex as reviewer. Karajan auto-detects installed agents during `kj init`.
|
package/docs/README.es.md
CHANGED
|
@@ -124,7 +124,7 @@ Guias completas: [`docs/multi-instance.md`](multi-instance.md) | [`docs/install-
|
|
|
124
124
|
| **Claude** | `claude` | `npm install -g @anthropic-ai/claude-code` |
|
|
125
125
|
| **Codex** | `codex` | `npm install -g @openai/codex` |
|
|
126
126
|
| **Gemini** | `gemini` | Ver [Gemini CLI docs](https://github.com/google-gemini/gemini-cli) |
|
|
127
|
-
| **Aider** | `aider` | `
|
|
127
|
+
| **Aider** | `aider` | `pipx install aider-chat` (o `pip3 install aider-chat`) |
|
|
128
128
|
|
|
129
129
|
`kj init` auto-detecta los agentes instalados. Si solo hay uno disponible, se asigna a todos los roles automaticamente.
|
|
130
130
|
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -8,6 +8,7 @@ import { isSonarReachable } from "../sonar/manager.js";
|
|
|
8
8
|
import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
9
9
|
import { ensureGitRepo } from "../utils/git.js";
|
|
10
10
|
import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
|
|
11
|
+
import { getInstallCommand } from "../utils/os-detect.js";
|
|
11
12
|
|
|
12
13
|
function getPackageVersion() {
|
|
13
14
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
@@ -162,23 +163,23 @@ async function checkBecariaSecrets() {
|
|
|
162
163
|
const { detectRepo } = await import("../becaria/repo.js");
|
|
163
164
|
const repo = await detectRepo();
|
|
164
165
|
if (!repo) return null;
|
|
166
|
+
|
|
165
167
|
const secretsRes = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
|
|
166
168
|
if (secretsRes.exitCode !== 0) return null;
|
|
169
|
+
|
|
167
170
|
const names = new Set(secretsRes.stdout.split("\n").map((s) => s.trim()));
|
|
168
171
|
const hasAppId = names.has("BECARIA_APP_ID");
|
|
169
172
|
const hasKey = names.has("BECARIA_APP_PRIVATE_KEY");
|
|
170
173
|
const secretsOk = hasAppId && hasKey;
|
|
174
|
+
const missing = [!hasAppId && "BECARIA_APP_ID", !hasKey && "BECARIA_APP_PRIVATE_KEY"].filter(Boolean).join(" ");
|
|
171
175
|
return {
|
|
172
176
|
name: "becaria:secrets",
|
|
173
177
|
label: "BecarIA: GitHub secrets",
|
|
174
178
|
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(" ")}`,
|
|
179
|
+
detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${missing}`,
|
|
178
180
|
fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
|
|
179
181
|
};
|
|
180
182
|
} catch {
|
|
181
|
-
// Skip secrets check if we can't access the API
|
|
182
183
|
return null;
|
|
183
184
|
}
|
|
184
185
|
}
|
|
@@ -205,33 +206,15 @@ async function checkBecariaInfra(config) {
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
async function checkRtk() {
|
|
209
|
+
const NOT_FOUND_DETAIL = `Not found — install for 60-90% token savings: ${getInstallCommand("rtk")}`;
|
|
210
|
+
let detail = NOT_FOUND_DETAIL;
|
|
208
211
|
try {
|
|
209
212
|
const res = await runCommand("rtk", ["--version"]);
|
|
210
213
|
if (res.exitCode === 0) {
|
|
211
|
-
|
|
212
|
-
name: "rtk",
|
|
213
|
-
label: "RTK (Rust Token Killer)",
|
|
214
|
-
ok: true,
|
|
215
|
-
detail: `${res.stdout.trim()} — token savings active`,
|
|
216
|
-
fix: null
|
|
217
|
-
};
|
|
214
|
+
detail = `${res.stdout.trim()} — token savings active`;
|
|
218
215
|
}
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
}
|
|
216
|
+
} catch { /* not installed */ }
|
|
217
|
+
return { name: "rtk", label: "RTK (Rust Token Killer)", ok: true, detail, fix: null };
|
|
235
218
|
}
|
|
236
219
|
|
|
237
220
|
async function checkRuleFiles(config) {
|
package/src/commands/init.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getKarajanHome } from "../utils/paths.js";
|
|
|
8
8
|
import { detectAvailableAgents } from "../utils/agent-detect.js";
|
|
9
9
|
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
10
10
|
import { runCommand } from "../utils/process.js";
|
|
11
|
+
import { getInstallCommand } from "../utils/os-detect.js";
|
|
11
12
|
|
|
12
13
|
async function runWizard(config, logger) {
|
|
13
14
|
const agents = await detectAvailableAgents();
|
|
@@ -292,7 +293,7 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
292
293
|
if (!hasRtk) {
|
|
293
294
|
logger.info("");
|
|
294
295
|
logger.info("RTK (Rust Token Killer) can reduce token usage by 60-90%.");
|
|
295
|
-
logger.info(
|
|
296
|
+
logger.info(` Install: ${getInstallCommand("rtk")}`);
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
await setupSonarQube(config, logger);
|
package/src/commands/report.js
CHANGED
|
@@ -155,50 +155,49 @@ async function buildReport(dir, sessionId) {
|
|
|
155
155
|
return report;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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(
|
|
192
|
+
console.log(report.plan_executed.length > 0 ? report.plan_executed.join(" -> ") : "N/A");
|
|
192
193
|
console.log("Iterations (Coder/Reviewer):");
|
|
193
|
-
console.log(
|
|
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(
|
|
198
|
+
console.log(formatBudgetText(report.budget_consumed));
|
|
200
199
|
console.log("Commits Generated:");
|
|
201
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/orchestrator.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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:
|
|
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
|
-
|
|
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();
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { runCommand } from "./process.js";
|
|
2
2
|
import { resolveBin } from "../agents/resolve-bin.js";
|
|
3
|
+
import { getInstallCommand } from "./os-detect.js";
|
|
3
4
|
|
|
4
5
|
const KNOWN_AGENTS = [
|
|
5
|
-
{ name: "claude", install: "
|
|
6
|
-
{ name: "codex", install: "
|
|
7
|
-
{ name: "gemini", install: "
|
|
8
|
-
{ name: "aider", install: "
|
|
9
|
-
{ name: "opencode", install: "
|
|
6
|
+
{ name: "claude", install: getInstallCommand("claude") },
|
|
7
|
+
{ name: "codex", install: getInstallCommand("codex") },
|
|
8
|
+
{ name: "gemini", install: getInstallCommand("gemini") },
|
|
9
|
+
{ name: "aider", install: getInstallCommand("aider") },
|
|
10
|
+
{ name: "opencode", install: getInstallCommand("opencode") }
|
|
10
11
|
];
|
|
11
12
|
|
|
12
13
|
export async function checkBinary(name, versionArg = "--version") {
|
package/src/utils/display.js
CHANGED
|
@@ -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 (
|
|
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
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const
|
|
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;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect platform and return OS-appropriate install commands.
|
|
5
|
+
*/
|
|
6
|
+
export function getPlatform() {
|
|
7
|
+
const platform = os.platform();
|
|
8
|
+
return platform === "darwin" ? "macos" : "linux";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const INSTALL_COMMANDS = {
|
|
12
|
+
rtk: {
|
|
13
|
+
macos: "brew install rtk && rtk init --global",
|
|
14
|
+
linux: "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh && rtk init --global"
|
|
15
|
+
},
|
|
16
|
+
claude: {
|
|
17
|
+
macos: "npm install -g @anthropic-ai/claude-code",
|
|
18
|
+
linux: "npm install -g @anthropic-ai/claude-code"
|
|
19
|
+
},
|
|
20
|
+
codex: {
|
|
21
|
+
macos: "npm install -g @openai/codex",
|
|
22
|
+
linux: "npm install -g @openai/codex"
|
|
23
|
+
},
|
|
24
|
+
gemini: {
|
|
25
|
+
macos: "npm install -g @google/gemini-cli",
|
|
26
|
+
linux: "npm install -g @google/gemini-cli"
|
|
27
|
+
},
|
|
28
|
+
aider: {
|
|
29
|
+
macos: "pipx install aider-chat",
|
|
30
|
+
linux: "pipx install aider-chat || pip3 install aider-chat"
|
|
31
|
+
},
|
|
32
|
+
opencode: {
|
|
33
|
+
macos: "curl -fsSL https://opencode.ai/install | bash",
|
|
34
|
+
linux: "curl -fsSL https://opencode.ai/install | bash"
|
|
35
|
+
},
|
|
36
|
+
docker: {
|
|
37
|
+
macos: "brew install --cask docker",
|
|
38
|
+
linux: "sudo apt install docker.io docker-compose-v2 (or see https://docs.docker.com/engine/install/)"
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function getInstallCommand(tool) {
|
|
43
|
+
const platform = getPlatform();
|
|
44
|
+
return INSTALL_COMMANDS[tool]?.[platform] || INSTALL_COMMANDS[tool]?.linux || `Install ${tool} manually`;
|
|
45
|
+
}
|