karajan-code 1.12.0 → 1.13.1

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.12.0",
3
+ "version": "1.13.1",
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",
@@ -120,7 +120,7 @@ export class ClaudeAgent extends BaseAgent {
120
120
 
121
121
  // Use stream-json when onOutput is provided to get real-time feedback
122
122
  if (task.onOutput) {
123
- args.push("--output-format", "stream-json");
123
+ args.push("--output-format", "stream-json", "--verbose");
124
124
  const streamFilter = createStreamJsonFilter(task.onOutput);
125
125
  const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
126
126
  onOutput: streamFilter,
@@ -141,7 +141,7 @@ export class ClaudeAgent extends BaseAgent {
141
141
  }
142
142
 
143
143
  async reviewTask(task) {
144
- const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS, "--output-format", "stream-json"];
144
+ const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS, "--output-format", "stream-json", "--verbose"];
145
145
  const model = this.getRoleModel(task.role || "reviewer");
146
146
  if (model) args.push("--model", model);
147
147
  const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
@@ -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" },
@@ -402,7 +402,14 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
402
402
  })
403
403
  );
404
404
 
405
- 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
+ }
406
413
  const reviewerOnOutput = ({ stream, line }) => {
407
414
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
408
415
  message: line,
@@ -17,7 +17,9 @@ import { emitProgress, makeEvent } from "./utils/events.js";
17
17
  import { BudgetTracker, extractUsageMetrics } from "./utils/budget.js";
18
18
  import {
19
19
  prepareGitAutomation,
20
- finalizeGitAutomation
20
+ finalizeGitAutomation,
21
+ earlyPrCreation,
22
+ incrementalPush
21
23
  } from "./git/automation.js";
22
24
  import { resolveRoleMdPath, loadFirstExisting } from "./roles/base-role.js";
23
25
  import { resolveReviewProfile } from "./review/profiles.js";
@@ -288,6 +290,23 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
288
290
  const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, triageDecomposition, trackBudget });
289
291
  plannedTask = plannerResult.plannedTask;
290
292
  stageResults.planner = plannerResult.stageResult;
293
+
294
+ // BecarIA: dispatch planner comment (only on resume where PR already exists)
295
+ if (Boolean(config.becaria?.enabled) && session.becaria_pr_number) {
296
+ try {
297
+ const { dispatchComment } = await import("./becaria/dispatch.js");
298
+ const { detectRepo } = await import("./becaria/repo.js");
299
+ const repo = await detectRepo();
300
+ if (repo) {
301
+ const p = plannerResult.stageResult;
302
+ await dispatchComment({
303
+ repo, prNumber: session.becaria_pr_number, agent: "Planner",
304
+ body: `Plan: ${p?.summary || plannedTask}`,
305
+ becariaConfig: config.becaria
306
+ });
307
+ }
308
+ } catch { /* non-blocking */ }
309
+ }
291
310
  }
292
311
 
293
312
  const gitCtx = await prepareGitAutomation({ config, task, logger, session });
@@ -393,6 +412,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
393
412
 
394
413
  eventBase.iteration = i;
395
414
  const iterStart = Date.now();
415
+ const becariaEnabled = Boolean(config.becaria?.enabled) && gitCtx?.enabled;
396
416
  logger.setContext({ iteration: i, stage: "iteration" });
397
417
 
398
418
  emitProgress(
@@ -483,6 +503,75 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
483
503
  }
