karajan-code 1.38.0 → 1.38.2

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
@@ -82,7 +82,7 @@ This is the primary use case. Karajan runs as an MCP server inside Claude Code,
82
82
  You → Claude Code → kj_run (via MCP) → triage → coder → sonar → reviewer → tester → security
83
83
  ```
84
84
 
85
- The MCP server auto-registers during `npm install`. Your AI agent sees 20 tools (`kj_run`, `kj_code`, `kj_review`, etc.) and uses them as needed.
85
+ The MCP server auto-registers during `npm install`. Your AI agent sees 21 tools (`kj_run`, `kj_code`, `kj_review`, etc.) and uses them as needed.
86
86
 
87
87
  **The problem**: when Karajan runs inside an AI agent, you lose visibility. The agent shows you the final result, but not the pipeline stages, iterations, or Solomon decisions happening in real time.
88
88
 
@@ -154,7 +154,7 @@ kj-tail --help # Full options
154
154
 
155
155
  ┌─ Terminal 2: kj-tail ────────────────────────────────────────────────────────┐
156
156
  │ │
157
- │ kj-tail v1.37.0 — .kj/run.log │
157
+ │ kj-tail v1.38.1 — .kj/run.log │
158
158
  │ │
159
159
  │ ├─ 📋 Triage: medium (sw) — enabling researcher, architect, planner │
160
160
  │ ├─ ⚙️ Preflight passed — all checks OK │
@@ -205,7 +205,7 @@ hu-reviewer? → triage → discover? → architect? → planner? → coder →
205
205
 
206
206
  | Role | What it does | Default |
207
207
  |------|-------------|---------|
208
- | **hu-reviewer** | Certifies user stories before coding (6 dimensions, 7 antipatterns) | Off |
208
+ | **hu-reviewer** | Certifies user stories before coding (6 dimensions, 7 antipatterns) | Auto (medium/complex) |
209
209
  | **triage** | Classifies complexity, activates roles, auto-simplifies for trivial tasks | **On** |
210
210
  | **discover** | Detects gaps in requirements (Mom Test, Wendel, JTBD) | Off |
211
211
  | **architect** | Designs solution architecture before planning | Off |
@@ -233,7 +233,7 @@ hu-reviewer? → triage → discover? → architect? → planner? → coder →
233
233
 
234
234
  Mix and match. Use Claude as coder and Codex as reviewer. Karajan auto-detects installed agents during `kj init`.
235
235
 
236
- ## MCP server (20 tools)
236
+ ## MCP server (21 tools)
237
237
 
238
238
  After `npm install -g karajan-code`, the MCP server auto-registers in Claude and Codex. Manual config if needed:
239
239
 
@@ -245,7 +245,7 @@ After `npm install -g karajan-code`, the MCP server auto-registers in Claude and
245
245
  # command = "karajan-mcp"
246
246
  ```
247
247
 
248
- **20 tools** available: `kj_run`, `kj_code`, `kj_review`, `kj_plan`, `kj_audit`, `kj_scan`, `kj_doctor`, `kj_config`, `kj_report`, `kj_resume`, `kj_roles`, `kj_agents`, `kj_preflight`, `kj_status`, `kj_init`, `kj_discover`, `kj_triage`, `kj_researcher`, `kj_architect`, `kj_impeccable`.
248
+ **21 tools** available: `kj_run`, `kj_code`, `kj_review`, `kj_plan`, `kj_audit`, `kj_scan`, `kj_doctor`, `kj_config`, `kj_report`, `kj_resume`, `kj_roles`, `kj_agents`, `kj_preflight`, `kj_status`, `kj_init`, `kj_discover`, `kj_triage`, `kj_researcher`, `kj_architect`, `kj_impeccable`, `kj_hu`.
249
249
 
