karajan-code 1.11.1 → 1.13.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.
- package/README.md +3 -2
- package/docs/README.es.md +2 -1
- package/package.json +1 -1
- package/src/becaria/dispatch.js +99 -0
- package/src/becaria/index.js +3 -0
- package/src/becaria/pr-diff.js +26 -0
- package/src/becaria/repo.js +45 -0
- package/src/cli.js +2 -0
- package/src/commands/doctor.js +56 -1
- package/src/commands/init.js +33 -0
- package/src/commands/review.js +54 -2
- package/src/config.js +11 -0
- package/src/git/automation.js +65 -2
- package/src/mcp/tools.js +1 -0
- package/src/orchestrator/iteration-stages.js +85 -3
- package/src/orchestrator/solomon-rules.js +25 -2
- package/src/orchestrator.js +194 -6
- package/src/prompts/coder.js +5 -1
- package/src/prompts/reviewer.js +2 -0
- package/src/review/scope-filter.js +153 -0
- package/src/roles/coder-role.js +3 -2
- package/templates/roles/coder.md +11 -7
- package/templates/roles/planner.md +2 -0
- package/templates/roles/refactorer.md +1 -1
- package/templates/roles/reviewer.md +11 -4
- package/templates/workflows/automerge.yml +30 -0
- package/templates/workflows/becaria-gateway.yml +58 -0
- package/templates/workflows/houston-override.yml +46 -0
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ Instead of running one AI agent and manually reviewing its output, `kj` chains a
|
|
|
47
47
|
- **Rate-limit standby** — when agents hit rate limits, Karajan parses cooldown times, waits with exponential backoff, and auto-resumes instead of failing
|
|
48
48
|
- **Preflight handshake** — `kj_preflight` requires human confirmation of agent assignments before execution, preventing AI from silently overriding your config
|
|
49
49
|
- **3-tier config** — session > project > global config layering with `kj_agents` scoping
|
|
50
|
+
- **Intelligent reviewer mediation** — scope filter auto-defers out-of-scope reviewer issues (files not in the diff) as tracked tech debt instead of stalling; Solomon mediates stalled reviews; deferred context injected into coder prompt
|
|
50
51
|
- **Planning Game integration** — optionally pair with [Planning Game](https://github.com/AgenteIA-Geniova/planning-game) for agile project management (tasks, sprints, estimation) — like Jira, but open-source and XP-native
|
|
51
52
|
|
|
52
53
|
> **Best with MCP** — Karajan Code is designed to be used as an MCP server inside your AI agent (Claude, Codex, etc.). The agent sends tasks to `kj_run`, gets real-time progress notifications, and receives structured results — no copy-pasting needed.
|
|
@@ -74,7 +75,7 @@ triage? ─> researcher? ─> planner? ─> coder ─> refactorer? ─> sonar?
|
|
|
74
75
|
| **reviewer** | Code review with configurable strictness profiles | **Always on** |
|
|
75
76
|
| **tester** | Test quality gate and coverage verification | **On** |
|
|
76
77
|
| **security** | OWASP security audit | **On** |
|
|
77
|
-
| **solomon** | Session supervisor — monitors iteration health with
|
|
78
|
+
| **solomon** | Session supervisor — monitors iteration health with 5 rules (incl. reviewer overreach), mediates stalled reviews, escalates on anomalies | **On** |
|
|
78
79
|
| **commiter** | Git commit, push, and PR automation after approval | Off |
|
|
79
80
|
|
|
80
81
|
Roles marked with `?` are optional and can be enabled per-run or via config.
|
|
@@ -477,7 +478,7 @@ Use `kj roles show <role>` to inspect any template. Create a project override to
|
|
|
477
478
|
git clone https://github.com/manufosela/karajan-code.git
|
|
478
479
|
cd karajan-code
|
|
479
480
|
npm install
|
|
480
|
-
npm test # Run
|
|
481
|
+
npm test # Run 1190+ tests with Vitest
|
|
481
482
|
npm run test:watch # Watch mode
|
|
482
483
|
npm run validate # Lint + test
|
|
483
484
|
```
|
package/docs/README.es.md
CHANGED
|
@@ -46,6 +46,7 @@ En lugar de ejecutar un agente de IA y revisar manualmente su output, `kj` encad
|
|
|
46
46
|
- **Standby por rate-limit** — cuando un agente alcanza limites de uso, Karajan parsea el tiempo de espera, espera con backoff exponencial y reanuda automaticamente en vez de fallar
|
|
47
47
|
- **Preflight handshake** — `kj_preflight` requiere confirmacion humana de la configuracion de agentes antes de ejecutar, previniendo que la IA cambie asignaciones silenciosamente
|
|
48
48
|
- **Config de 3 niveles** — sesion > proyecto > global con scoping de `kj_agents`
|
|
49
|
+
- **Mediacion inteligente del reviewer** — el scope filter difiere automaticamente issues del reviewer fuera de scope (ficheros no presentes en el diff) como deuda tecnica rastreada en vez de bloquear; Solomon media reviews estancados; el contexto diferido se inyecta en el prompt del coder
|
|
49
50
|
- **Integracion con Planning Game** — combina opcionalmente con [Planning Game](https://github.com/AgenteIA-Geniova/planning-game) para gestion agil de proyectos (tareas, sprints, estimacion) — como Jira, pero open-source y nativo XP
|
|
50
51
|
|
|
51
52
|
> **Mejor con MCP** — Karajan Code esta disenado para usarse como servidor MCP dentro de tu agente de IA (Claude, Codex, etc.). El agente envia tareas a `kj_run`, recibe notificaciones de progreso en tiempo real, y obtiene resultados estructurados — sin copiar y pegar.
|
|
@@ -73,7 +74,7 @@ triage? ─> researcher? ─> planner? ─> coder ─> refactorer? ─> sonar?
|
|
|
73
74
|
| **reviewer** | Revision de codigo con perfiles de exigencia configurables | **Siempre activo** |
|
|
74
75
|
| **tester** | Quality gate de tests y verificacion de cobertura | **On** |
|
|
75
76
|
| **security** | Auditoria de seguridad OWASP | **On** |
|
|
76
|
-
| **solomon** | Supervisor de sesion — monitoriza salud de iteraciones con
|
|
77
|
+
| **solomon** | Supervisor de sesion — monitoriza salud de iteraciones con 5 reglas (incl. reviewer overreach), media reviews estancados, escala ante anomalias | **On** |
|
|
77
78
|
| **commiter** | Automatizacion de git commit, push y PR tras aprobacion | Off |
|
|
78
79
|
|
|
79
80
|
Los roles marcados con `?` son opcionales y se pueden activar por ejecucion o via config.
|
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BecarIA dispatch client — sends repository_dispatch events via gh CLI
|
|
3
|
+
* so the BecarIA Gateway can publish comments and reviews on PRs.
|
|
4
|
+
*
|
|
5
|
+
* Event types are configurable via becaria config:
|
|
6
|
+
* - comment_event (default: "becaria-comment")
|
|
7
|
+
* - review_event (default: "becaria-review")
|
|
8
|
+
*
|
|
9
|
+
* Only active when becaria.enabled: true.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { runCommand } from "../utils/process.js";
|
|
13
|
+
|
|
14
|
+
export const VALID_AGENTS = [
|
|
15
|
+
"Coder",
|
|
16
|
+
"Reviewer",
|
|
17
|
+
"Solomon",
|
|
18
|
+
"Sonar",
|
|
19
|
+
"Tester",
|
|
20
|
+
"Security",
|
|
21
|
+
"Planner"
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const VALID_REVIEW_EVENTS = ["APPROVE", "REQUEST_CHANGES"];
|
|
25
|
+
|
|
26
|
+
function validateCommon({ repo, prNumber }) {
|
|
27
|
+
if (!repo) throw new Error("repo is required (e.g. 'owner/repo')");
|
|
28
|
+
if (!prNumber) throw new Error("prNumber is required (positive integer)");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validateAgent(agent) {
|
|
32
|
+
if (!VALID_AGENTS.includes(agent)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Invalid agent "${agent}". Must be one of: ${VALID_AGENTS.join(", ")}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function sendDispatch(repo, payload) {
|
|
40
|
+
const res = await runCommand(
|
|
41
|
+
"gh",
|
|
42
|
+
["api", `repos/${repo}/dispatches`, "--method", "POST", "--input", "-"],
|
|
43
|
+
{ input: JSON.stringify(payload) }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (res.exitCode === 127) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"gh CLI not found. Install GitHub CLI: https://cli.github.com/"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (res.exitCode !== 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Dispatch failed (exit ${res.exitCode}): ${res.stderr || res.stdout}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Send a comment event so the gateway posts a PR comment.
|
|
61
|
+
* @param {object} opts
|
|
62
|
+
* @param {object} [opts.becariaConfig] - becaria config section (optional)
|
|
63
|
+
*/
|
|
64
|
+
export async function dispatchComment({ repo, prNumber, agent, body, becariaConfig }) {
|
|
65
|
+
validateCommon({ repo, prNumber });
|
|
66
|
+
validateAgent(agent);
|
|
67
|
+
if (!body) throw new Error("body is required (comment text)");
|
|
68
|
+
|
|
69
|
+
const prefix = becariaConfig?.comment_prefix !== false ? `[${agent}] ` : "";
|
|
70
|
+
const eventType = becariaConfig?.comment_event || "becaria-comment";
|
|
71
|
+
|
|
72
|
+
await sendDispatch(repo, {
|
|
73
|
+
event_type: eventType,
|
|
74
|
+
client_payload: { pr_number: prNumber, agent, body: `${prefix}${body}` }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Send a review event so the gateway submits a formal PR review.
|
|
80
|
+
* @param {object} opts
|
|
81
|
+
* @param {object} [opts.becariaConfig] - becaria config section (optional)
|
|
82
|
+
*/
|
|
83
|
+
export async function dispatchReview({ repo, prNumber, event, body, agent, becariaConfig }) {
|
|
84
|
+
validateCommon({ repo, prNumber });
|
|
85
|
+
validateAgent(agent);
|
|
86
|
+
if (!VALID_REVIEW_EVENTS.includes(event)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`event must be one of: ${VALID_REVIEW_EVENTS.join(", ")} (got "${event}")`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (!body) throw new Error("body is required (review text)");
|
|
92
|
+
|
|
93
|
+
const eventType = becariaConfig?.review_event || "becaria-review";
|
|
94
|
+
|
|
95
|
+
await sendDispatch(repo, {
|
|
96
|
+
event_type: eventType,
|
|
97
|
+
client_payload: { pr_number: prNumber, event, body, agent }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read PR diff via gh CLI for BecarIA Gateway flow.
|
|
3
|
+
* The reviewer reads the PR diff instead of local git diff.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { runCommand } from "../utils/process.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the diff of a PR via `gh pr diff <number>`.
|
|
10
|
+
* Returns the diff string or throws on failure.
|
|
11
|
+
*/
|
|
12
|
+
export async function getPrDiff(prNumber) {
|
|
13
|
+
if (!prNumber) throw new Error("prNumber is required");
|
|
14
|
+
|
|
15
|
+
const res = await runCommand("gh", [
|
|
16
|
+
"pr",
|
|
17
|
+
"diff",
|
|
18
|
+
String(prNumber)
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
if (res.exitCode !== 0) {
|
|
22
|
+
throw new Error(`gh pr diff ${prNumber} failed: ${res.stderr || res.stdout}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return res.stdout;
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect GitHub repo and PR number from local git context.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { runCommand } from "../utils/process.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect owner/repo from the git remote URL (origin).
|
|
9
|
+
* Supports HTTPS, SSH, and custom SSH aliases (e.g. github.com-user).
|
|
10
|
+
* Returns null if not a GitHub repo or not a git repo.
|
|
11
|
+
*/
|
|
12
|
+
export async function detectRepo() {
|
|
13
|
+
const res = await runCommand("git", [
|
|
14
|
+
"remote",
|
|
15
|
+
"get-url",
|
|
16
|
+
"origin"
|
|
17
|
+
]);
|
|
18
|
+
if (res.exitCode !== 0) return null;
|
|
19
|
+
|
|
20
|
+
const url = res.stdout.trim();
|
|
21
|
+
// SSH: git@github.com:owner/repo.git or git@github.com-alias:owner/repo.git
|
|
22
|
+
const sshMatch = url.match(/github\.com[^:]*:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
23
|
+
if (sshMatch) return sshMatch[1];
|
|
24
|
+
|
|
25
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
26
|
+
const httpsMatch = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
27
|
+
if (httpsMatch) return httpsMatch[1];
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect the PR number for a given branch using gh CLI.
|
|
34
|
+
* Returns null if no PR exists for the branch.
|
|
35
|
+
*/
|
|
36
|
+
export async function detectPrNumber(branch) {
|
|
37
|
+
const args = ["pr", "view"];
|
|
38
|
+
if (branch) args.push(branch);
|
|
39
|
+
args.push("--json", "number", "--jq", ".number");
|
|
40
|
+
const res = await runCommand("gh", args);
|
|
41
|
+
if (res.exitCode !== 0) return null;
|
|
42
|
+
|
|
43
|
+
const num = parseInt(res.stdout.trim(), 10);
|
|
44
|
+
return Number.isFinite(num) ? num : null;
|
|
45
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ program
|
|
|
37
37
|
.command("init")
|
|
38
38
|
.description("Initialize config, review rules and SonarQube")
|
|
39
39
|
.option("--no-interactive", "Skip wizard, use defaults (for CI/scripts)")
|
|
40
|
+
.option("--scaffold-becaria", "Scaffold BecarIA Gateway workflow files")
|
|
40
41
|
.action(async (flags) => {
|
|
41
42
|
await withConfig("init", flags, async ({ config, logger }) => {
|
|
42
43
|
await initCommand({ logger, flags });
|
|
@@ -84,6 +85,7 @@ program
|
|
|
84
85
|
.option("--auto-commit")
|
|
85
86
|
.option("--auto-push")
|
|
86
87
|
.option("--auto-pr")
|
|
88
|
+
.option("--enable-becaria", "Enable BecarIA Gateway (early PR + dispatch comments/reviews)")
|
|
87
89
|
.option("--branch-prefix <prefix>")
|
|
88
90
|
.option("--methodology <name>")
|
|
89
91
|
.option("--no-auto-rebase")
|
package/src/commands/doctor.js
CHANGED
|
@@ -129,7 +129,62 @@ export async function runChecks({ config }) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
// 8.
|
|
132
|
+
// 8. BecarIA Gateway infrastructure
|
|
133
|
+
if (config.becaria?.enabled) {
|
|
134
|
+
const projectDir = config.projectDir || process.cwd();
|
|
135
|
+
|
|
136
|
+
// Workflow files
|
|
137
|
+
const workflowDir = path.join(projectDir, ".github", "workflows");
|
|
138
|
+
const requiredWorkflows = ["becaria-gateway.yml", "automerge.yml", "houston-override.yml"];
|
|
139
|
+
for (const wf of requiredWorkflows) {
|
|
140
|
+
const wfPath = path.join(workflowDir, wf);
|
|
141
|
+
const wfExists = await exists(wfPath);
|
|
142
|
+
checks.push({
|
|
143
|
+
name: `becaria:workflow:${wf}`,
|
|
144
|
+
label: `BecarIA workflow: ${wf}`,
|
|
145
|
+
ok: wfExists,
|
|
146
|
+
detail: wfExists ? "Found" : "Not found",
|
|
147
|
+
fix: wfExists ? null : `Run 'kj init --scaffold-becaria' or copy from karajan-code/templates/workflows/${wf}`
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// gh CLI
|
|
152
|
+
const ghCheck = await checkBinary("gh");
|
|
153
|
+
checks.push({
|
|
154
|
+
name: "becaria:gh",
|
|
155
|
+
label: "BecarIA: gh CLI",
|
|
156
|
+
ok: ghCheck.ok,
|
|
157
|
+
detail: ghCheck.ok ? ghCheck.version : "Not found",
|
|
158
|
+
fix: ghCheck.ok ? null : "Install GitHub CLI: https://cli.github.com/"
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Secrets check via gh api (best effort — only works if user has admin access)
|
|
162
|
+
let secretsOk = false;
|
|
163
|
+
try {
|
|
164
|
+
const { detectRepo } = await import("../becaria/repo.js");
|
|
165
|
+
const repo = await detectRepo();
|
|
166
|
+
if (repo) {
|
|
167
|
+
const secretsRes = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
|
|
168
|
+
if (secretsRes.exitCode === 0) {
|
|
169
|
+
const names = secretsRes.stdout.split("\n").map((s) => s.trim());
|
|
170
|
+
const hasAppId = names.includes("BECARIA_APP_ID");
|
|
171
|
+
const hasKey = names.includes("BECARIA_APP_PRIVATE_KEY");
|
|
172
|
+
secretsOk = hasAppId && hasKey;
|
|
173
|
+
checks.push({
|
|
174
|
+
name: "becaria:secrets",
|
|
175
|
+
label: "BecarIA: GitHub secrets",
|
|
176
|
+
ok: secretsOk,
|
|
177
|
+
detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${!hasAppId ? "BECARIA_APP_ID " : ""}${!hasKey ? "BECARIA_APP_PRIVATE_KEY" : ""}`.trim(),
|
|
178
|
+
fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Skip secrets check if we can't access the API
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 9. Review rules / Coder rules
|
|
133
188
|
const projectDir = config.projectDir || process.cwd();
|
|
134
189
|
const reviewRules = await loadFirstExisting(resolveRoleMdPath("reviewer", projectDir));
|
|
135
190
|
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
package/src/commands/init.js
CHANGED
|
@@ -177,4 +177,37 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
177
177
|
} else {
|
|
178
178
|
logger.info("SonarQube disabled — skipping container setup.");
|
|
179
179
|
}
|
|
180
|
+
|
|
181
|
+
// --- BecarIA Gateway scaffolding ---
|
|
182
|
+
if (config.becaria?.enabled || flags.scaffoldBecaria) {
|
|
183
|
+
const projectDir = process.cwd();
|
|
184
|
+
const workflowDir = path.join(projectDir, ".github", "workflows");
|
|
185
|
+
await ensureDir(workflowDir);
|
|
186
|
+
|
|
187
|
+
const templatesDir = path.resolve(import.meta.dirname, "../../templates/workflows");
|
|
188
|
+
const workflows = ["becaria-gateway.yml", "automerge.yml", "houston-override.yml"];
|
|
189
|
+
|
|
190
|
+
for (const wf of workflows) {
|
|
191
|
+
const destPath = path.join(workflowDir, wf);
|
|
192
|
+
if (!(await exists(destPath))) {
|
|
193
|
+
const srcPath = path.join(templatesDir, wf);
|
|
194
|
+
try {
|
|
195
|
+
const content = await fs.readFile(srcPath, "utf8");
|
|
196
|
+
await fs.writeFile(destPath, content, "utf8");
|
|
197
|
+
logger.info(`Created ${path.relative(projectDir, destPath)}`);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
logger.warn(`Could not scaffold ${wf}: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
logger.info(`${wf} already exists — skipping`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
logger.info("");
|
|
207
|
+
logger.info("BecarIA Gateway scaffolded. Next steps:");
|
|
208
|
+
logger.info(" 1. Create a GitHub App named 'becaria-reviewer' with pull_request write permissions");
|
|
209
|
+
logger.info(" 2. Install the App on your repository");
|
|
210
|
+
logger.info(" 3. Add secrets: BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY");
|
|
211
|
+
logger.info(" 4. Push the workflow files and enable 'kj run --enable-becaria'");
|
|
212
|
+
}
|
|
180
213
|
}
|
package/src/commands/review.js
CHANGED
|
@@ -10,8 +10,26 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
|
|
|
10
10
|
await assertAgentsAvailable([reviewerRole.provider, config.reviewer_options?.fallback_reviewer]);
|
|
11
11
|
logger.info(`Reviewer (${reviewerRole.provider}) starting...`);
|
|
12
12
|
const reviewer = createAgent(reviewerRole.provider, config, logger);
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
let diff;
|
|
15
|
+
if (config.becaria?.enabled) {
|
|
16
|
+
// BecarIA mode: read diff from open PR
|
|
17
|
+
const { detectRepo, detectPrNumber } = await import("../becaria/repo.js");
|
|
18
|
+
const { getPrDiff } = await import("../becaria/pr-diff.js");
|
|
19
|
+
const repo = await detectRepo();
|
|
20
|
+
const prNumber = await detectPrNumber();
|
|
21
|
+
if (!prNumber) {
|
|
22
|
+
throw new Error("BecarIA enabled but no open PR found for current branch. Create a PR first or disable BecarIA.");
|
|
23
|
+
}
|
|
24
|
+
logger.info(`BecarIA: reading PR diff #${prNumber}`);
|
|
25
|
+
diff = await getPrDiff(prNumber);
|
|
26
|
+
// Store for dispatch later
|
|
27
|
+
config._becaria_pr = { repo, prNumber };
|
|
28
|
+
} else {
|
|
29
|
+
const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef });
|
|
30
|
+
diff = await generateDiff({ baseRef: resolvedBase });
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
const { rules } = await resolveReviewProfile({ mode: config.review_mode, projectDir: process.cwd() });
|
|
16
34
|
|
|
17
35
|
const prompt = buildReviewerPrompt({ task, diff, reviewRules: rules, mode: config.review_mode });
|
|
@@ -23,4 +41,38 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
|
|
|
23
41
|
}
|
|
24
42
|
console.log(result.output);
|
|
25
43
|
logger.info(`Reviewer completed (exit ${result.exitCode})`);
|
|
44
|
+
|
|
45
|
+
// BecarIA: dispatch review result
|
|
46
|
+
if (config.becaria?.enabled && config._becaria_pr) {
|
|
47
|
+
try {
|
|
48
|
+
const { dispatchReview, dispatchComment } = await import("../becaria/dispatch.js");
|
|
49
|
+
const { repo, prNumber } = config._becaria_pr;
|
|
50
|
+
const bc = config.becaria;
|
|
51
|
+
|
|
52
|
+
// Try to parse structured review from output
|
|
53
|
+
let review;
|
|
54
|
+
try {
|
|
55
|
+
review = JSON.parse(result.output);
|
|
56
|
+
} catch {
|
|
57
|
+
review = { approved: true, summary: result.output };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const event = review.approved ? "APPROVE" : "REQUEST_CHANGES";
|
|
61
|
+
await dispatchReview({
|
|
62
|
+
repo, prNumber, event,
|
|
63
|
+
body: review.summary || result.output.slice(0, 500),
|
|
64
|
+
agent: "Reviewer", becariaConfig: bc
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await dispatchComment({
|
|
68
|
+
repo, prNumber, agent: "Reviewer",
|
|
69
|
+
body: `Standalone review: ${event}\n\n${review.summary || result.output.slice(0, 1000)}`,
|
|
70
|
+
becariaConfig: bc
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
logger.info(`BecarIA: dispatched review for PR #${prNumber}`);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.warn(`BecarIA dispatch failed (non-blocking): ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
26
78
|
}
|
package/src/config.js
CHANGED
|
@@ -99,6 +99,7 @@ const DEFAULTS = {
|
|
|
99
99
|
},
|
|
100
100
|
serena: { enabled: false },
|
|
101
101
|
planning_game: { enabled: false, project_id: null, codeveloper: null },
|
|
102
|
+
becaria: { enabled: false, review_event: "becaria-review", comment_event: "becaria-comment", comment_prefix: true },
|
|
102
103
|
git: { auto_commit: false, auto_push: false, auto_pr: false, auto_rebase: true, branch_prefix: "feat/" },
|
|
103
104
|
output: { report_dir: "./.reviews", log_level: "info" },
|
|
104
105
|
budget: {
|
|
@@ -287,6 +288,16 @@ export function applyRunOverrides(config, flags) {
|
|
|
287
288
|
if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
|
|
288
289
|
out.serena = out.serena || { enabled: false };
|
|
289
290
|
if (flags.enableSerena !== undefined) out.serena.enabled = Boolean(flags.enableSerena);
|
|
291
|
+
out.becaria = out.becaria || { enabled: false };
|
|
292
|
+
if (flags.enableBecaria !== undefined) {
|
|
293
|
+
out.becaria.enabled = Boolean(flags.enableBecaria);
|
|
294
|
+
// BecarIA requires git automation (commit + push + PR)
|
|
295
|
+
if (out.becaria.enabled) {
|
|
296
|
+
out.git.auto_commit = true;
|
|
297
|
+
out.git.auto_push = true;
|
|
298
|
+
out.git.auto_pr = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
290
301
|
out.planning_game = out.planning_game || {};
|
|
291
302
|
if (flags.pgTask) out.planning_game.enabled = true;
|
|
292
303
|
if (flags.pgProject) out.planning_game.project_id = flags.pgProject;
|
package/src/git/automation.js
CHANGED
|
@@ -82,6 +82,67 @@ export function buildPrBody({ task, stageResults }) {
|
|
|
82
82
|
return sections.join("\n");
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Create an early PR after the first coder iteration (BecarIA Gateway flow).
|
|
87
|
+
* Commits, pushes, and creates a PR before the reviewer runs.
|
|
88
|
+
* Returns { prNumber, prUrl, commits } or null if nothing to commit.
|
|
89
|
+
*/
|
|
90
|
+
export async function earlyPrCreation({ gitCtx, task, logger, session, stageResults = null }) {
|
|
91
|
+
if (!gitCtx?.enabled) return null;
|
|
92
|
+
|
|
93
|
+
const commitMsg = commitMessageFromTask(task);
|
|
94
|
+
const commitResult = await commitAll(commitMsg);
|
|
95
|
+
if (!commitResult.committed) {
|
|
96
|
+
logger.info("earlyPrCreation: no changes to commit");
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const commits = commitResult.commit ? [commitResult.commit] : [];
|
|
101
|
+
await addCheckpoint(session, { stage: "becaria-commit", committed: true });
|
|
102
|
+
|
|
103
|
+
await pushBranch(gitCtx.branch);
|
|
104
|
+
await addCheckpoint(session, { stage: "becaria-push", branch: gitCtx.branch });
|
|
105
|
+
logger.info(`Pushed branch for early PR: ${gitCtx.branch}`);
|
|
106
|
+
|
|
107
|
+
const body = buildPrBody({ task, stageResults });
|
|
108
|
+
const prUrl = await createPullRequest({
|
|
109
|
+
baseBranch: gitCtx.baseBranch,
|
|
110
|
+
branch: gitCtx.branch,
|
|
111
|
+
title: commitMessageFromTask(task),
|
|
112
|
+
body
|
|
113
|
+
});
|
|
114
|
+
await addCheckpoint(session, { stage: "becaria-pr", branch: gitCtx.branch, pr: prUrl });
|
|
115
|
+
logger.info(`Early PR created: ${prUrl}`);
|
|
116
|
+
|
|
117
|
+
// Extract PR number from URL (e.g. https://github.com/owner/repo/pull/42)
|
|
118
|
+
const prNumber = parseInt(prUrl.split("/").pop(), 10) || null;
|
|
119
|
+
return { prNumber, prUrl, commits };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Incremental push after each coder iteration (BecarIA Gateway flow).
|
|
124
|
+
* Commits and pushes without creating a new PR.
|
|
125
|
+
*/
|
|
126
|
+
export async function incrementalPush({ gitCtx, task, logger, session }) {
|
|
127
|
+
if (!gitCtx?.enabled) return null;
|
|
128
|
+
|
|
129
|
+
const commitMsg = commitMessageFromTask(task);
|
|
130
|
+
const commitResult = await commitAll(commitMsg);
|
|
131
|
+
if (!commitResult.committed) {
|
|
132
|
+
logger.info("incrementalPush: no changes to commit");
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const commits = commitResult.commit ? [commitResult.commit] : [];
|
|
137
|
+
await addCheckpoint(session, { stage: "becaria-incremental-commit", committed: true });
|
|
138
|
+
|
|
139
|
+
await pushBranch(gitCtx.branch);
|
|
140
|
+
await addCheckpoint(session, { stage: "becaria-incremental-push", branch: gitCtx.branch });
|
|
141
|
+
logger.info(`Incremental push: ${gitCtx.branch}`);
|
|
142
|
+
|
|
143
|
+
return { commits };
|
|
144
|
+
}
|
|
145
|
+
|
|
85
146
|
export async function finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults = null }) {
|
|
86
147
|
if (!gitCtx?.enabled) return { git: "disabled", commits: [] };
|
|
87
148
|
|
|
@@ -114,8 +175,8 @@ export async function finalizeGitAutomation({ config, gitCtx, task, logger, sess
|
|
|
114
175
|
logger.info(`Pushed branch: ${gitCtx.branch}`);
|
|
115
176
|
}
|
|
116
177
|
|
|
117
|
-
let prUrl = null;
|
|
118
|
-
if (config.git.auto_pr) {
|
|
178
|
+
let prUrl = session.becaria_pr_url || null;
|
|
179
|
+
if (config.git.auto_pr && !prUrl) {
|
|
119
180
|
const body = buildPrBody({ task, stageResults });
|
|
120
181
|
prUrl = await createPullRequest({
|
|
121
182
|
baseBranch: gitCtx.baseBranch,
|
|
@@ -125,6 +186,8 @@ export async function finalizeGitAutomation({ config, gitCtx, task, logger, sess
|
|
|
125
186
|
});
|
|
126
187
|
await addCheckpoint(session, { stage: "git-pr", branch: gitCtx.branch, pr: prUrl });
|
|
127
188
|
logger.info("Pull request created");
|
|
189
|
+
} else if (prUrl) {
|
|
190
|
+
logger.info(`PR already exists (BecarIA flow): ${prUrl}`);
|
|
128
191
|
}
|
|
129
192
|
|
|
130
193
|
return { committed, branch: gitCtx.branch, prUrl, pr: prUrl, commits };
|
package/src/mcp/tools.js
CHANGED
|
@@ -71,6 +71,7 @@ export const tools = [
|
|
|
71
71
|
enableSecurity: { type: "boolean" },
|
|
72
72
|
enableTriage: { type: "boolean" },
|
|
73
73
|
enableSerena: { type: "boolean" },
|
|
74
|
+
enableBecaria: { type: "boolean", description: "Enable BecarIA Gateway (early PR + dispatch comments/reviews)" },
|
|
74
75
|
reviewerFallback: { type: "string" },
|
|
75
76
|
reviewerRetries: { type: "number" },
|
|
76
77
|
mode: { type: "string" },
|
|
@@ -6,6 +6,7 @@ import { addCheckpoint, markSessionStatus, saveSession, pauseSession } from "../
|
|
|
6
6
|
import { generateDiff } from "../review/diff-generator.js";
|
|
7
7
|
import { evaluateTddPolicy } from "../review/tdd-policy.js";
|
|
8
8
|
import { validateReviewResult } from "../review/schema.js";
|
|
9
|
+
import { filterReviewScope, buildDeferredContext } from "../review/scope-filter.js";
|
|
9
10
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
10
11
|
import { runReviewerWithFallback } from "./reviewer-fallback.js";
|
|
11
12
|
import { runCoderWithFallback } from "./agent-fallback.js";
|
|
@@ -39,6 +40,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
39
40
|
task: plannedTask,
|
|
40
41
|
reviewerFeedback: session.last_reviewer_feedback,
|
|
41
42
|
sonarSummary: session.last_sonar_summary,
|
|
43
|
+
deferredContext: buildDeferredContext(session.deferred_issues),
|
|
42
44
|
onOutput: coderStall.onOutput
|
|
43
45
|
});
|
|
44
46
|
} finally {
|
|
@@ -390,7 +392,7 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
|
|
|
390
392
|
return { action: "ok", stageResult };
|
|
391
393
|
}
|
|
392
394
|
|
|
393
|
-
export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary }) {
|
|
395
|
+
export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
|
|
394
396
|
logger.setContext({ iteration, stage: "reviewer" });
|
|
395
397
|
emitProgress(
|
|
396
398
|
emitter,
|
|
@@ -400,7 +402,14 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
400
402
|
})
|
|
401
403
|
);
|
|
402
404
|
|
|
403
|
-
|
|
405
|
+
let diff;
|
|
406
|
+
if (session.becaria_pr_number) {
|
|
407
|
+
const { getPrDiff } = await import("../becaria/pr-diff.js");
|
|
408
|
+
diff = await getPrDiff(session.becaria_pr_number);
|
|
409
|
+
logger.info(`Reviewer reading PR diff #${session.becaria_pr_number}`);
|
|
410
|
+
} else {
|
|
411
|
+
diff = await generateDiff({ baseRef: session.session_start_sha });
|
|
412
|
+
}
|
|
404
413
|
const reviewerOnOutput = ({ stream, line }) => {
|
|
405
414
|
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
|
|
406
415
|
message: line,
|
|
@@ -489,6 +498,39 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
489
498
|
confidence: 0
|
|
490
499
|
};
|
|
491
500
|
}
|
|
501
|
+
// --- Scope filter: auto-defer out-of-scope blocking issues ---
|
|
502
|
+
const { review: filteredReview, demoted, deferred, allDemoted } = filterReviewScope(review, diff);
|
|
503
|
+
review = filteredReview;
|
|
504
|
+
|
|
505
|
+
if (demoted.length > 0) {
|
|
506
|
+
logger.info(`Scope filter: deferred ${demoted.length} out-of-scope issue(s)${allDemoted ? " — auto-approved" : ""}`);
|
|
507
|
+
|
|
508
|
+
// Accumulate deferred issues in session for tracking
|
|
509
|
+
if (!session.deferred_issues) session.deferred_issues = [];
|
|
510
|
+
session.deferred_issues.push(...deferred);
|
|
511
|
+
await saveSession(session);
|
|
512
|
+
|
|
513
|
+
emitProgress(
|
|
514
|
+
emitter,
|
|
515
|
+
makeEvent("reviewer:scope_filter", { ...eventBase, stage: "reviewer" }, {
|
|
516
|
+
message: `Scope filter deferred ${demoted.length} out-of-scope issue(s)`,
|
|
517
|
+
detail: {
|
|
518
|
+
demotedCount: demoted.length,
|
|
519
|
+
autoApproved: allDemoted,
|
|
520
|
+
totalDeferred: session.deferred_issues.length,
|
|
521
|
+
deferred: deferred.map(d => ({ file: d.file, id: d.id, description: d.description }))
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
await addCheckpoint(session, {
|
|
526
|
+
stage: "reviewer-scope-filter",
|
|
527
|
+
iteration,
|
|
528
|
+
demoted_count: demoted.length,
|
|
529
|
+
auto_approved: allDemoted,
|
|
530
|
+
total_deferred: session.deferred_issues.length
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
492
534
|
await addCheckpoint(session, {
|
|
493
535
|
stage: "reviewer",
|
|
494
536
|
iteration,
|
|
@@ -518,8 +560,48 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
518
560
|
const repeatState = repeatDetector.isStalled();
|
|
519
561
|
if (repeatState.stalled) {
|
|
520
562
|
const repeatCounts = repeatDetector.getRepeatCounts();
|
|
563
|
+
|
|
564
|
+
// --- Solomon mediation for stalled reviewer ---
|
|
565
|
+
logger.warn(`Reviewer stalled (${repeatCounts.reviewer} repeats). Invoking Solomon mediation.`);
|
|
566
|
+
emitProgress(
|
|
567
|
+
emitter,
|
|
568
|
+
makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
|
|
569
|
+
message: `Reviewer stalled — Solomon mediating`,
|
|
570
|
+
detail: { repeats: repeatCounts.reviewer, reason: repeatState.reason }
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const solomonResult = await invokeSolomon({
|
|
575
|
+
config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration,
|
|
576
|
+
conflict: {
|
|
577
|
+
stage: "reviewer",
|
|
578
|
+
task,
|
|
579
|
+
iterationCount: repeatCounts.reviewer,
|
|
580
|
+
maxIterations: config.session?.fail_fast_repeats ?? 2,
|
|
581
|
+
stalledReason: repeatState.reason,
|
|
582
|
+
blockingIssues: review.blocking_issues,
|
|
583
|
+
history: [{ agent: "reviewer", feedback: review.blocking_issues.map(x => x.description).join("; ") }]
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
if (solomonResult.action === "pause") {
|
|
588
|
+
await markSessionStatus(session, "stalled");
|
|
589
|
+
return { review, stalled: true, stalledResult: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_stalled" } };
|
|
590
|
+
}
|
|
591
|
+
if (solomonResult.action === "continue") {
|
|
592
|
+
repeatDetector.reviewer = { lastHash: null, repeatCount: 0 };
|
|
593
|
+
if (solomonResult.humanGuidance) {
|
|
594
|
+
session.last_reviewer_feedback = `Solomon/user guidance: ${solomonResult.humanGuidance}`;
|
|
595
|
+
await saveSession(session);
|
|
596
|
+
}
|
|
597
|
+
return { review };
|
|
598
|
+
}
|
|
599
|
+
if (solomonResult.action === "subtask") {
|
|
600
|
+
return { review, stalled: true, stalledResult: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" } };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Fallback
|
|
521
604
|
const message = `Manual intervention required: reviewer issues repeated ${repeatCounts.reviewer} times.`;
|
|
522
|
-
logger.warn(message);
|
|
523
605
|
await markSessionStatus(session, "stalled");
|
|
524
606
|
emitProgress(
|
|
525
607
|
emitter,
|