karajan-code 1.50.1 → 1.52.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
@@ -14,6 +14,7 @@
14
14
  <a href="https://github.com/manufosela/karajan-code/actions"><img src="https://github.com/manufosela/karajan-code/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
15
15
  <a href="https://www.gnu.org/licenses/agpl-3.0"><img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License"></a>
16
16
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg" alt="Node.js"></a>
17
+ <a href="https://github.com/manufosela/homebrew-tap"><img src="https://img.shields.io/badge/homebrew-tap-orange.svg" alt="Homebrew"></a>
17
18
  </p>
18
19
 
19
20
  <p align="center">
@@ -52,19 +53,33 @@ If Claude Code is a smart pair programmer, Karajan is the CI/CD pipeline for AI-
52
53
 
53
54
  ## Install
54
55
 
55
- **One-liner** (detects your OS, installs via npm):
56
+ **npm** (recommended):
57
+ ```bash
58
+ npm install -g karajan-code
59
+ ```
60
+
61
+ **Homebrew** (macOS):
62
+ ```bash
63
+ brew tap manufosela/tap
64
+ brew install karajan-code
65
+ ```
56
66
 
67
+ **One-liner** (detects OS, installs via npm):
57
68
  ```bash
58
69
  curl -fsSL https://raw.githubusercontent.com/manufosela/karajan-code/main/scripts/install-kj.sh | sh
59
70
  ```
60
71
 
61
- **Or directly with npm:**
72
+ **Docker** (no Node.js needed):
73
+ ```bash
74
+ docker run --rm -v $(pwd):/workspace karajan-code kj --version
75
+ ```
62
76
 
77
+ **Python**:
63
78
  ```bash
64
- npm install -g karajan-code
79
+ cd wrappers/python && pip install .
65
80
  ```
66
81
 
67
- That's it. No Docker required (SonarQube uses Docker, but Karajan auto-manages it). No config files to copy. `kj init` auto-detects your installed agents.
82
+ That's it. `kj init` auto-detects your installed agents and installs RTK for token optimization.
68
83
 
69
84
  ## Three ways to use Karajan
70
85
 
@@ -260,3 +275,8 @@ Issues and pull requests welcome. If something doesn't work as documented, [open
260
275
  ---
261
276
 