484
504
  if (sonarResult.stageResult) {
485
505
  stageResults.sonar = sonarResult.stageResult;
506
+ // BecarIA: dispatch sonar comment
507
+ if (becariaEnabled && session.becaria_pr_number) {
508
+ try {
509
+ const { dispatchComment } = await import("./becaria/dispatch.js");
510
+ const { detectRepo } = await import("./becaria/repo.js");
511
+ const repo = await detectRepo();
512
+ if (repo) {
513
+ const s = sonarResult.stageResult;
514
+ await dispatchComment({
515
+ repo, prNumber: session.becaria_pr_number, agent: "Sonar",
516
+ body: `SonarQube scan: ${s.summary || "completed"}`,
517
+ becariaConfig: config.becaria
518
+ });
519
+ }
520
+ } catch { /* non-blocking */ }
521
+ }
522
+ }
523
+ }
524
+
525
+ // --- BecarIA Gateway: early PR or incremental push ---
526
+ if (becariaEnabled) {
527
+ try {
528
+ const { dispatchComment } = await import("./becaria/dispatch.js");
529
+ const { detectRepo } = await import("./becaria/repo.js");
530
+ const repo = await detectRepo();
531
+
532
+ if (!session.becaria_pr_number) {
533
+ // First iteration: commit + push + create PR
534
+ const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
535
+ if (earlyPr) {
536
+ session.becaria_pr_number = earlyPr.prNumber;
537
+ session.becaria_pr_url = earlyPr.prUrl;
538
+ session.becaria_commits = earlyPr.commits;
539
+ await saveSession(session);
540
+ emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
541
+ message: `Early PR created: #${earlyPr.prNumber}`,
542
+ detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
543
+ }));
544
+
545
+ // Post coder comment on new PR
546
+ if (repo) {
547
+ const commitList = earlyPr.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
548
+ await dispatchComment({
549
+ repo, prNumber: earlyPr.prNumber, agent: "Coder",
550
+ body: `Iteración ${i} completada.\n\nCommits:\n${commitList}`,
551
+ becariaConfig: config.becaria
552
+ });
553
+ }
554
+ }
555
+ } else {
556
+ // Subsequent iterations: incremental push + comment
557
+ const pushResult = await incrementalPush({ gitCtx, task, logger, session });
558
+ if (pushResult) {
559
+ session.becaria_commits = [...(session.becaria_commits || []), ...pushResult.commits];
560
+ await saveSession(session);
561
+
562
+ if (repo) {
563
+ const feedback = session.last_reviewer_feedback || "N/A";
564
+ const commitList = pushResult.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
565
+ await dispatchComment({
566
+ repo, prNumber: session.becaria_pr_number, agent: "Coder",
567
+ body: `Issues corregidos:\n${feedback}\n\nCommits:\n${commitList}`,
568
+ becariaConfig: config.becaria
569
+ });
570
+ }
571
+ }
572
+ }
573
+ } catch (err) {
574
+ logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
486
575
  }
487
576
  }
488
577
 
@@ -576,11 +665,74 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
576
665
  }
577
666
  }
578
667
  }
668
+
669
+ // BecarIA: dispatch solomon comment
670
+ if (becariaEnabled && session.becaria_pr_number) {
671
+ try {
672
+ const { dispatchComment } = await import("./becaria/dispatch.js");
673
+ const { detectRepo } = await import("./becaria/repo.js");
674
+ const repo = await detectRepo();
675
+ if (repo) {
676
+ const alerts = rulesResult.alerts || [];
677
+ const alertMsg = alerts.length > 0
678
+ ? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
679
+ : "No anomalies detected";
680
+ await dispatchComment({
681
+ repo, prNumber: session.becaria_pr_number, agent: "Solomon",
682
+ body: `Supervisor check iteración ${i}: ${alertMsg}`,
683
+ becariaConfig: config.becaria
684
+ });
685
+ }
686
+ } catch { /* non-blocking */ }
687
+ }
579
688
  } catch (err) {
580
689
  logger.warn(`Solomon rules evaluation failed: ${err.message}`);
581
690
  }
582
691
  }
583
692
 
