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 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 4 rules, escalates on anomalies | **On** |
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 1180+ tests with Vitest
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 4 reglas, escala ante anomalias | **On** |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
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",
@@ -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,3 @@
1
+ export { dispatchComment, dispatchReview, VALID_AGENTS } from "./dispatch.js";
2
+ export { detectRepo, detectPrNumber } from "./repo.js";
3
+ export { getPrDiff } from "./pr-diff.js";
@@ -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")
@@ -129,7 +129,62 @@ export async function runChecks({ config }) {
129
129
  });
130
130
  }
131
131
 
132
- // 8. Review rules / Coder rules
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));
@@ -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
  }
@@ -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
- const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef });
14
- const diff = await generateDiff({ baseRef: resolvedBase });
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;
@@ -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
- const diff = await generateDiff({ baseRef: session.session_start_sha });
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,