250
250
  Use `kj-tail` in a separate terminal to see what the pipeline is doing in real time (see [Three ways to use Karajan](#three-ways-to-use-karajan)).
251
251
 
@@ -267,13 +267,14 @@ Use `kj roles show <role>` to inspect any template.
267
267
 
268
268
  Karajan auto-detects and auto-configures everything it can:
269
269
 
270
- - **TDD**: Detects test framework (vitest, jest, mocha) → auto-enables TDD
270
+ - **TDD**: Detects test framework for 12 languages (vitest, jest, JUnit, pytest, go test, cargo test, and more). Auto-enables TDD for code tasks, skips for doc/infra
271
271
  - **Bootstrap gate**: Validates all prerequisites (git repo, remote, config, agents, SonarQube) before any tool runs. Fails hard with actionable fix instructions, never silently degrades
272
272
  - **Injection guard**: Scans diffs for prompt injection before AI review. Detects directive overrides, invisible Unicode, oversized comment payloads. Also runs as a GitHub Action on every PR
273
273
  - **SonarQube**: Auto-starts Docker container, generates config if missing
274
274
  - **Pipeline complexity**: Triage classifies task → trivial tasks skip reviewer loop
275
275
  - **Provider outages**: Retries on 500/502/503/504 with backoff (same as rate limits)
276
276
  - **Coverage**: Coverage-only quality gate failures treated as advisory
277
+ - **HU Manager**: Complex tasks auto-decompose into formal user stories with dependencies. Each HU runs as its own sub-pipeline with state tracking visible in the HU Board
277
278
 
278
279
  No per-project configuration required. If you want to customize, config is layered: session > project > global.
279
280
 
@@ -298,7 +299,7 @@ Not nostalgia, not stubbornness. I've been using JavaScript since 1997, when Bre
298
299
  git clone https://github.com/manufosela/karajan-code.git
299
300
  cd karajan-code
300
301
  npm install
301
- npm test # Run 2044 tests with Vitest
302
+ npm test # Run 2142 tests with Vitest
302
303
  npm run validate # Lint + test
303
304
  ```
304
305
 
@@ -314,4 +315,4 @@ Issues and pull requests welcome. If something doesn't work as documented, [open
314
315
 
315
316
  ---
316
317
 
317
- Built by [@manufosela](https://github.com/manufosela). Head of Engineering at Geniova Technologies, co-organizer of NodeJS Madrid, author of [Liderazgo Afectivo](https://www.amazon.es/dp/B0D7F4C8KC). 90+ npm packages published.
318
+ 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.
package/docs/README.es.md CHANGED
@@ -38,6 +38,7 @@ Claude Code es excelente. Usalo para codificacion interactiva basada en sesiones
38
38
 
39
39
  Usa Karajan cuando quieras:
40
40
 
41
+ - **Un pipeline repetible y documentado** que corre igual cada vez
41
42
  - **TDD por defecto.** Los tests se escriben antes de la implementacion, no despues
42
43
  - **Integracion con SonarQube.** Quality gates como parte del flujo, no como algo secundario
43
44
  - **Solomon como jefe del pipeline.** Cada rechazo del reviewer es evaluado por un supervisor que decide si es valido o solo ruido de estilo
@@ -47,7 +48,7 @@ Usa Karajan cuando quieras:
47
48
  - **Local-first.** Tu codigo, tus claves, tu maquina. Ningun dato sale salvo que tu lo digas
48
49
  - **Zero costes de API.** Karajan usa CLIs de agentes de IA (Claude Code, Codex, Gemini CLI), no APIs. Pagas tu suscripcion existente (Claude Pro, ChatGPT Plus), no tarifas por token
49
50
 
50
- Si Claude Code es un programador de pares inteligente, Karajan es el pipeline CI/CD para desarrollo asistido por IA. Funcionan genial juntos: Karajan esta disenado para usarse como servidor MCP dentro de Claude Code.
51
+ Si Claude Code es un programador de pares inteligente, Karajan es el pipeline CI/CD para desarrollo asistido por IA. Funcionan genial juntos: Karajan esta diseñado para usarse como servidor MCP dentro de Claude Code.
51
52
 
52
53
  ## Instalacion
53
54
 
@@ -81,7 +82,7 @@ Este es el caso de uso principal. Karajan corre como servidor MCP dentro de Clau
81
82
  Tu → Claude Code → kj_run (via MCP) → triage → coder → sonar → reviewer → tester → security
82
83
  ```
83
84
 
84
- El servidor MCP se auto-registra durante `npm install`. Tu agente de IA ve 20 herramientas (`kj_run`, `kj_code`, `kj_review`, etc.) y las usa segun necesite.
85
+ El servidor MCP se auto-registra durante `npm install`. Tu agente de IA ve 21 herramientas (`kj_run`, `kj_code`, `kj_review`, `kj_hu`, etc.) y las usa segun necesite.
85
86
 
86
87
  **El problema**: cuando Karajan corre dentro de un agente de IA, pierdes visibilidad. El agente te muestra el resultado final, pero no las etapas del pipeline, iteraciones o decisiones de Solomon en tiempo real.
87
88
 
@@ -106,28 +107,6 @@ kj-tail --help # Todas las opciones
106
107
 
107
108
  > **Importante**: `kj-tail` debe ejecutarse desde el mismo directorio donde el agente de IA esta trabajando. Lee `<proyecto>/.kj/run.log`, que se crea cuando Karajan arranca un pipeline via MCP.
108
109
 
109
- **Flujo tipico:**
110
-
111
- ```
112
- ┌──────────────────────────┐ ┌──────────────────────────┐
113
- │ Terminal 1 │ │ Terminal 2 │
114
- │ │ │ │
115
- │ $ claude │ │ $ kj-tail │
116
- │ > implementa la │ │ │
117
- │ siguiente tarea │ │ ├─ 📋 Triage: medium │
118
- │ prioritaria │ │ ├─ 🔬 Researcher ✅ │
119
- │ │ │ ├─ 🧠 Planner ✅ │
120
- │ (Claude llama a kj_run │ │ ├─ 🔨 Coder ✅ │
121
- │ via MCP, solo ves │ │ ├─ 🔍 Sonar: OK │
122
- │ el resultado final) │ │ ├─ 👁️ Reviewer ❌ │
123
- │ │ │ ├─ ⚖️ Solomon: 2 cond. │
124
- │ │ │ ├─ 🔨 Coder (iter 2) ✅ │
125
- │ │ │ ├─ ✅ Review: APPROVED │
126
- │ │ │ ├─ 🧪 Tester: passed │
127
- │ │ │ └─ 🏁 Result: APPROVED │
128
- └──────────────────────────┘ └──────────────────────────┘
129
- ```
130
-
131
110
  [**Ver la demo completa del pipeline**](https://karajancode.com#demo): triage, arquitectura, TDD, SonarQube, code review, arbitraje de Solomon, auditoria de seguridad.
132
111
 
133
112
  ## El pipeline
@@ -140,10 +119,10 @@ hu-reviewer? → triage → discover? → architect? → planner? → coder →
140
119
 
141
120
  | Rol | Que hace | Por defecto |
142
121
  |-----|----------|-------------|
143
- | **hu-reviewer** | Certifica historias de usuario antes de codificar (6 dimensiones, 7 antipatrones) | Off |
122
+ | **hu-reviewer** | Certifica historias de usuario antes de codificar (6 dimensiones, 7 antipatrones) | Auto (media/compleja) |
144
123
  | **triage** | Clasifica complejidad, activa roles, auto-simplifica para tareas triviales | **On** |
145
124
  | **discover** | Detecta huecos en requisitos (Mom Test, Wendel, JTBD) | Off |
146
- | **architect** | Disena la arquitectura de la solucion antes de planificar | Off |
125
+ | **architect** | Diseña la arquitectura de la solucion antes de planificar | Off |
147
126
  | **planner** | Genera planes de implementacion estructurados | Off |
148
127
  | **coder** | Escribe codigo y tests siguiendo metodologia TDD | **Siempre on** |
149
128
  | **refactorer** | Mejora la claridad del codigo sin cambiar comportamiento | Off |
@@ -168,19 +147,19 @@ hu-reviewer? → triage → discover? → architect? → planner? → coder →
168
147
 
169
148
  Mezcla y combina. Usa Claude como coder y Codex como reviewer. Karajan auto-detecta agentes instalados durante `kj init`.
170
149
 
171
- ## Servidor MCP (20 herramientas)
150
+ ## Servidor MCP (21 herramientas)
172
151
 
173
152
  Tras `npm install -g karajan-code`, el servidor MCP se auto-registra en Claude y Codex. Config manual si es necesario:
174
153
 
175
154
  ```bash
176
- # Claude: anadir a ~/.claude.json → "mcpServers":
155
+ # Claude: añadir a ~/.claude.json → "mcpServers":
177
156
  # { "karajan-mcp": { "command": "karajan-mcp" } }
178
157
 
179
- # Codex: anadir a ~/.codex/config.toml → [mcp_servers."karajan-mcp"]
158
+ # Codex: añadir a ~/.codex/config.toml → [mcp_servers."karajan-mcp"]
180
159
  # command = "karajan-mcp"
181
160
  ```
182
161
 
183
- **20 herramientas** disponibles: `kj_run`, `kj_code`, `kj_review`, `kj_plan`, `kj_audit`, `kj_scan`, `kj_doctor`, `kj_config`, `kj_report`, `kj_resume`, `kj_roles`, `kj_agents`, `kj_preflight`, `kj_status`, `kj_init`, `kj_discover`, `kj_triage`, `kj_researcher`, `kj_architect`, `kj_impeccable`.
162
+ **21 herramientas** disponibles: `kj_run`, `kj_code`, `kj_review`, `kj_plan`, `kj_audit`, `kj_scan`, `kj_doctor`, `kj_config`, `kj_report`, `kj_resume`, `kj_roles`, `kj_agents`, `kj_preflight`, `kj_status`, `kj_init`, `kj_discover`, `kj_triage`, `kj_researcher`, `kj_architect`, `kj_impeccable`, `kj_hu`.
184
163
 
185
164
  Usa `kj-tail` en un terminal separado para ver lo que el pipeline esta haciendo en tiempo real (ver [Tres formas de usar Karajan](#tres-formas-de-usar-karajan)).
186
165
 
@@ -202,21 +181,22 @@ Usa `kj roles show <rol>` para inspeccionar cualquier template.
202
181
 
203
182
  Karajan auto-detecta y auto-configura todo lo que puede:
204
183
 
205
- - **TDD**: Detecta framework de tests (vitest, jest, mocha) y auto-activa TDD
184
+ - **TDD**: Detecta framework de tests para 12 lenguajes (vitest, jest, JUnit, pytest, go test, cargo test, y mas). Auto-activa TDD para tareas de codigo, lo salta para doc/infra
206
185
  - **Bootstrap gate**: Valida todos los prerequisitos (repo git, remote, config, agentes, SonarQube) antes de ejecutar. Falla con instrucciones claras, nunca degrada silenciosamente
207
186
  - **Injection guard**: Escanea diffs en busca de prompt injection antes del review de IA. Detecta directivas de override, Unicode invisible, payloads en comentarios sobredimensionados. Tambien como GitHub Action en cada PR
208
187
  - **SonarQube**: Auto-arranca contenedor Docker, genera config si falta
209
188
  - **Complejidad del pipeline**: Triage clasifica la tarea, las triviales saltan el loop del reviewer
210
189
  - **Caidas de proveedor**: Reintentos en 500/502/503/504 con backoff (igual que rate limits)
211
190
  - **Cobertura**: Fallos de quality gate solo por cobertura se tratan como advisory
191
+ - **HU Manager**: Las tareas complejas se descomponen automaticamente en historias de usuario formales con dependencias. Cada HU se ejecuta como su propio sub-pipeline con seguimiento de estado visible en el HU Board
212
192
 
213
193
  Sin configuracion por proyecto requerida. Si quieres personalizar, la config se apila: sesion > proyecto > global.
214
194
 
215
- ## Por qué JavaScript vanilla?
195
+ ## Por que JavaScript vanilla?
216
196
 
217
- No es nostalgia ni cabezonería. Es que llevo usando JavaScript desde 1997, cuando Brendan Eich lo creó en una semana y nos cambió la vida a los que hacíamos webs. Conozco sus tripas, sus bugs, sus rarezas. Y sé que quien conoce JS de verdad convierte esos bugs en features. TypeScript existe para que developers acostumbrados a lenguajes fuertemente tipados no entren en pánico al ver JS. Respeto eso. Pero yo no lo necesito. Los tests son mi seguridad de tipos. JSDoc y un buen IDE son mi intellisense. Y no tener un compilador entre el código y yo es lo que me permite moverme a 57 releases en 45 días sin miedo.
197
+ No es nostalgia ni cabezoneria. Es que llevo usando JavaScript desde 1997, cuando Brendan Eich lo creo en una semana y nos cambio la vida a los que haciamos webs. Conozco sus tripas, sus bugs, sus rarezas. Y se que quien conoce JS de verdad convierte esos bugs en features. TypeScript existe para que developers acostumbrados a lenguajes fuertemente tipados no entren en panico al ver JS. Respeto eso. Pero yo no lo necesito. Los tests son mi seguridad de tipos. JSDoc y un buen IDE son mi intellisense. Y no tener un compilador entre el codigo y yo es lo que me permite moverme a 57 releases en 45 dias sin miedo.
218
198
 
219
- [Por qué JavaScript vanilla: la versión larga](why-vanilla-js.md)
199
+ [Por que JavaScript vanilla: la version larga](why-vanilla-js.md)
220
200
 
221
201
  ## Compañeros recomendados
222
202
 
@@ -233,7 +213,7 @@ No es nostalgia ni cabezonería. Es que llevo usando JavaScript desde 1997, cuan
233
213
  git clone https://github.com/manufosela/karajan-code.git
234
214
  cd karajan-code
235
215
  npm install
236
- npm test # Ejecutar 2044 tests con Vitest
216
+ npm test # Ejecutar 2142 tests con Vitest
237
217
  npm run validate # Lint + test
238
218
  ```
239
219
 
@@ -249,4 +229,4 @@ Issues y pull requests bienvenidos. Si algo no funciona como esta documentado, [
249
229
 
250
230
  ---
251
231
 
252
- Construido por [@manufosela](https://github.com/manufosela). Head of Engineering en Geniova Technologies, co-organizador de NodeJS Madrid, autor de [Liderazgo Afectivo](https://www.amazon.es/dp/B0D7F4C8KC). 90+ paquetes npm publicados.
232
+ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.38.0",
3
+ "version": "1.38.2",
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",
@@ -13,7 +13,9 @@ export async function codeCommand({ task, config, logger }) {
13
13
  if (config.coder_rules) {
14
14
  try {
15
15
  coderRules = await fs.readFile(config.coder_rules, "utf8");
16
- } catch { /* no coder rules file, that's ok */ }
16
+ } catch {
17
+ try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
18
+ }
17
19
  }
18
20
  const prompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
19
21
  const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
@@ -128,7 +128,7 @@ async function ensureReviewRules(reviewRulesPath, logger) {
128
128
  "# Review Rules\n\n- Focus on security, correctness, and test coverage.\n",
129
129
  "utf8"
130
130
  );
131
- logger.info("Created review-rules.md");
131
+ logger.info("Created .karajan/review-rules.md");
132
132
  }
133
133
 
134
134
  async function ensureCoderRules(coderRulesPath, logger) {
@@ -150,7 +150,7 @@ async function ensureCoderRules(coderRulesPath, logger) {
150
150
  ].join("\n");
151
151
  }
152
152
  await fs.writeFile(coderRulesPath, content, "utf8");
153
- logger.info("Created coder-rules.md");
153
+ logger.info("Created .karajan/coder-rules.md");
154
154
  }
155
155
 
156
156
  async function setupSonarQube(config, logger) {
@@ -271,8 +271,10 @@ export async function initCommand({ logger, flags = {} }) {
271
271
  logger.info(`Ensured ${karajanHome} exists`);
272
272
 
273
273
  const configPath = getConfigPath();
274
- const reviewRulesPath = path.resolve(process.cwd(), "review-rules.md");
275
- const coderRulesPath = path.resolve(process.cwd(), "coder-rules.md");
274
+ const karajanDir = path.join(process.cwd(), ".karajan");
275
+ await ensureDir(karajanDir);
276
+ const reviewRulesPath = path.resolve(karajanDir, "review-rules.md");
277
+ const coderRulesPath = path.resolve(karajanDir, "coder-rules.md");
276
278
 
277
279
  const { config, exists: configExists } = await loadConfig();
278
280
  const interactive = flags.noInteractive !== true && isTTY();
package/src/config.js CHANGED
@@ -39,8 +39,8 @@ const DEFAULTS = {
39
39
  review_mode: "standard",
40
40
  max_iterations: 5,
41
41
  max_budget_usd: null,
42
- review_rules: "./review-rules.md",
43
- coder_rules: "./coder-rules.md",
42
+ review_rules: "./.karajan/review-rules.md",
43
+ coder_rules: "./.karajan/coder-rules.md",
44
44
  base_branch: "main",
45
45
  coder_options: { model: null, auto_approve: true, fallback_coder: null },
46
46
  reviewer_options: {
@@ -10,13 +10,23 @@ const DESTRUCTIVE_PATTERNS = [
10
10
  { id: "format-disk", pattern: /mkfs\.|fdisk|dd\s+if=/, severity: "critical", message: "Disk format operation detected" },
11
11
  ];
12
12
 
13
- // Built-in credential patterns
13
+ // Built-in credential patterns (ALL critical — secrets must NEVER be committed)
14
14
  const CREDENTIAL_PATTERNS = [
15
15
  { id: "aws-key", pattern: /AKIA[0-9A-Z]{16}/, severity: "critical", message: "AWS access key exposed" },
16
16
  { id: "private-key", pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, severity: "critical", message: "Private key exposed" },
17
- { id: "generic-secret", pattern: /(password|secret|token|api_key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i, severity: "warning", message: "Potential secret/credential exposed" },
17
+ { id: "generic-secret", pattern: /(password|secret|token|api_key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i, severity: "critical", message: "Hardcoded secret detected. Use .env file + process.env instead" },
18
18
  { id: "github-token", pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, severity: "critical", message: "GitHub token exposed" },
19
19
  { id: "npm-token", pattern: /npm_[A-Za-z0-9]{36,}/, severity: "critical", message: "npm token exposed" },
20
+ { id: "openai-key", pattern: /sk-[A-Za-z0-9]{20,}/, severity: "critical", message: "OpenAI API key exposed" },
21
+ { id: "anthropic-key", pattern: /sk-ant-[A-Za-z0-9_-]{20,}/, severity: "critical", message: "Anthropic API key exposed" },
22
+ { id: "stripe-key", pattern: /sk_live_[A-Za-z0-9]{20,}/, severity: "critical", message: "Stripe secret key exposed" },
23
+ { id: "stripe-test", pattern: /sk_test_[A-Za-z0-9]{20,}/, severity: "critical", message: "Stripe test key exposed. Use .env even for test keys" },
24
+ { id: "google-api-key", pattern: /AIza[A-Za-z0-9_-]{35}/, severity: "critical", message: "Google API key exposed" },
25
+ { id: "firebase-key", pattern: /"apiKey"\s*:\s*"AIza[A-Za-z0-9_-]{35}"/, severity: "critical", message: "Firebase API key hardcoded. Use .env file" },
26
+ { id: "slack-token", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/, severity: "critical", message: "Slack token exposed" },
27
+ { id: "jwt-secret", pattern: /jwt[_-]?secret\s*[:=]\s*["'][^"']{8,}["']/i, severity: "critical", message: "JWT secret hardcoded. Use .env file" },
28
+ { id: "database-url", pattern: /(mongodb|postgres|mysql|redis):\/\/[^"'\s]+:[^"'\s]+@/i, severity: "critical", message: "Database URL with credentials exposed. Use .env file" },
29
+ { id: "hardcoded-key-assignment", pattern: /(?:const|let|var)\s+\w*(?:key|secret|token|password)\w*\s*=\s*["'][A-Za-z0-9_-]{16,}["']/i, severity: "critical", message: "Hardcoded key in variable. Use process.env or .env file" },
20
30
  ];
21
31
 
22
32
  // Default protected files (block if these appear in added/modified lines)
package/src/hu/store.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { execSync } from "node:child_process";
3
4
  import { getKarajanHome } from "../utils/paths.js";
4
5
 
5
6
  // FUTURE: hu-storage adapter for PG/Trello/etc — currently local files only
@@ -208,3 +209,138 @@ export async function createHistoryRecord(sessionId, { task, result, approved, s
208
209
  await fs.writeFile(path.join(dir, "batch.json"), JSON.stringify(batch, null, 2));
209
210
  return batch;
210
211
  }
212
+
213
+ /* ── Manual HU management ─────────────────────────────────────────── */
214
+
215
+ /**
216
+ * Detect project info from the given directory.
217
+ * @param {string} cwd - Directory to detect project from.
218
+ * @returns {{ name: string, remoteUrl: string|null }}
219
+ */
220
+ export function detectProject(cwd) {
221
+ const name = path.basename(cwd);
222
+ let remoteUrl = null;
223
+ try {
224
+ remoteUrl = execSync("git remote get-url origin", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
225
+ } catch { /* not a git repo or no remote */ }
226
+ return { name, remoteUrl };
227
+ }
228
+
229
+ /**
230
+ * Derive a project slug from a directory path.
231
+ * @param {string} projectDir - Absolute path to the project.
232
+ * @returns {string}
233
+ */
234
+ function projectSlug(projectDir) {
235
+ return path.basename(projectDir);
236
+ }
237
+
238
+ /**
239
+ * Get the batch file path for a project.
240
+ * @param {string} projectDir
241
+ * @returns {string}
242
+ */
243
+ function projectBatchPath(projectDir) {
244
+ return path.join(getHuDir(), projectSlug(projectDir), "batch.json");
245
+ }
246
+
247
+ /**
248
+ * Load the project batch, or return a fresh empty batch if none exists.
249
+ * @param {string} projectDir
250
+ * @returns {Promise<object>}
251
+ */
252
+ async function loadProjectBatch(projectDir) {
253
+ const filePath = projectBatchPath(projectDir);
254
+ try {
255
+ const raw = await fs.readFile(filePath, "utf8");
256
+ return JSON.parse(raw);
257
+ } catch {
258
+ return {
259
+ project: projectSlug(projectDir),
260
+ created_at: new Date().toISOString(),
261
+ stories: []
262
+ };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Save a project batch to disk (auto-creates directory).
268
+ * @param {string} projectDir
269
+ * @param {object} batch
270
+ */
271
+ async function saveProjectBatch(projectDir, batch) {
272
+ const filePath = projectBatchPath(projectDir);
273
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
274
+ batch.updated_at = new Date().toISOString();
275
+ await fs.writeFile(filePath, JSON.stringify(batch, null, 2));
276
+ }
277
+
278
+ /**
279
+ * Create a manual HU (user story) for a project.
280
+ * Auto-creates the batch file if it doesn't exist.
281
+ * @param {string} projectDir
282
+ * @param {{ title: string, description?: string, status?: string, acceptanceCriteria?: string }} data
283
+ * @returns {Promise<object>} The created HU.
284
+ */
285
+ export async function createManualHu(projectDir, { title, description, status, acceptanceCriteria }) {
286
+ if (!title) throw new Error("title is required to create a HU");
287
+ const batch = await loadProjectBatch(projectDir);
288
+ const now = new Date().toISOString();
289
+ const hu = {
290
+ id: `HU-${Date.now()}-${batch.stories.length}`,
291
+ title,
292
+ description: description || "",
293
+ status: status || HU_STATUS.PENDING,
294
+ acceptanceCriteria: acceptanceCriteria || "",
295
+ createdAt: now,
296
+ updatedAt: now
297
+ };
298
+ batch.stories.push(hu);
299
+ await saveProjectBatch(projectDir, batch);
300
+ return hu;
301
+ }
302
+
303
+ /**
304
+ * List all HUs for a project.
305
+ * @param {string} projectDir
306
+ * @returns {Promise<Array<{ id: string, title: string, status: string, createdAt: string }>>}
307
+ */
308
+ export async function listHus(projectDir) {
309
+ const batch = await loadProjectBatch(projectDir);
310
+ return batch.stories.map(s => ({
311
+ id: s.id,
312
+ title: s.title,
313
+ status: s.status,
314
+ createdAt: s.createdAt
315
+ }));
316
+ }
317
+
318
+ /**
319
+ * Update the status of a specific HU.
320
+ * @param {string} projectDir
321
+ * @param {string} huId
322
+ * @param {string} status
323
+ * @returns {Promise<object>} The updated HU.
324
+ */
325
+ export async function updateHuStatus(projectDir, huId, status) {
326
+ const batch = await loadProjectBatch(projectDir);
327
+ const hu = batch.stories.find(s => s.id === huId);
328
+ if (!hu) throw new Error(`HU ${huId} not found`);
329
+ hu.status = status;
330
+ hu.updatedAt = new Date().toISOString();
331
+ await saveProjectBatch(projectDir, batch);
332
+ return hu;
333
+ }
334
+
335
+ /**
336
+ * Get a single HU by id.
337
+ * @param {string} projectDir
338
+ * @param {string} huId
339
+ * @returns {Promise<object>}
340
+ */
341
+ export async function getHu(projectDir, huId) {
342
+ const batch = await loadProjectBatch(projectDir);
343
+ const hu = batch.stories.find(s => s.id === huId);
344
+ if (!hu) throw new Error(`HU ${huId} not found`);
345
+ return hu;
346
+ }
@@ -482,7 +482,9 @@ export async function handleCodeDirect(a, server, extra) {
482
482
  if (config.coder_rules) {
483
483
  try {
484
484
  coderRules = await fs.readFile(config.coder_rules, "utf8");
485
- } catch { /* no coder rules file */ }
485
+ } catch {
486
+ try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
487
+ }
486
488
  }
487
489
  const prompt = buildCoderPrompt({ task: a.task, coderRules, methodology: config.development?.methodology || "tdd" });
488
490
  sendTrackerLog(server, "coder", "running", coderRole.provider);
@@ -904,6 +906,44 @@ async function handleAudit(a, server, extra) {
904
906
  return handleAuditDirect(a, server, extra);
905
907
  }
906
908
 
909
+ async function handleHu(a, server) {
910
+ const action = a.action;
911
+ if (!action) return failPayload("Missing required field: action");
912
+
913
+ const projectDir = await resolveProjectDir(server, a.projectDir);
914
+ const { createManualHu, listHus, getHu, updateHuStatus } = await import("../hu/store.js");
915
+
916
+ switch (action) {
917
+ case "create": {
918
+ if (!a.title) return failPayload("Missing required field: title (required for create)");
919
+ const hu = await createManualHu(projectDir, {
920
+ title: a.title,
921
+ description: a.description,
922
+ status: a.status,
923
+ acceptanceCriteria: a.acceptanceCriteria
924
+ });
925
+ return { ok: true, hu };
926
+ }
927
+ case "list": {
928
+ const hus = await listHus(projectDir);
929
+ return { ok: true, hus };
930
+ }
931
+ case "get": {
932
+ if (!a.huId) return failPayload("Missing required field: huId (required for get)");
933
+ const hu = await getHu(projectDir, a.huId);
934
+ return { ok: true, hu };
935
+ }
936
+ case "update": {
937
+ if (!a.huId) return failPayload("Missing required field: huId (required for update)");
938
+ if (!a.status) return failPayload("Missing required field: status (required for update)");
939
+ const hu = await updateHuStatus(projectDir, a.huId, a.status);
940
+ return { ok: true, hu };
941
+ }
942
+ default:
943
+ return failPayload(`Unknown hu action: ${action}`);
944
+ }
945
+ }
946
+
907
947
  async function handleBoard(a) {
908
948
  const action = a.action || "status";
909
949
  const { loadConfig: lc } = await import("../config.js");
@@ -945,7 +985,8 @@ const toolHandlers = {
945
985
  kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),
946
986
  kj_architect: (a, server, extra) => handleArchitect(a, server, extra),
947
987
  kj_audit: (a, server, extra) => handleAudit(a, server, extra),
948
- kj_board: (a) => handleBoard(a)
988
+ kj_board: (a) => handleBoard(a),
989
+ kj_hu: (a, server) => handleHu(a, server)
949
990
  };
950
991
 
951
992
  export async function handleToolCall(name, args, server, extra) {
package/src/mcp/tools.js CHANGED
@@ -47,7 +47,7 @@ export const tools = [
47
47
  {
48
48
  name: "kj_run",
49
49
  description:
50
- "Run full coder -> sonar -> reviewer loop. Sends real-time progress notifications via MCP logging. May return paused state with a question if fail-fast is triggered.",
50
+ "Run the full Karajan pipeline. IMPORTANT: Pass the user's task description exactly as they wrote it. Do NOT group, split, reorder, or modify tasks yourself. Karajan handles decomposition, role activation, iteration, and quality gates internally. Do NOT override pipeline parameters (enableHuReviewer, mode, methodology) unless the user explicitly requested it. If you have observations about the task, use kj_suggest instead.",
51
51
  inputSchema: {
52
52
  type: "object",
53
53
  required: ["task"],
@@ -182,7 +182,7 @@ export const tools = [
182
182
  },
183
183
  {
184
184
  name: "kj_code",
185
- description: "Run coder-only mode",
185
+ description: "Run coder-only mode. Pass the user's task exactly as described. Do NOT split or reinterpret the task.",
186
186
  inputSchema: {
187
187
  type: "object",
188
188
  required: ["task"],
@@ -197,7 +197,7 @@ export const tools = [
197
197
  },
198
198
  {
199
199
  name: "kj_review",
200
- description: "Run reviewer-only mode against current diff",
200
+ description: "Run reviewer-only mode against current diff. Do NOT filter or pre-process the diff before passing it.",
201
201
  inputSchema: {
202
202
  type: "object",
203
203
  required: ["task"],
@@ -223,7 +223,7 @@ export const tools = [
223
223
  },
224
224
  {
225
225
  name: "kj_plan",
226
- description: "Generate implementation plan for a task",
226
+ description: "Generate implementation plan for a task. Pass the task as the user described it. Karajan's planner decides the approach.",
227
227
  inputSchema: {
228
228
  type: "object",
229
229
  required: ["task"],
@@ -317,5 +317,22 @@ export const tools = [
317
317
  port: { type: "number", description: "Port (default: 4000)" }
318
318
  }
319
319
  }
320
+ },
321
+ {
322
+ name: "kj_hu",
323
+ description: "Manage user stories (HUs) in the local board",
324
+ inputSchema: {
325
+ type: "object",
326
+ required: ["action"],
327
+ properties: {
328
+ action: { type: "string", enum: ["create", "update", "list", "get"], description: "Action to perform" },
329
+ title: { type: "string", description: "HU title (required for create)" },
330
+ description: { type: "string", description: "HU description" },
331
+ acceptanceCriteria: { type: "string", description: "Acceptance criteria" },
332
+ huId: { type: "string", description: "HU ID (required for update/get)" },
333
+ status: { type: "string", description: "HU status (for create/update)", enum: ["pending", "coding", "reviewing", "done", "failed", "blocked"] },
334
+ projectDir: { type: "string", description: "Project directory (defaults to cwd)" }
335
+ }
336
+ }
320
337
  }
321
338
  ];
@@ -444,15 +444,20 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
444
444
  const sonarResult = sonarOutput.result;
445
445
 
446
446
  if (!sonarResult.gateStatus && sonarResult.error) {
447
+ const isTokenError = /unable to resolve sonar token/i.test(sonarResult.error);
448
+ const errorMessage = isTokenError
449
+ ? "SonarQube is running but no authentication token is configured. Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml."
450
+ : `Sonar scan failed: ${sonarResult.error}`;
451
+
447
452
  await markSessionStatus(session, "failed");
448
453
  emitProgress(
449
454
  emitter,
450
455
  makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
451
456
  status: "fail",
452
- message: `Sonar scan failed: ${sonarResult.error}`
457
+ message: errorMessage
453
458
  })
454
459
  );
455
- throw new Error(`Sonar scan failed: ${sonarResult.error}`);
460
+ throw new Error(errorMessage);
456
461
  }
457
462
 
458
463
  session.last_sonar_summary = sonarOutput.summary;
@@ -680,7 +685,7 @@ async function fetchReviewDiff(session, logger) {
680
685
  logger.info(`Reviewer reading PR diff #${session.becaria_pr_number}`);
681
686
  return diff;
682
687
  }
683
- return generateDiff({ baseRef: session.session_start_sha });
688
+ return generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true });
684
689
  }
685
690
 
686
691
  export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
@@ -252,8 +252,8 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
252
252
  result.ok = false;
253
253
  result.errors.push({
254
254
  check: "sonar-auth",
255
- message: "SonarQube authentication failed.",
256
- fix: "Regenerate the SonarQube token and update it via kj_init or in kj.config.yml under sonarqube.token."
255
+ message: "SonarQube is running but no authentication token is configured.",
256
+ fix: "Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml."
257
257
  });
258
258
  logger.error("Preflight: Sonar auth failed");
259
259
  }