693
+ // --- BecarIA Gateway: dispatch review result ---
694
+ if (becariaEnabled && session.becaria_pr_number) {
695
+ try {
696
+ const { dispatchReview, dispatchComment } = await import("./becaria/dispatch.js");
697
+ const { detectRepo } = await import("./becaria/repo.js");
698
+ const repo = await detectRepo();
699
+ if (repo) {
700
+ const bc = config.becaria;
701
+ // Formal review (APPROVE / REQUEST_CHANGES)
702
+ if (review.approved) {
703
+ await dispatchReview({
704
+ repo, prNumber: session.becaria_pr_number,
705
+ event: "APPROVE", body: review.summary || "Approved", agent: "Reviewer", becariaConfig: bc
706
+ });
707
+ } else {
708
+ const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
709
+ await dispatchReview({
710
+ repo, prNumber: session.becaria_pr_number,
711
+ event: "REQUEST_CHANGES",
712
+ body: blocking || review.summary || "Changes requested",
713
+ agent: "Reviewer", becariaConfig: bc
714
+ });
715
+ }
716
+
717
+ // Detailed comment
718
+ const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
719
+ const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
720
+ const suggestions = review.non_blocking_suggestions?.map((s) => `- ${typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`}`).join("\n") || "";
721
+ let reviewBody = `Review iteración ${i}: ${status}`;
722
+ if (blocking) reviewBody += `\n\n**Blocking:**\n${blocking}`;
723
+ if (suggestions) reviewBody += `\n\n**Suggestions:**\n${suggestions}`;
724
+ await dispatchComment({
725
+ repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
726
+ body: reviewBody, becariaConfig: bc
727
+ });
728
+
729
+ logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
730
+ }
731
+ } catch (err) {
732
+ logger.warn(`BecarIA dispatch failed (non-blocking): ${err.message}`);
733
+ }
734
+ }
735
+
584
736
  if (review.approved) {
585
737
  session.reviewer_retry_count = 0;
586
738
 
@@ -600,6 +752,22 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
600
752
  }
601
753
  if (testerResult.stageResult) {
602
754
  stageResults.tester = testerResult.stageResult;
755
+ // BecarIA: dispatch tester comment
756
+ if (becariaEnabled && session.becaria_pr_number) {
757
+ try {
758
+ const { dispatchComment } = await import("./becaria/dispatch.js");
759
+ const { detectRepo } = await import("./becaria/repo.js");
760
+ const repo = await detectRepo();
761
+ if (repo) {
762
+ const t = testerResult.stageResult;
763
+ await dispatchComment({
764
+ repo, prNumber: session.becaria_pr_number, agent: "Tester",
765
+ body: `Tests: ${t.summary || "completed"}`,
766
+ becariaConfig: config.becaria
767
+ });
768
+ }
769
+ } catch { /* non-blocking */ }
770
+ }
603
771
  }
604
772
  }
605
773
 
@@ -616,6 +784,22 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
616
784
  }
617
785
  if (securityResult.stageResult) {
618
786
  stageResults.security = securityResult.stageResult;
787
+ // BecarIA: dispatch security comment
788
+ if (becariaEnabled && session.becaria_pr_number) {
789
+ try {
790
+ const { dispatchComment } = await import("./becaria/dispatch.js");
791
+ const { detectRepo } = await import("./becaria/repo.js");
792
+ const repo = await detectRepo();
793
+ if (repo) {
794
+ const s = securityResult.stageResult;
795
+ await dispatchComment({
796
+ repo, prNumber: session.becaria_pr_number, agent: "Security",
797
+ body: `Security scan: ${s.summary || "completed"}`,
798
+ becariaConfig: config.becaria
799
+ });
800
+ }
801
+ } catch { /* non-blocking */ }
802
+ }
619
803
  }
620
804
  }
621
805
 
@@ -7,9 +7,20 @@ You are the **Coder** in a multi-role AI pipeline. Your job is to write code and
7
7
  - Follow TDD methodology when `methodology=tdd` is configured.
8
8
  - Write tests BEFORE implementation when using TDD.
9
9
  - Keep changes minimal and focused on the task.
10
+ - "Minimal" means no unnecessary changes — it does NOT mean avoiding new files. If the task requires creating new files (pages, components, modules, tests), you MUST create them. Updating references/links without creating the actual files is an incomplete implementation.
10
11
  - Do not modify code unrelated to the task.
12
+ - Before creating a new utility or helper, check if a similar one already exists in the codebase. Reuse existing code over creating duplicates.
11
13
  - Follow existing code conventions and patterns in the repository.
12
14
 
15
+ ## Task completeness
16
+
17
+ Before reporting done, verify that ALL parts of the task are addressed:
18
+ - Re-read the task description and acceptance criteria.
19
+ - Check every requirement — if the task says "create pages X and Y", both must exist.
20
+ - If the task lists multiple deliverables, each one must be implemented, not just some.
21
+ - Run the test suite after implementation to verify nothing is broken.
22
+ - An incomplete implementation is worse than an error — never report success if parts are missing.
23
+
13
24
  ## File modification safety
14
25
 
15
26
  - NEVER overwrite existing files entirely. Always make targeted, minimal edits.
@@ -18,13 +29,6 @@ You are the **Coder** in a multi-role AI pipeline. Your job is to write code and
18
29
  - If unintended changes are detected, revert immediately with `git checkout -- <file>`.
19
30
  - Pay special attention to CSS, HTML, and config files where full rewrites destroy prior work.
20
31
 
21
- ## Multi-agent environment
22
-
23
- - Multiple developers and AI agents may be committing and modifying code simultaneously.
24
- - ALWAYS run `git fetch origin main` and check recent commits before starting work.
25
- - Before pushing or merging, rebase on the latest main: `git rebase origin/main`.
26
- - Create a dedicated branch per task and merge via PR, never push directly to main.
27
-
28
32
  ## Output format
29
33
 
30
34
  Return a JSON object:
@@ -20,6 +20,8 @@ You are the **Planner** in a multi-role AI pipeline. Your job is to create an im
20
20
  ## Rules
21
21
 
22
22
  - Each step should be small and independently verifiable.
23
+ - Steps must list ALL files involved: both files to modify AND new files to create. If a step requires creating a new file, list it explicitly in the `files` array.
24
+ - The plan must cover ALL requirements from the task. Re-read the task description before finalizing — if something is mentioned in the task, it must appear in a step.
23
25
  - Identify the testing strategy (unit, integration, E2E).
24
26
  - Consider backward compatibility.
25
27
  - Reference research findings when available.
@@ -5,7 +5,7 @@ You are the **Refactorer** in a multi-role AI pipeline. Your job is to improve c
5
5
  ## Constraints
6
6
 
7
7
  - Do NOT change any observable behavior or API contracts.
8
- - Do NOT expand the scope of changes beyond what was already modified.
8
+ - Focus on the files that were already modified in this session. You may create new files when extracting code (e.g., extracting a helper to a new module), but do not refactor unrelated parts of the codebase.
9
9
  - Keep all existing tests passing — run tests after every change.
10
10
  - Follow existing code conventions and patterns in the repository.
11
11
  - Do NOT add new features or fix unrelated bugs.
@@ -2,6 +2,12 @@
2
2
 
3
3
  You are the **Reviewer** in a multi-role AI pipeline. Your job is to review code changes against task requirements and quality standards.
4
4
 
5
+ ## Scope constraint
6
+
7
+ - **ONLY review files present in the diff.** Do not flag issues in files that were not changed.
8
+ - If you notice problems in untouched files, mention them as `non_blocking_suggestions` with a note that they are outside the current scope — never as `blocking_issues`.
9
+ - Your job is to review THIS change, not audit the entire codebase.
10
+
5
11
  ## Review priorities (in order)
6
12
 
7
13
  1. **Security** — vulnerabilities, exposed secrets, injection vectors
@@ -13,9 +19,10 @@ You are the **Reviewer** in a multi-role AI pipeline. Your job is to review code
13
19
  ## Rules
14
20
 
15
21
  - Focus on security, correctness, and tests first.
16
- - Only raise blocking issues for concrete production risks.
22
+ - Only raise blocking issues for concrete production risks in the changed files.
17
23
  - Keep non-blocking suggestions separate.
18
24
  - Style preferences NEVER block approval.
25
+ - Confidence threshold: reject only if < 0.70.
19
26
 
20
27
  ## File overwrite detection (BLOCKING)
21
28
 
@@ -31,7 +38,7 @@ Return a strict JSON object:
31
38
  "result": {
32
39
  "approved": true,
33
40
  "blocking_issues": [],
34
- "suggestions": ["Optional improvement ideas"],
41
+ "non_blocking_suggestions": ["Optional improvement ideas"],
35
42
  "confidence": 0.95
36
43
  },
37
44
  "summary": "Approved: all changes look correct and well-tested"
@@ -45,9 +52,9 @@ When rejecting:
45
52
  "result": {
46
53
  "approved": false,
47
54
  "blocking_issues": [
48
- { "file": "src/foo.js", "line": 42, "severity": "critical", "issue": "SQL injection vulnerability" }
55
+ { "id": "R-1", "file": "src/foo.js", "line": 42, "severity": "critical", "description": "SQL injection vulnerability", "suggested_fix": "Use parameterized queries instead of string concatenation" }
49
56
  ],
50
- "suggestions": [],
57
+ "non_blocking_suggestions": [],
51
58
  "confidence": 0.9
52
59
  },
53
60
  "summary": "Rejected: 1 critical security issue found"
@@ -0,0 +1,30 @@
1
+ # Auto-merge PRs approved by BecarIA reviewer.
2
+ # Merges when becaria-reviewer[bot] approves and all checks pass.
3
+
4
+ name: Auto-merge (BecarIA)
5
+
6
+ on:
7
+ pull_request_review:
8
+ types: [submitted]
9
+
10
+ permissions:
11
+ contents: write
12
+ pull-requests: write
13
+
14
+ jobs:
15
+ automerge:
16
+ if: >
17
+ github.event.review.state == 'approved' &&
18
+ contains(github.event.review.user.login, 'becaria-reviewer')
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Enable auto-merge
22
+ uses: actions/github-script@v7
23
+ with:
24
+ script: |
25
+ await github.rest.pulls.merge({
26
+ owner: context.repo.owner,
27
+ repo: context.repo.repo,
28
+ pull_number: context.payload.pull_request.number,
29
+ merge_method: 'squash'
30
+ });
@@ -0,0 +1,58 @@
1
+ # BecarIA Gateway — publishes PR comments and reviews from repository_dispatch events.
2
+ # Triggered by Karajan Code pipeline via `gh api repos/{owner}/{repo}/dispatches`.
3
+ #
4
+ # Required secrets:
5
+ # BECARIA_APP_ID — GitHub App ID for becaria-reviewer
6
+ # BECARIA_APP_PRIVATE_KEY — PEM private key for the GitHub App
7
+
8
+ name: BecarIA Gateway
9
+
10
+ on:
11
+ repository_dispatch:
12
+ types:
13
+ - becaria-comment
14
+ - becaria-review
15
+
16
+ permissions:
17
+ contents: read
18
+ pull-requests: write
19
+
20
+ jobs:
21
+ gateway:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - name: Generate App Token
25
+ id: app-token
26
+ uses: actions/create-github-app-token@v1
27
+ with:
28
+ app-id: ${{ secrets.BECARIA_APP_ID }}
29
+ private-key: ${{ secrets.BECARIA_APP_PRIVATE_KEY }}
30
+
31
+ - name: Post Comment
32
+ if: github.event.action == 'becaria-comment'
33
+ uses: actions/github-script@v7
34
+ with:
35
+ github-token: ${{ steps.app-token.outputs.token }}
36
+ script: |
37
+ const { pr_number, agent, body } = context.payload.client_payload;
38
+ await github.rest.issues.createComment({
39
+ owner: context.repo.owner,
40
+ repo: context.repo.repo,
41
+ issue_number: pr_number,
42
+ body: `**${agent}** (via BecarIA)\n\n${body}`
43
+ });
44
+
45
+ - name: Submit Review
46
+ if: github.event.action == 'becaria-review'
47
+ uses: actions/github-script@v7
48
+ with:
49
+ github-token: ${{ steps.app-token.outputs.token }}
50
+ script: |
51
+ const { pr_number, event, body, agent } = context.payload.client_payload;
52
+ await github.rest.pulls.createReview({
53
+ owner: context.repo.owner,
54
+ repo: context.repo.repo,
55
+ pull_number: pr_number,
56
+ event: event,
57
+ body: `**${agent}** (via BecarIA)\n\n${body}`
58
+ });
@@ -0,0 +1,46 @@
1
+ # Houston Override — allows a human to force-approve a PR by commenting "/houston approve".
2
+ # Bypasses BecarIA's REQUEST_CHANGES review.
3
+
4
+ name: Houston Override
5
+
6
+ on:
7
+ issue_comment:
8
+ types: [created]
9
+
10
+ permissions:
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ override:
15
+ if: >
16
+ github.event.issue.pull_request &&
17
+ contains(github.event.comment.body, '/houston approve')
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Dismiss BecarIA review and approve
21
+ uses: actions/github-script@v7
22
+ with:
23
+ script: |
24
+ const { owner, repo } = context.repo;
25
+ const pr_number = context.payload.issue.number;
26
+
27
+ // Find and dismiss becaria-reviewer reviews
28
+ const reviews = await github.rest.pulls.listReviews({
29
+ owner, repo, pull_number: pr_number
30
+ });
31
+
32
+ for (const review of reviews.data) {
33
+ if (review.user.login.includes('becaria-reviewer') && review.state === 'CHANGES_REQUESTED') {
34
+ await github.rest.pulls.dismissReview({
35
+ owner, repo, pull_number: pr_number, review_id: review.id,
36
+ message: `Dismissed by Houston Override (${context.payload.comment.user.login})`
37
+ });
38
+ }
39
+ }
40
+
41
+ // Approve the PR
42
+ await github.rest.pulls.createReview({
43
+ owner, repo, pull_number: pr_number,
44
+ event: 'APPROVE',
45
+ body: `Houston Override by @${context.payload.comment.user.login}`
46
+ });