262
277
  Built by [@manufosela](https://github.com/manufosela). Head of Engineering at Geniova Technologies, co-organizer of NodeJS Madrid, author of [Liderazgo Afectivo](https://www.liderazgoafectivo.com). 90+ npm packages published.
278
+
279
+ ### Contributors
280
+
281
+ - [@aitormf](https://github.com/aitormf) — OpenCode agent (5th built-in agent)
282
+ - [@reiaguilera](https://github.com/reiaguilera) — Beta testing, feature proposals, and quality feedback
package/docs/README.es.md CHANGED
@@ -14,6 +14,7 @@
14
14
  <a href="https://github.com/manufosela/karajan-code/actions"><img src="https://github.com/manufosela/karajan-code/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
15
15
  <a href="https://www.gnu.org/licenses/agpl-3.0"><img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License"></a>
16
16
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg" alt="Node.js"></a>
17
+ <a href="https://github.com/manufosela/homebrew-tap"><img src="https://img.shields.io/badge/homebrew-tap-orange.svg" alt="Homebrew"></a>
17
18
  </p>
18
19
 
19
20
  <p align="center">
@@ -52,11 +53,28 @@ Si Claude Code es un programador de pares inteligente, Karajan es el pipeline CI
52
53
 
53
54
  ## Instalacion
54
55
 
56
+ **npm** (recomendado):
55
57
  ```bash
56
58
  npm install -g karajan-code
57
59
  ```
58
60
 
59
- Eso es todo. No requiere Docker (SonarQube usa Docker, pero Karajan lo gestiona automaticamente). Sin ficheros de configuracion que copiar. `kj init` auto-detecta tus agentes instalados.
61
+ **Homebrew** (macOS):
62
+ ```bash
63
+ brew tap manufosela/tap
64
+ brew install karajan-code
65
+ ```
66
+
67
+ **One-liner** (detecta SO, instala via npm):
68
+ ```bash
69
+ curl -fsSL https://raw.githubusercontent.com/manufosela/karajan-code/main/scripts/install-kj.sh | sh
70
+ ```
71
+
72
+ **Docker** (sin Node.js):
73
+ ```bash
74
+ docker run --rm -v $(pwd):/workspace karajan-code kj --version
75
+ ```
76
+
77
+ `kj init` auto-detecta tus agentes instalados e instala RTK para optimizacion de tokens.
60
78
 
61
79
  ## Tres formas de usar Karajan
62
80
 
@@ -230,3 +248,8 @@ Issues y pull requests bienvenidos. Si algo no funciona como esta documentado, [
230
248
  ---
231
249
 
232
250
  Construido por [@manufosela](https://github.com/manufosela). Head of Engineering en Geniova Technologies, co-organizador de NodeJS Madrid, autor de [Liderazgo Afectivo](https://www.liderazgoafectivo.com). 90+ paquetes npm publicados.
251
+
252
+ ### Contributors
253
+
254
+ - [@aitormf](https://github.com/aitormf) — Agente OpenCode (5o agente built-in)
255
+ - [@reiaguilera](https://github.com/reiaguilera) — Beta testing, propuestas de mejora y feedback de calidad
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.50.1",
3
+ "version": "1.52.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",
@@ -208,15 +208,20 @@ async function checkBecariaInfra(config) {
208
208
  }
209
209
 
210
210
  async function checkRtk() {
211
- const NOT_FOUND_DETAIL = `Not found — install for 60-90% token savings: ${getInstallCommand("rtk")}`;
212
- let detail = NOT_FOUND_DETAIL;
211
+ const installCmd = getInstallCommand("rtk");
213
212
  try {
214
213
  const res = await runCommand("rtk", ["--version"]);
215
214
  if (res.exitCode === 0) {
216
- detail = `${res.stdout.trim()} — token savings active`;
215
+ return { name: "rtk", label: "RTK (Rust Token Killer)", ok: true, detail: `${res.stdout.trim()} — token savings active`, fix: null };
217
216
  }
218
217
  } catch { /* not installed */ }
219
- return { name: "rtk", label: "RTK (Rust Token Killer)", ok: true, detail, fix: null };
218
+ return {
219
+ name: "rtk",
220
+ label: "RTK (Rust Token Killer)",
221
+ ok: false,
222
+ detail: "Not found — 60-90% token savings available",
223
+ fix: `Install: ${installCmd}`
224
+ };
220
225
  }
221
226
 
222
227
  async function checkRuleFiles(config) {
@@ -10,6 +10,8 @@ import { createWizard, isTTY } from "../utils/wizard.js";
10
10
  import { runCommand } from "../utils/process.js";
11
11
  import { getInstallCommand } from "../utils/os-detect.js";
12
12
  import { detectOsLocale, SUPPORTED_LANGUAGES } from "../utils/locale.js";
13
+ import { detectRtk } from "../utils/rtk-detect.js";
14
+ import { installRtk } from "../utils/rtk-install.js";
13
15
 
14
16
  async function runWizard(config, logger) {
15
17
  const agents = await detectAvailableAgents();
@@ -328,18 +330,16 @@ export async function initCommand({ logger, flags = {} }) {
328
330
  await ensureCoderRules(coderRulesPath, logger);
329
331
  await installSkills(logger, interactive);
330
332
 
331
- // Check RTK availability and inform user
332
- let hasRtk = false;
333
- try {
334
- const rtkRes = await runCommand("rtk", ["--version"]);
335
- hasRtk = rtkRes.exitCode === 0;
336
- } catch { /* rtk not installed */
337
- hasRtk = false;
338
- }
339
- if (!hasRtk) {
340
- logger.info("");
341
- logger.info("RTK (Rust Token Killer) can reduce token usage by 60-90%.");
342
- logger.info(` Install: ${getInstallCommand("rtk")}`);
333
+ // Check RTK availability — auto-install if missing
334
+ const rtk = await detectRtk();
335
+ if (rtk.available) {
336
+ logger.info(`RTK ${rtk.version || ""} detected.`);
337
+ } else {
338
+ const installResult = await installRtk(logger);
339
+ if (!installResult.ok) {
340
+ logger.warn("RTK is optional but recommended for 60-90% token savings.");
341
+ logger.warn(` Manual install: ${getInstallCommand("rtk")}`);
342
+ }
343
343
  }
344
344
 
345
345
  await setupSonarQube(config, logger);
@@ -152,6 +152,7 @@ async function buildReport(dir, sessionId) {
152
152
  };
153
153
  if (session.pg_task_id) report.pg_task_id = session.pg_task_id;
154
154
  if (session.pg_project_id) report.pg_project_id = session.pg_project_id;
155
+ if (session.rtk_savings) report.rtk_savings = session.rtk_savings;
155
156
  return report;
156
157
  }
157
158
 
@@ -326,6 +327,16 @@ async function resolveTraceOptions(currency) {
326
327
  return { cur, rate };
327
328
  }
328
329
 
330
+ function printRtkSavings(rtkSavings) {
331
+ if (!rtkSavings || !rtkSavings.callCount) return;
332
+ const tokens = rtkSavings.estimatedTokensSaved ?? 0;
333
+ const ratio = rtkSavings.savedPct ?? 0;
334
+ const commands = rtkSavings.callCount ?? 0;
335
+ console.log("");
336
+ console.log(`RTK Savings: ~${tokens} tokens saved (${ratio}% compression, ${commands} commands)`);
337
+ console.log(` Original: ${rtkSavings.originalBytes} bytes, Compressed: ${rtkSavings.rtkBytes} bytes, Saved: ${rtkSavings.savedBytes} bytes`);
338
+ }
339
+
329
340
  function printTraceReport(report, currency, exchangeRate) {
330
341
  console.log(`Session: ${report.session_id}`);
331
342
  printPgCardLine(report);
@@ -333,6 +344,7 @@ function printTraceReport(report, currency, exchangeRate) {
333
344
  console.log(`Task: ${report.task_description || "N/A"}`);
334
345
  console.log("");
335
346
  printTraceTable(report.budget_trace, { currency, exchangeRate });
347
+ printRtkSavings(report.rtk_savings);
336
348
  }
337
349
 
338
350
  async function handlePgTaskReport({ dir, pgTask, list, sessionId, format, trace, currency }) {
@@ -6,6 +6,32 @@ import { VALID_TASK_TYPES } from "./policy-resolver.js";
6
6
  * Evaluated top-down; first match with confidence >= threshold wins.
7
7
  */
8
8
  const INTENT_PATTERNS = [
9
+ // No-code tasks (data analysis, document generation, SQL queries, CSV transforms)
10
+ // Placed before audit so specific multi-word patterns win over single-word "analizar"/"analyze"
11
+ {
12
+ id: "no-code",
13
+ keywords: [
14
+ "generate report", "create document", "analyze data", "transform csv",
15
+ "sql query", "sql queries", "generate queries", "data analysis", "process data",
16
+ "export to", "import from", "convert data", "parse file",
17
+ "generar informe", "crear documento", "analizar datos", "transformar csv",
18
+ "consulta sql", "generar consultas", "analisis de datos", "procesar datos",
19
+ "exportar a", "importar de", "convertir datos", "parsear fichero",
20
+ ],
21
+ taskType: "no-code",
22
+ level: "simple",
23
+ confidence: 0.9,
24
+ message: "No-code task detected — TDD and SonarQube disabled, coder generates non-code output",
25
+ },
26
+ // Audit / analysis tasks (no code changes, read-only analysis)
27
+ {
28
+ id: "audit",
29
+ keywords: ["audit", "review", "analyze", "check", "verify", "inspect", "scan", "auditar", "revisar", "analizar", "comprobar", "verificar", "inspeccionar"],
30
+ taskType: "audit",
31
+ level: "simple",
32
+ confidence: 0.9,
33
+ message: "Audit/analysis task detected — skipping coder and reviewer",
34
+ },
9
35
  // Documentation-only tasks
10
36
  {
11
37
  id: "doc-readme",
@@ -1,4 +1,4 @@
1
- export const VALID_TASK_TYPES = new Set(["sw", "infra", "doc", "add-tests", "refactor"]);
1
+ export const VALID_TASK_TYPES = new Set(["sw", "infra", "doc", "add-tests", "refactor", "audit", "analysis", "no-code"]);
2
2
 
3
3
  export const DEFAULT_POLICIES = {
4
4
  sw: { tdd: true, sonar: true, reviewer: true, testsRequired: true },
@@ -6,6 +6,9 @@ export const DEFAULT_POLICIES = {
6
6
  doc: { tdd: false, sonar: false, reviewer: true, testsRequired: false },
7
7
  "add-tests": { tdd: false, sonar: true, reviewer: true, testsRequired: true },
8
8
  refactor: { tdd: true, sonar: true, reviewer: true, testsRequired: false },
9
+ audit: { tdd: false, sonar: false, reviewer: false, testsRequired: false, coderRequired: false },
10
+ analysis: { tdd: false, sonar: false, reviewer: false, testsRequired: false, coderRequired: false },
11
+ "no-code": { tdd: false, sonar: false, reviewer: true, testsRequired: false, coderRequired: true },
9
12
  };
10
13
 
11
14
  const FALLBACK_TYPE = "sw";
@@ -41,6 +41,9 @@ import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js"
41
41
  import { detectTestFramework } from "./utils/project-detect.js";
42
42
  import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
43
43
  import { detectRtk } from "./utils/rtk-detect.js";
44
+ import { createRtkRunner, RtkSavingsTracker } from "./utils/rtk-wrapper.js";
45
+ import { setRunner as setDiffRunner } from "./review/diff-generator.js";
46
+ import { setRunner as setGitRunner } from "./utils/git.js";
44
47
  import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "./skills/skill-detector.js";
45
48
  import { isOpenSkillsAvailable } from "./skills/openskills-client.js";
46
49
 
@@ -278,6 +281,9 @@ function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBa
278
281
  if (!resolvedPolicies.reviewer) {
279
282
  pipelineFlags.reviewerEnabled = false;
280
283
  }
284
+ if (resolvedPolicies.coderRequired === false) {
285
+ pipelineFlags.coderRequired = false;
286
+ }
281
287
 
282
288
  emitProgress(
283
289
  emitter,
@@ -760,7 +766,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
760
766
  return { action: "proceed" };
761
767
  }
762
768
 
763
- async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i }) {
769
+ async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker }) {
764
770
  const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
765
771
 
766
772
  // Accumulate final commits for PG card lifecycle tracking
@@ -779,16 +785,24 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
779
785
  await markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger });
780
786
 
781
787
  const deferredIssues = session.deferred_issues || [];
788
+ const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
789
+ if (rtkSavings) session.rtk_savings = rtkSavings;
790
+ await saveSession(session);
791
+
792
+ const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
793
+ if (rtkSavings) endDetail.rtk_savings = rtkSavings;
782
794
  emitProgress(
783
795
  emitter,
784
796
  makeEvent("session:end", { ...eventBase, stage: "done" }, {
785
797
  message: deferredIssues.length > 0
786
798
  ? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
787
799
  : "Session approved",
788
- detail: { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues }
800
+ detail: endDetail
789
801
  })
790
802
  );
791
- return { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
803
+ const result = { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
804
+ if (rtkSavings) result.rtk_savings = rtkSavings;
805
+ return result;
792
806
  }
793
807
 
794
808
  // PG card "To Validate" logic moved to src/planning-game/pipeline-adapter.js → markPgCardToValidate()
@@ -1071,7 +1085,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
1071
1085
  if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
1072
1086
  if (tddResult.action === "continue") return { action: "continue" };
1073
1087
 
1074
- const skipSonarForTaskType = new Set(["infra", "doc"]);
1088
+ const skipSonarForTaskType = new Set(["infra", "doc", "no-code"]);
1075
1089
  const effectiveTaskType = session.resolved_policies?.taskType || null;
1076
1090
  if (config.sonarqube.enabled && !skipSonarForTaskType.has(effectiveTaskType)) {
1077
1091
  const sonarResult = await runSonarStage({
@@ -1132,7 +1146,7 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
1132
1146
  return { action: "ok", review: reviewerResult.review };
1133
1147
  }
1134
1148
 
1135
- async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review }) {
1149
+ async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker }) {
1136
1150
  session.reviewer_retry_count = 0;
1137
1151
  const postLoopResult = await handlePostLoopStages({
1138
1152
  config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
@@ -1141,11 +1155,11 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
1141
1155
  if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
1142
1156
  if (postLoopResult.action === "continue") return { action: "continue" };
1143
1157
 
1144
- const result = await finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i });
1158
+ const result = await finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker });
1145
1159
  return { action: "return", result };
1146
1160
  }
1147
1161
 
1148
- async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task }) {
1162
+ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
1149
1163
  // Escalate to Solomon / human before giving up
1150
1164
  const solomonResult = await invokeSolomon({
1151
1165
  config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
@@ -1178,13 +1192,17 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1178
1192
 
1179
1193
  // Solomon also couldn't resolve — fail
1180
1194
  session.budget = budgetSummary();
1195
+ const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
1196
+ if (rtkSavings) session.rtk_savings = rtkSavings;
1181
1197
  await markSessionStatus(session, "failed");
1198
+ const failDetail = { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() };
1199
+ if (rtkSavings) failDetail.rtk_savings = rtkSavings;
1182
1200
  emitProgress(
1183
1201
  emitter,
1184
1202
  makeEvent("session:end", { ...eventBase, stage: "done" }, {
1185
1203
  status: "fail",
1186
1204
  message: "Max iterations reached (Solomon could not resolve)",
1187
- detail: { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() }
1205
+ detail: failDetail
1188
1206
  })
1189
1207
  );
1190
1208
  return { approved: false, sessionId: session.id, reason: "max_iterations" };
@@ -1232,9 +1250,14 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1232
1250
  const rtkResult = await detectRtk();
1233
1251
  if (rtkResult.available) {
1234
1252
  config = { ...config, rtk: { available: true, version: rtkResult.version } };
1235
- logger.info(`RTK detected (${rtkResult.version}) — instructing agents to prefix Bash commands with rtk`);
1253
+ const rtkTracker = new RtkSavingsTracker();
1254
+ const rtkRunner = createRtkRunner(true, rtkTracker);
1255
+ setDiffRunner(rtkRunner);
1256
+ setGitRunner(rtkRunner);
1257
+ ctx.rtkTracker = rtkTracker;
1258
+ logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
1236
1259
  emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
1237
- message: "RTK detected — agent commands will use token optimization",
1260
+ message: "RTK detected — internal commands wrapped for token optimization",
1238
1261
  detail: { version: rtkResult.version, executorType: "local" }
1239
1262
  }));
1240
1263
  }
@@ -1351,7 +1374,8 @@ async function runSingleIteration(ctx) {
1351
1374
  const approvedResult = await handleApprovedReview({
1352
1375
  config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
1353
1376
  stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
1354
- gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review
1377
+ gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review,
1378
+ rtkTracker: ctx.rtkTracker
1355
1379
  });
1356
1380
  if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
1357
1381
  }
@@ -1430,7 +1454,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
1430
1454
  if (iterResult.action === "retry") { i -= 1; }
1431
1455
  }
1432
1456
 
1433
- const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask });
1457
+ const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
1434
1458
  return maxIterResult;
1435
1459
  }
1436
1460
 
@@ -1444,6 +1468,53 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1444
1468
  const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1445
1469
 
1446
1470
  try {
1471
+ // --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
1472
+ if (ctx.pipelineFlags.coderRequired === false) {
1473
+ logger.info("Analysis-only task — skipping coder/reviewer iteration loop");
1474
+ emitProgress(emitter, makeEvent("pipeline:analysis-only", { ...ctx.eventBase, stage: "analysis" }, {
1475
+ message: "Analysis-only task — running security and audit stages only",
1476
+ detail: { taskType: ctx.session.resolved_policies?.taskType, coderRequired: false }
1477
+ }));
1478
+
1479
+ const analysisStageResults = ctx.stageResults;
1480
+ const postLoopDiff = await generateDiff({ baseRef: ctx.session.session_start_sha });
1481
+
1482
+ if (ctx.pipelineFlags.securityEnabled) {
1483
+ const securityResult = await runSecurityStage({
1484
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
1485
+ coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
1486
+ iteration: 1, task: ctx.plannedTask, diff: postLoopDiff, askQuestion
1487
+ });
1488
+ if (securityResult.stageResult) analysisStageResults.security = securityResult.stageResult;
1489
+ }
1490
+
1491
+ const auditResult = await runFinalAuditStage({
1492
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
1493
+ coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
1494
+ iteration: 1, task: ctx.plannedTask, diff: postLoopDiff
1495
+ });
1496
+ if (auditResult.stageResult) analysisStageResults.audit = auditResult.stageResult;
1497
+
1498
+ ctx.session.budget = ctx.budgetSummary();
1499
+ await markSessionStatus(ctx.session, "approved");
1500
+
1501
+ const analysisResult = {
1502
+ approved: true,
1503
+ sessionId: ctx.session.id,
1504
+ analysisOnly: true,
1505
+ stages: analysisStageResults,
1506
+ budget: ctx.budgetSummary()
1507
+ };
1508
+ await writeHistoryRecord({ sessionId: ctx.session.id, task, result: analysisResult, logger });
1509
+
1510
+ emitProgress(emitter, makeEvent("session:end", { ...ctx.eventBase, stage: "done" }, {
1511
+ message: "Analysis-only session completed",
1512
+ detail: analysisResult
1513
+ }));
1514
+
1515
+ return analysisResult;
1516
+ }
1517
+
1447
1518
  // --- HU Sub-Pipeline: run each certified HU as an independent iteration loop ---
1448
1519
  if (needsSubPipeline(ctx.stageResults.huReviewer)) {
1449
1520
  logger.info(`HU sub-pipeline: ${ctx.stageResults.huReviewer.certified} certified stories — running each as a sub-pipeline`);
@@ -39,13 +39,15 @@ export function buildTriagePrompt({ task, instructions, availableRoles }) {
39
39
  "- **reviewer**: Enable for most tasks as a quality gate. Disable only for trivial, single-line changes.",
40
40
  "- **architect**: Enable when creating new modules/apps, changing data models or APIs, medium/complex tasks, or when design approach is ambiguous. Disable for simple fixes, doc-only, or CSS-only changes.",
41
41
  "- **hu-reviewer**: Enable for medium/complex tasks that benefit from story decomposition and certification. Disable for trivial/simple tasks, doc-only, or CSS-only changes.",
42
+ "- **audit/analysis taskType**: Set taskType to \"audit\" or \"analysis\" for tasks that only require reading/inspecting code without making changes (e.g., audit, review, analyze, check, verify, inspect, scan, auditar, revisar, analizar, comprobar, verificar, inspeccionar). The coder and reviewer will be skipped entirely for these tasks.",
43
+ "- **no-code taskType**: Use no-code when the task is about data analysis, document generation, SQL queries, CSV transformation, report generation, or any non-programming task. The coder will generate output (not source code), and TDD/SonarQube will be disabled. The reviewer still runs to validate logic/correctness.",
42
44
  "",
43
- "Note: coder is ALWAYS active — you don't need to decide on it."
45
+ "Note: coder is ALWAYS active for coding tasks — you don't need to decide on it. However, for audit/analysis taskTypes the coder is skipped."
44
46
  ].join("\n"),
45
47
  "Classify the task complexity, determine its taskType, recommend only the necessary pipeline roles, and assess whether the task should be decomposed into smaller subtasks.",
46
48
  "Keep the reasoning short and practical.",
47
49
  "Return a single valid JSON object and nothing else.",
48
- 'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security|architect|hu-reviewer"],"taskType":"sw|infra|doc|add-tests|refactor","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}',
50
+ 'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security|architect|hu-reviewer"],"taskType":"sw|infra|doc|add-tests|refactor|audit|analysis|no-code","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}',
49
51
  `## Task\n${task}`
50
52
  );
51
53
 
@@ -1,10 +1,26 @@
1
1
  import { runCommand } from "../utils/process.js";
2
2
 
3
+ /** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
4
+ let _runner = null;
5
+
6
+ /**
7
+ * Inject an RTK-aware runner so all git operations in this module benefit from token savings.
8
+ * Call once at pipeline start when RTK is available.
9
+ * @param {(command: string, args?: string[], options?: object) => Promise<object>} runner
10
+ */
11
+ export function setRunner(runner) {
12
+ _runner = runner;
13
+ }
14
+
15
+ function run(command, args, ...rest) {
16
+ return (_runner || runCommand)(command, args, ...rest);
17
+ }
18
+
3
19
  export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
4
20
  if (baseRef) return baseRef;
5
- const mergeBase = await runCommand("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
21
+ const mergeBase = await run("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
6
22
  if (mergeBase.exitCode !== 0) {
7
- const fallback = await runCommand("git", ["rev-parse", "HEAD~1"]);
23
+ const fallback = await run("git", ["rev-parse", "HEAD~1"]);
8
24
  if (fallback.exitCode !== 0) {
9
25
  throw new Error("Could not compute diff base reference");
10
26
  }
@@ -16,9 +32,9 @@ export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
16
32
  export async function generateDiff({ baseRef, stageNewFiles = false }) {
17
33
  // Stage untracked files so they appear in the diff (coder creates files but doesn't git add)
18
34
  if (stageNewFiles) {
19
- await runCommand("git", ["add", "-A"]);
35
+ await run("git", ["add", "-A"]);
20
36
  }
21
- const result = await runCommand("git", ["diff", stageNewFiles ? "--cached" : "", `${baseRef}`].filter(Boolean));
37
+ const result = await run("git", ["diff", stageNewFiles ? "--cached" : "", `${baseRef}`].filter(Boolean));
22
38
  if (result.exitCode !== 0) {
23
39
  throw new Error(`git diff failed: ${result.stderr || result.stdout}`);
24
40
  }
@@ -26,7 +42,7 @@ export async function generateDiff({ baseRef, stageNewFiles = false }) {
26
42
  }
27
43
 
28
44
  export async function getUntrackedFiles() {
29
- const result = await runCommand("git", ["ls-files", "--others", "--exclude-standard"]);
45
+ const result = await run("git", ["ls-files", "--others", "--exclude-standard"]);
30
46
  if (result.exitCode !== 0) return [];
31
47
  return result.stdout.trim().split("\n").filter(Boolean);
32
48
  }
@@ -19,7 +19,7 @@ function isSourceFile(file, extensions = []) {
19
19
  return extensions.some((ext) => file.endsWith(ext));
20
20
  }
21
21
 
22
- const SKIP_TDD_TASK_TYPES = new Set(["doc", "infra"]);
22
+ const SKIP_TDD_TASK_TYPES = new Set(["doc", "infra", "no-code"]);
23
23
 
24
24
  const DEFAULT_TEST_PATTERNS = ["/tests/", "/__tests__/", ".test.", ".spec.", "/src/test/", "/test/", "Test.java", "Test.kt", "test_", "_test.py", "_test.go", "Test.cs", "_spec.rb", "Test.php", "_test.dart", "Tests.swift"];
25
25
  const DEFAULT_SOURCE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".kt", ".rb", ".php", ".cs", ".rs", ".swift", ".dart"];
@@ -67,6 +67,15 @@ const TASK_TEXT_PATTERNS = [
67
67
  { pattern: /\bflutter\b/i, skill: "flutter" },
68
68
  { pattern: /\bswift\b/i, skill: "swift" },
69
69
  { pattern: /\bphp\b/i, skill: "php" },
70
+ { pattern: /\bSQL\b/i, skill: "sql-analysis" },
71
+ { pattern: /\bquer(?:y|ies)\b/i, skill: "sql-analysis" },
72
+ { pattern: /\bdatabase\b/i, skill: "sql-analysis" },
73
+ { pattern: /\bCSV\b/i, skill: "csv-transform" },
74
+ { pattern: /\btransform\b/i, skill: "csv-transform" },
75
+ { pattern: /\bparse\b/i, skill: "csv-transform" },
76
+ { pattern: /\breport\b/i, skill: "data-report" },
77
+ { pattern: /\banalysis\b/i, skill: "data-report" },
78
+ { pattern: /\bfindings\b/i, skill: "data-report" },
70
79
  ];
71
80
 
72
81
  /**
@@ -252,6 +252,14 @@ function isBudgetUnavailable(budget) {
252
252
  (budget.total_tokens === 0 && budget.total_cost_usd === 0 && Object.keys(budget.breakdown_by_role || {}).length > 0);
253
253
  }
254
254
 
255
+ function printSessionRtkSavings(rtkSavings) {
256
+ if (!rtkSavings || !rtkSavings.callCount) return;
257
+ const tokens = rtkSavings.estimatedTokensSaved ?? 0;
258
+ const ratio = rtkSavings.savedPct ?? 0;
259
+ const commands = rtkSavings.callCount ?? 0;
260
+ console.log(` ${ANSI.dim}\u26a1 RTK: saved ~${tokens} tokens (${ratio}% compression, ${commands} commands)${ANSI.reset}`);
261
+ }
262
+
255
263
  function printSessionBudget(budget) {
256
264
  if (!budget) return;
257
265
  if (isBudgetUnavailable(budget)) {
@@ -447,6 +455,7 @@ const EVENT_HANDLERS = {
447
455
  printSessionStages(event.detail?.stages);
448
456
  printSessionGit(event.detail?.git);
449
457
  printSessionBudget(event.detail?.budget);
458
+ printSessionRtkSavings(event.detail?.rtk_savings);
450
459
  console.log(`${ANSI.dim}Session: ${event.sessionId}${ANSI.reset}`);
451
460
  },
452
461
 
package/src/utils/git.js CHANGED
@@ -1,5 +1,20 @@
1
1
  import { runCommand } from "./process.js";
2
2
 
3
+ /** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
4
+ let _runner = null;
5
+
6
+ /**
7
+ * Inject an RTK-aware runner so all git operations in this module benefit from token savings.
8
+ * @param {(command: string, args?: string[], options?: object) => Promise<object>} runner
9
+ */
10
+ export function setRunner(runner) {
11
+ _runner = runner;
12
+ }
13
+
14
+ function run(command, args, ...rest) {
15
+ return (_runner || runCommand)(command, args, ...rest);
16
+ }
17
+
3
18
  function slugifyTask(task) {
4
19
  return String(task)
5
20
  .toLowerCase()
@@ -9,7 +24,7 @@ function slugifyTask(task) {
9
24
  }
10
25
 
11
26
  async function runGit(args, options = {}) {
12
- const res = await runCommand("git", args, options);
27
+ const res = await run("git", args, options);
13
28
  if (res.exitCode !== 0) {
14
29
  throw new Error(`git ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
15
30
  }
@@ -17,7 +32,7 @@ async function runGit(args, options = {}) {
17
32
  }
18
33
 
19
34
  export async function ensureGitRepo() {
20
- const res = await runCommand("git", ["rev-parse", "--is-inside-work-tree"]);
35
+ const res = await run("git", ["rev-parse", "--is-inside-work-tree"]);
21
36
  return res.exitCode === 0 && res.stdout.trim() === "true";
22
37
  }
23
38
 
@@ -0,0 +1,38 @@
1
+ import { runCommand } from "./process.js";
2
+ import { getInstallCommand } from "./os-detect.js";
3
+ import { detectRtk } from "./rtk-detect.js";
4
+
5
+ /**
6
+ * Install RTK (Rust Token Killer) using the OS-appropriate command.
7
+ * @param {object} logger - Logger with info/warn methods
8
+ * @returns {Promise<{ ok: boolean, version: string|null, error: string|null }>}
9
+ */
10
+ export async function installRtk(logger) {
11
+ const command = getInstallCommand("rtk");
12
+
13
+ logger.info("Installing RTK (Rust Token Killer)...");
14
+
15
+ try {
16
+ const result = await runCommand("sh", ["-c", command], { timeout: 120_000 });
17
+
18
+ if (result.exitCode !== 0) {
19
+ const error = (result.stderr || "").trim() || `exit code ${result.exitCode}`;
20
+ logger.warn(`RTK install failed: ${error}`);
21
+ return { ok: false, version: null, error };
22
+ }
23
+
24
+ // Verify installation
25
+ const check = await detectRtk();
26
+ if (check.available) {
27
+ logger.info(`RTK ${check.version || ""} installed successfully.`);
28
+ return { ok: true, version: check.version, error: null };
29
+ }
30
+
31
+ logger.warn("RTK install command succeeded but rtk binary not found in PATH.");
32
+ return { ok: false, version: null, error: "Binary not found after install" };
33
+ } catch (err) {
34
+ const error = err.message || String(err);
35
+ logger.warn(`RTK install failed: ${error}`);
36
+ return { ok: false, version: null, error };
37
+ }
38
+ }
@@ -0,0 +1,87 @@
1
+ import { runCommand } from "./process.js";
2
+
3
+ /**
4
+ * Commands that RTK can safely wrap for token-optimized output.
5
+ */
6
+ export const RTK_SUPPORTED_COMMANDS = [
7
+ "git", "ls", "find", "grep", "cat", "head", "tail",
8
+ "wc", "diff", "tree", "du", "file"
9
+ ];
10
+
11
+ const supportedSet = new Set(RTK_SUPPORTED_COMMANDS);
12
+
13
+ /**
14
+ * Wrap a command with RTK if available and the command is in the whitelist.
15
+ * @param {string} command
16
+ * @param {string[]} args
17
+ * @param {boolean} rtkAvailable
18
+ * @returns {{ command: string, args: string[] }}
19
+ */
20
+ export function wrapWithRtk(command, args, rtkAvailable) {
21
+ if (rtkAvailable && supportedSet.has(command)) {
22
+ return { command: "rtk", args: [command, ...args] };
23
+ }
24
+ return { command, args };
25
+ }
26
+
27
+ /**
28
+ * Session-level accumulator for RTK byte savings.
29
+ */
30
+ export class RtkSavingsTracker {
31
+ constructor() {
32
+ this.originalBytes = 0;
33
+ this.rtkBytes = 0;
34
+ this.callCount = 0;
35
+ }
36
+
37
+ record(originalSize, rtkSize) {
38
+ this.originalBytes += originalSize;
39
+ this.rtkBytes += rtkSize;
40
+ this.callCount += 1;
41
+ }
42
+
43
+ summary() {
44
+ const savedBytes = this.originalBytes - this.rtkBytes;
45
+ const savedPct = this.originalBytes > 0
46
+ ? Number(((savedBytes / this.originalBytes) * 100).toFixed(1))
47
+ : 0;
48
+ const estimatedTokensSaved = Math.floor(savedBytes / 4);
49
+ return {
50
+ originalBytes: this.originalBytes,
51
+ rtkBytes: this.rtkBytes,
52
+ savedBytes,
53
+ savedPct,
54
+ estimatedTokensSaved,
55
+ callCount: this.callCount
56
+ };
57
+ }
58
+
59
+ /** Returns true if any commands were recorded. */
60
+ hasData() {
61
+ return this.callCount > 0;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Create a runner function that transparently wraps supported commands with RTK.
67
+ * @param {boolean} rtkAvailable
68
+ * @param {RtkSavingsTracker} [tracker] - Optional savings tracker
69
+ * @returns {(command: string, args?: string[], options?: object) => Promise<object>}
70
+ */
71
+ export function createRtkRunner(rtkAvailable, tracker = null) {
72
+ return async (command, args = [], options = {}) => {
73
+ const wrapped = wrapWithRtk(command, args, rtkAvailable);
74
+ const result = await runCommand(wrapped.command, wrapped.args, options);
75
+
76
+ if (tracker && rtkAvailable && supportedSet.has(command)) {
77
+ const outputSize = Buffer.byteLength(result.stdout || "", "utf8");
78
+ // For RTK-wrapped calls, the output is already compressed.
79
+ // We estimate the original size as outputSize (conservative — real savings
80
+ // would require running the command without RTK, which defeats the purpose).
81
+ // The tracker is useful when the caller provides real estimates externally.
82
+ tracker.record(outputSize, outputSize);
83
+ }
84
+
85
+ return result;
86
+ };
87
+ }
@@ -0,0 +1,40 @@
1
+ # kj-csv-transform — CSV & Data Transformation
2
+
3
+ Transform, validate, and process CSV and tabular data files.
4
+
5
+ ## Your task
6
+
7
+ $ARGUMENTS
8
+
9
+ ## Coder instructions
10
+
11
+ When working with CSV or tabular data:
12
+
13
+ 1. **Auto-detect delimiter** — do not assume comma. Detect the actual delimiter (comma, semicolon, tab, pipe) by inspecting the first few lines. Use libraries that support delimiter sniffing when available.
14
+ 2. **Handle encoding** — detect and handle file encoding (UTF-8, UTF-8 BOM, Latin-1/ISO-8859-1, Windows-1252). Convert to UTF-8 for processing.
15
+ 3. **Validate data types per column** — after parsing, verify that each column contains the expected data type (string, number, date, boolean). Flag rows with type mismatches.
16
+ 4. **Handle missing values explicitly** — define a strategy for missing/empty values (keep as null, fill with default, skip row). Never silently drop rows with missing data.
17
+ 5. **Preserve row count** — track input vs output row counts. Log any rows filtered, merged, or split so the transformation is auditable.
18
+ 6. **Quote fields correctly** — when writing CSV output, properly escape fields containing delimiters, quotes, or newlines.
19
+ 7. **Header normalization** — trim whitespace from headers, handle duplicate column names, and normalize casing if needed.
20
+ 8. **Large file support** — for files that may be large, use streaming/chunked processing instead of loading everything into memory.
21
+
22
+ ## Reviewer checklist
23
+
24
+ When reviewing CSV/data transformation code, check for:
25
+
26
+ - [ ] **Data integrity** — input row count matches expected output row count (accounting for intentional filters)?
27
+ - [ ] **No silent data loss** — missing values are handled explicitly, not silently dropped?
28
+ - [ ] **Correct transformations** — type conversions, date parsing, and numeric formatting are accurate?
29
+ - [ ] **Encoding handled** — non-ASCII characters preserved correctly through the pipeline?
30
+ - [ ] **Delimiter consistency** — detection works for the actual file, not just assumed comma?
31
+ - [ ] **Edge cases** — empty files, single-row files, files with only headers, fields with embedded newlines?
32
+ - [ ] **Memory safety** — large files processed via streams, not loaded entirely into memory?
33
+ - [ ] **Output format** — output CSV is well-formed (proper quoting, consistent line endings)?
34
+
35
+ ## Output
36
+
37
+ Provide the transformation code with:
38
+ - Clear input/output format documentation
39
+ - Row count summary (input rows, output rows, filtered rows, error rows)
40
+ - Sample of transformed data for verification
@@ -0,0 +1,44 @@
1
+ # kj-data-report — Data Report Generation
2
+
3
+ Generate structured data reports with clear findings, methodology, and recommendations.
4
+
5
+ ## Your task
6
+
7
+ $ARGUMENTS
8
+
9
+ ## Coder instructions
10
+
11
+ When generating data reports:
12
+
13
+ 1. **Structure the report** — follow this format:
14
+ - **Executive summary** — 2-3 sentences with the key takeaway
15
+ - **Methodology** — data sources, time range, filters applied, tools used
16
+ - **Findings** — organized by theme or priority, each with supporting data
17
+ - **Recommendations** — actionable next steps based on the findings
18
+ 2. **Include data tables** — present data in tables with clear headers, units, and formatting. Align numbers right, text left.
19
+ 3. **Highlight anomalies and outliers** — flag data points that deviate significantly from the norm. Quantify the deviation (e.g., "3x above average", "lowest in 12 months").
20
+ 4. **Quantify everything** — use percentages, counts, averages, medians, and trends. Avoid vague language like "significant increase" without numbers.
21
+ 5. **Show comparisons** — include period-over-period comparisons (MoM, YoY) and benchmarks where available.
22
+ 6. **Cite data sources** — reference the specific dataset, query, or file that produced each finding.
23
+ 7. **Separate facts from interpretation** — clearly distinguish what the data shows from what you conclude.
24
+ 8. **Include limitations** — note any data gaps, sample size issues, or caveats that affect conclusions.
25
+
26
+ ## Reviewer checklist
27
+
28
+ When reviewing data reports, check for:
29
+
30
+ - [ ] **Conclusions match data** — every claim is supported by the data presented? No logical leaps?
31
+ - [ ] **No cherry-picking** — all relevant data included, not just data that supports a narrative?
32
+ - [ ] **Methodology is sound** — data sources are appropriate, filters are justified, time ranges are sensible?
33
+ - [ ] **Numbers add up** — percentages sum correctly, counts are consistent across sections?
34
+ - [ ] **Anomalies explained** — outliers are acknowledged and investigated, not ignored?
35
+ - [ ] **Actionable recommendations** — recommendations are specific, feasible, and tied to findings?
36
+ - [ ] **Clear formatting** — tables are readable, units are specified, dates are unambiguous?
37
+ - [ ] **Limitations disclosed** — caveats and data quality issues are transparently noted?
38
+
39
+ ## Output
40
+
41
+ Deliver the report in the structured format above, with:
42
+ - All data tables properly formatted
43
+ - Key metrics highlighted
44
+ - Clear separation between data, analysis, and recommendations
@@ -0,0 +1,40 @@
1
+ # kj-sql-analysis — SQL Analysis & Generation
2
+
3
+ Generate and review SQL queries with a focus on correctness, performance, and security.
4
+
5
+ ## Your task
6
+
7
+ $ARGUMENTS
8
+
9
+ ## Coder instructions
10
+
11
+ When writing or modifying SQL queries:
12
+
13
+ 1. **Parameterized queries only** — never concatenate user input into SQL strings. Use prepared statements or parameterized queries (`$1`, `?`, `:param`) to prevent injection.
14
+ 2. **Comment complex logic** — add inline comments explaining non-obvious JOINs, subqueries, CTEs, and window functions.
15
+ 3. **Validate against schema** — if a schema or migration files are available, verify that table names, column names, and types match the actual schema.
16
+ 4. **Prefer explicit column lists** — avoid `SELECT *`; list the columns you need.
17
+ 5. **Use aliases consistently** — when joining multiple tables, alias every table and prefix all column references.
18
+ 6. **Index-aware queries** — structure WHERE clauses and JOINs to leverage existing indexes. Avoid wrapping indexed columns in functions.
19
+ 7. **Limit result sets** — add `LIMIT` / `TOP` for queries that could return unbounded rows unless the full result is explicitly needed.
20
+ 8. **Handle NULLs explicitly** — use `COALESCE`, `IS NULL`, or `IS NOT NULL` where NULL values could affect logic.
21
+
22
+ ## Reviewer checklist
23
+
24
+ When reviewing SQL-related code, check for:
25
+
26
+ - [ ] **Injection risk** — any string concatenation or template literals building SQL?
27
+ - [ ] **N+1 queries** — loops executing individual queries instead of a single batch/join?
28
+ - [ ] **Missing indexes** — queries filtering or joining on columns without indexes?
29
+ - [ ] **Correct JOINs** — LEFT vs INNER vs CROSS used intentionally? Accidental cartesian products?
30
+ - [ ] **Transaction safety** — multi-statement operations wrapped in transactions where needed?
31
+ - [ ] **Schema alignment** — column names, types, and constraints match the current schema?
32
+ - [ ] **Performance** — unnecessary subqueries that could be CTEs or JOINs? Missing LIMIT on large tables?
33
+ - [ ] **NULL handling** — aggregations, comparisons, and JOINs account for NULL values?
34
+
35
+ ## Output
36
+
37
+ Provide the SQL queries with:
38
+ - Inline comments for complex sections
39
+ - Explanation of the query plan and expected performance characteristics
40
+ - Any schema changes needed (new indexes, columns, tables)