@@ -2,6 +2,68 @@ import { SolomonRole } from "../roles/solomon-role.js";
2
2
  import { addCheckpoint, pauseSession } from "../session-store.js";
3
3
  import { emitProgress, makeEvent } from "../utils/events.js";
4
4
 
5
+ /**
6
+ * Build a human-readable escalation message from raw Solomon conflict data.
7
+ * Replaces the raw JSON dump with structured, plain-text output.
8
+ *
9
+ * @param {object} solomonResult - The conflict object passed to escalateToHuman
10
+ * @param {object} [session] - Current session (optional, used for extra context)
11
+ * @returns {string} Formatted message suitable for display to the user
12
+ */
13
+ export function formatEscalationMessage(solomonResult, session) {
14
+ const stage = solomonResult?.stage || "unknown";
15
+ const solomonReason = solomonResult?.solomonReason || null;
16
+ const history = solomonResult?.history || [];
17
+
18
+ const lines = [];
19
+ lines.push(`--- Conflict: ${stage} ---`);
20
+ lines.push("");
21
+
22
+ // Extract reviewer/agent feedback from history
23
+ const feedbackEntries = history.filter(entry => entry && typeof entry === "object");
24
+ if (feedbackEntries.length > 0) {
25
+ lines.push("Reviewer feedback:");
26
+ for (const entry of feedbackEntries) {
27
+ const agent = entry.agent || "unknown";
28
+ const feedback = entry.feedback || "No feedback provided";
29
+ // Split multi-line feedback (e.g. "R-1: ...\nR-2: ...") into bullet points
30
+ const feedbackLines = String(feedback).split("\n").filter(l => l.trim());
31
+ if (feedbackLines.length === 1) {
32
+ lines.push(` [${agent}] ${feedbackLines[0]}`);
33
+ } else {
34
+ lines.push(` [${agent}]`);
35
+ for (const fl of feedbackLines) {
36
+ lines.push(` - ${fl}`);
37
+ }
38
+ }
39
+ }
40
+ lines.push("");
41
+ }
42
+
43
+ // Solomon's reason (if it tried and failed)
44
+ if (solomonReason) {
45
+ lines.push(`Solomon could not resolve the conflict: ${solomonReason}`);
46
+ lines.push("");
47
+ }
48
+
49
+ // Iteration context
50
+ const iterationCount = solomonResult?.iterationCount;
51
+ const maxIterations = solomonResult?.maxIterations;
52
+ if (iterationCount !== undefined && maxIterations !== undefined) {
53
+ lines.push(`Iterations: ${iterationCount}/${maxIterations}`);
54
+ lines.push("");
55
+ }
56
+
57
+ lines.push("Options:");
58
+ lines.push(" 1. Accept coder's work as-is");
59
+ lines.push(" 2. Retry with reviewer's feedback");
60
+ lines.push(" 3. Stop the session");
61
+ lines.push("");
62
+ lines.push("How should we proceed?");
63
+
64
+ return lines.join("\n");
65
+ }
66
+
5
67
  export async function invokeSolomon({ config, logger, emitter, eventBase, stage, conflict, askQuestion, session, iteration }) {
6
68
  const solomonEnabled = Boolean(config.pipeline?.solomon?.enabled);
7
69
 
@@ -86,8 +148,7 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
86
148
  }
87
149
 
88
150
  export async function escalateToHuman({ askQuestion, session, emitter, eventBase, stage, conflict, iteration }) {
89
- const reason = conflict?.solomonReason || `${stage} conflict unresolved`;
90
- const question = `${stage} conflict requires human intervention: ${reason}\nDetails: ${JSON.stringify(conflict?.history?.slice(-2) || [], null, 2)}\n\nHow should we proceed?`;
151
+ const question = formatEscalationMessage({ ...conflict, stage }, session);
91
152
 
92
153
  if (askQuestion) {
93
154
  const answer = await askQuestion(question, { iteration, stage });
@@ -13,8 +13,12 @@ export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
13
13
  return mergeBase.stdout.trim();
14
14
  }
15
15
 
16
- export async function generateDiff({ baseRef }) {
17
- const result = await runCommand("git", ["diff", `${baseRef}`]);
16
+ export async function generateDiff({ baseRef, stageNewFiles = false }) {
17
+ // Stage untracked files so they appear in the diff (coder creates files but doesn't git add)
18
+ if (stageNewFiles) {
19
+ await runCommand("git", ["add", "-A"]);
20
+ }
21
+ const result = await runCommand("git", ["diff", stageNewFiles ? "--cached" : "", `${baseRef}`].filter(Boolean));
18
22
  if (result.exitCode !== 0) {
19
23
  throw new Error(`git diff failed: ${result.stderr || result.stdout}`);
20
24
  }
@@ -21,15 +21,17 @@ function isSourceFile(file, extensions = []) {
21
21
 
22
22
  const SKIP_TDD_TASK_TYPES = new Set(["doc", "infra"]);
23
23
 
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
+ const DEFAULT_SOURCE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".kt", ".rb", ".php", ".cs", ".rs", ".swift", ".dart"];
26
+
24
27
  export function evaluateTddPolicy(diff, developmentConfig = {}, untrackedFiles = [], taskType = null) {
25
28
  if (taskType && SKIP_TDD_TASK_TYPES.has(taskType)) {
26
29
  return { ok: true, reason: "tdd_not_applicable_for_task_type", sourceFiles: [], testFiles: [] };
27
30
  }
28
31
 
29
32
  const requireTestChanges = developmentConfig.require_test_changes !== false;
30
- const patterns = developmentConfig.test_file_patterns || ["/tests/", "/__tests__/", ".test.", ".spec."];
31
- const extensions =
32
- developmentConfig.source_file_extensions || [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".rb", ".php", ".cs"];
33
+ const patterns = developmentConfig.test_file_patterns || DEFAULT_TEST_PATTERNS;
34
+ const extensions = developmentConfig.source_file_extensions || DEFAULT_SOURCE_EXTENSIONS;
33
35
 
34
36
  const diffFiles = extractChangedFiles(diff);
35
37
  const extra = Array.isArray(untrackedFiles) ? untrackedFiles : [];
@@ -225,7 +225,7 @@ export async function runSonarScan(config, projectKey = null) {
225
225
  ok: false,
226
226
  stdout: "",
227
227
  stderr:
228
- "Unable to resolve Sonar token. Set KJ_SONAR_TOKEN env var, configure sonarqube.token in kj.config.yml, or save credentials in ~/.karajan/sonar-credentials.json.",
228
+ "Unable to resolve Sonar token. Fix: (1) run 'kj init' to configure it, (2) set KJ_SONAR_TOKEN env var, or (3) add sonarqube.token to ~/.karajan/kj.config.yml.",
229
229
  exitCode: 1
230
230
  };
231
231
  }
@@ -452,7 +452,11 @@ const EVENT_HANDLERS = {
452
452
  question: (event, icon) => {
453
453
  console.log();
454
454
  console.log(`${ANSI.bold}${ANSI.yellow}${icon} Paused - question:${ANSI.reset}`);
455
- console.log(` ${event.detail?.question || event.message}`);
455
+ const questionText = event.detail?.question || event.message || "";
456
+ const questionLines = questionText.split("\n");
457
+ for (const line of questionLines) {
458
+ console.log(` ${line}`);
459
+ }
456
460
  console.log(`${ANSI.dim}Resume with: kj resume ${event.sessionId} --answer "<response>"${ANSI.reset}`);
457
461
  },
458
462
 
@@ -1,15 +1,62 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- const TEST_FRAMEWORKS = ["vitest", "jest", "@jest/core", "mocha", "ava", "tap", "playwright", "@playwright/test"];
5
-
6
- const CONFIG_FILES = [
4
+ // --- JS/TS ---
5
+ const JS_FRAMEWORKS = ["vitest", "jest", "@jest/core", "mocha", "ava", "tap", "playwright", "@playwright/test"];
6
+ const JS_CONFIG_FILES = [
7
7
  "vitest.config.js", "vitest.config.ts",
8
8
  "jest.config.js", "jest.config.ts", "jest.config.mjs",
9
9
  ".mocharc.yml", ".mocharc.json",
10
10
  "playwright.config.js", "playwright.config.ts"
11
11
  ];
12
12
 
13
+ // --- Multi-language project markers ---
14
+ const LANGUAGE_MARKERS = [
15
+ // Java / Kotlin (Maven)
16
+ { file: "pom.xml", framework: "junit", language: "java" },
17
+ // Java / Kotlin (Gradle)
18
+ { file: "build.gradle", framework: "junit", language: "java" },
19
+ { file: "build.gradle.kts", framework: "junit", language: "kotlin" },
20
+ // Python
21
+ { file: "pytest.ini", framework: "pytest", language: "python" },
22
+ { file: "pyproject.toml", framework: "pytest", language: "python" },
23
+ { file: "setup.py", framework: "pytest", language: "python" },
24
+ { file: "setup.cfg", framework: "pytest", language: "python" },
25
+ { file: "tox.ini", framework: "pytest", language: "python" },
26
+ // Go
27
+ { file: "go.mod", framework: "go-test", language: "go" },
28
+ // Rust
29
+ { file: "Cargo.toml", framework: "cargo-test", language: "rust" },
30
+ // C# / .NET
31
+ { file: "*.csproj", glob: true, framework: "dotnet-test", language: "csharp" },
32
+ { file: "*.sln", glob: true, framework: "dotnet-test", language: "csharp" },
33
+ // Ruby
34
+ { file: "Gemfile", framework: "rspec", language: "ruby" },
35
+ { file: ".rspec", framework: "rspec", language: "ruby" },
36
+ // PHP
37
+ { file: "phpunit.xml", framework: "phpunit", language: "php" },
38
+ { file: "phpunit.xml.dist", framework: "phpunit", language: "php" },
39
+ // Swift
40
+ { file: "Package.swift", framework: "xctest", language: "swift" },
41
+ // Dart / Flutter
42
+ { file: "pubspec.yaml", framework: "dart-test", language: "dart" },
43
+ ];
44
+
45
+ /** Test file patterns per language (used by TDD policy). */
46
+ export const TEST_PATTERNS_BY_LANGUAGE = {
47
+ javascript: ["/tests/", "/__tests__/", ".test.", ".spec."],
48
+ java: ["/src/test/", "/test/", "Test.java", "Tests.java", "IT.java"],
49
+ kotlin: ["/src/test/", "/test/", "Test.kt", "Tests.kt", "/src/androidTest/"],
50
+ python: ["/tests/", "/test/", "test_", "_test.py", "tests.py"],
51
+ go: ["_test.go"],
52
+ rust: ["/tests/", "#[test]", "#[cfg(test)]"],
53
+ csharp: ["/Tests/", "/Test/", "Tests.cs", "Test.cs", ".Tests/"],
54
+ ruby: ["/spec/", "/test/", "_spec.rb", "_test.rb"],
55
+ php: ["/tests/", "/test/", "Test.php"],
56
+ swift: ["/Tests/", "Tests.swift"],
57
+ dart: ["/test/", "_test.dart"],
58
+ };
59
+
13
60
  function frameworkFromConfig(filename) {
14
61
  if (filename.startsWith("vitest")) return "vitest";
15
62
  if (filename.startsWith("jest")) return "jest";
@@ -19,32 +66,49 @@ function frameworkFromConfig(filename) {
19
66
 
20
67
  /**
21
68
  * Detect if the project has a test framework configured.
22
- * Checks: package.json devDependencies/dependencies for known test frameworks.
23
- * Also checks for config files (vitest.config, jest.config, .mocharc, playwright.config).
69
+ * Checks JS (package.json), then multi-language project markers.
24
70
  * @param {string} cwd - Project root
25
- * @returns {Promise<{hasTests: boolean, framework: string|null}>}
71
+ * @returns {Promise<{hasTests: boolean, framework: string|null, language: string|null}>}
26
72
  */
27
73
  export async function detectTestFramework(cwd = process.cwd()) {
74
+ // 1. JS/TS: check package.json
28
75
  try {
29
76
  const pkgRaw = await fs.readFile(path.join(cwd, "package.json"), "utf8");
30
77
  const pkg = JSON.parse(pkgRaw);
31
78
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
32
79
 
33
- for (const fw of TEST_FRAMEWORKS) {
80
+ for (const fw of JS_FRAMEWORKS) {
34
81
  if (allDeps[fw]) {
35
- return { hasTests: true, framework: fw };
82
+ return { hasTests: true, framework: fw, language: "javascript" };
36
83
  }
37
84
  }
38
85
  } catch { /* no package.json or parse error */ }
39
86
 
40
- for (const cf of CONFIG_FILES) {
87
+ // 2. JS/TS: check config files
88
+ for (const cf of JS_CONFIG_FILES) {
41
89
  try {
42
90
  await fs.access(path.join(cwd, cf));
43
- return { hasTests: true, framework: frameworkFromConfig(cf) };
91
+ return { hasTests: true, framework: frameworkFromConfig(cf), language: "javascript" };
92
+ } catch { /* not found */ }
93
+ }
94
+
95
+ // 3. Multi-language: check project markers
96
+ for (const marker of LANGUAGE_MARKERS) {
97
+ try {
98
+ if (marker.glob) {
99
+ const entries = await fs.readdir(cwd);
100
+ const ext = marker.file.replace("*", "");
101
+ if (entries.some((e) => e.endsWith(ext))) {
102
+ return { hasTests: true, framework: marker.framework, language: marker.language };
103
+ }
104
+ } else {
105
+ await fs.access(path.join(cwd, marker.file));
106
+ return { hasTests: true, framework: marker.framework, language: marker.language };
107
+ }
44
108
  } catch { /* not found */ }
45
109
  }
46
110
 
47
- return { hasTests: false, framework: null };
111
+ return { hasTests: false, framework: null, language: null };
48
112
  }
49
113
 
50
114
  /**
@@ -28,6 +28,15 @@ Before reporting done, verify that ALL parts of the task are addressed:
28
28
  - If you write tests first (TDD), the implementation MUST make those tests pass.
29
29
  - Do NOT commit code that doesn't compile or doesn't pass tests.
30
30
 
31
+ ## Secrets and environment variables
32
+
33
+ - NEVER hardcode API keys, tokens, passwords, secrets, or credentials in source code. No exceptions, not even for public keys or test keys.
34
+ - ALWAYS use environment variables via `process.env` (Node.js), `os.environ` (Python), or the equivalent for the project's language.
35
+ - If the project needs API keys, create a `.env.example` file with placeholder values and add `.env` to `.gitignore`.
36
+ - If `.gitignore` does not already include `.env`, add it.
37
+ - If the task requires Firebase, Stripe, OpenAI, or any third-party API: use `.env` for the keys and document required variables in `.env.example` or README.
38
+ - Database connection strings with credentials MUST use environment variables.
39
+
31
40
  ## File modification safety
32
41
 
33
42
  - NEVER overwrite existing files entirely. Always make targeted, minimal edits.