karajan-code 4.34.0 → 4.35.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.34.0",
3
+ "version": "4.35.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",
@@ -34,6 +34,28 @@ export class BaseAgent {
34
34
  this.environment = environment;
35
35
  }
36
36
 
37
+ /**
38
+ * KJC-BUG-0217 (issue #1409) — los flags con los que kj invoca el CLI de un
39
+ * provider eran una constante del codigo, y hay versiones que rechazan la
40
+ * combinacion: kimi-code 0.27.0 responde "Cannot combine --prompt with
41
+ * --yolo" y dejaba `kj agent run` inutilizable, sin mas salida que parchear
42
+ * kj. Ahora el proyecto puede declararlos:
43
+ *
44
+ * agents: { kimi: { task_args: [], review_args: [] } }
45
+ *
46
+ * Sin declaracion valen los de siempre. Una declaracion que no sea una lista
47
+ * de cadenas se ignora: la config no puede romper la invocacion.
48
+ *
49
+ * @param {"task"|"review"} kind
50
+ * @param {string[]} fallback
51
+ * @returns {string[]}
52
+ */
53
+ declaredArgs(kind, fallback) {
54
+ const declared = this.config?.agents?.[this.name]?.[`${kind}_args`];
55
+ if (!Array.isArray(declared) || declared.some((a) => typeof a !== "string")) return fallback;
56
+ return declared;
57
+ }
58
+
37
59
  /**
38
60
  * Run a shell command through the injected CommandRunner. Subclasses should
39
61
  * call this instead of importing runCommand directly so tests can swap in
@@ -1,6 +1,9 @@
1
1
  import { BaseAgent } from "./base-agent.js";
2
2
  import { resolveBin } from "./resolve-bin.js";
3
3
 
4
+ /** El rechazo de kimi-code 0.27.0 a `-p` junto con `-y`, en su forma literal. */
5
+ const REJECTS_COMBINATION = /cannot combine .* with /i;
6
+
4
7
  /**
5
8
  * Kimi Code (Moonshot, binary `kimi`) — KJC-TSK-0729 fase 2: the eighth
6
9
  * built-in agent and the free-tier reserve of the reviewer/solomon panel.
@@ -17,7 +20,7 @@ import { resolveBin } from "./resolve-bin.js";
17
20
  export class KimiAgent extends BaseAgent {
18
21
  async runTask(task) {
19
22
  // Coder mode IS an agentic run: file edits and shell need approval.
20
- return this._exec(task, this.getRoleModel(task.role || "coder"), ["-y"]);
23
+ return this._exec(task, this.getRoleModel(task.role || "coder"), this.declaredArgs("task", ["-y"]));
21
24
  }
22
25
 
23
26
  async reviewTask(task) {
@@ -27,7 +30,7 @@ export class KimiAgent extends BaseAgent {
27
30
  return this._exec(
28
31
  { ...task, prompt: `Do NOT use any tools. Answer directly from this prompt only.\n\n${task.prompt}` },
29
32
  this.getRoleModel(task.role || "reviewer"),
30
- [],
33
+ this.declaredArgs("review", []),
31
34
  );
32
35
  }
33
36
 
@@ -40,6 +43,12 @@ export class KimiAgent extends BaseAgent {
40
43
  timeout: task.timeoutMs,
41
44
  cwd: task.cwd,
42
45
  });
43
- return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
46
+ // KJC-BUG-0217 (issue #1409): el CLI puede rechazar la COMBINACION de flags
47
+ // (kimi-code 0.27.0: "Cannot combine --prompt with --yolo"). Ese error crudo
48
+ // no dice que hacer; el de kj si.
49
+ const error = REJECTS_COMBINATION.test(String(res.stderr || ""))
50
+ ? `${res.stderr}\n\nkimi rechaza la combinacion de flags con la que kj lo invoca. Declara los que acepta esta version: agents.kimi.task_args (una lista vacia deja el prompt solo, sin auto-aprobado).`
51
+ : res.stderr;
52
+ return { ok: res.exitCode === 0, output: res.stdout, error, exitCode: res.exitCode };
44
53
  }
45
54
  }
@@ -11,7 +11,7 @@ import { identityCommand } from "../commands/identity.js";
11
11
  import { policyCommand } from "../commands/policy.js";
12
12
  import { claimsCommand, claimsGateCommand } from "../commands/claims.js";
13
13
  import { stewardSweepCommand } from "../commands/steward.js";
14
- import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand } from "../commands/rag.js";
14
+ import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand, ragCoversCommand } from "../commands/rag.js";
15
15
  import { qmdQueryCommand } from "../commands/qmd.js";
16
16
  import { ragMcpCommand } from "../commands/rag-mcp.js";
17
17
  import { watchStartCommand, watchStopCommand, watchStatusCommand } from "../commands/watch.js";
@@ -532,6 +532,16 @@ export function registerMeta(program, { pkgVersion }) {
532
532
  await ragIndexCommand({ config, logger, flags });
533
533
  });
534
534
  });
535
+ // KJC-BUG-0216: el gate rag-first no puede pedir lo imposible. Esto responde
536
+ // si el RAG PUEDE hablar de un fichero, y si no, por que.
537
+ rag.command("covers <file>")
538
+ .description("Whether the RAG can answer about a file: exit 0 when indexed, 1 with the reason it cannot (no adapter, skipped path, empty or stale index)")
539
+ .option("--json", "Emit the state as JSON")
540
+ .action(async (file, flags) => {
541
+ await withConfig(pkgVersion, "rag-covers", flags, async ({ config, logger }) => {
542
+ await ragCoversCommand({ file, config, logger, flags });
543
+ });
544
+ });
535
545
  rag.command("install-hooks")
536
546
  .description("Install the post-merge git hook so the RAG index auto-refreshes after pulls/merges (KJC-TSK-0455)")
537
547
  .option("--json", "Output the result as JSON")
@@ -5,6 +5,7 @@
5
5
  import { openVecStore, countChunks, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
6
6
  import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
7
7
  import { indexProject, indexProjectDelta } from "../rag/indexer.js";
8
+ import { fileIndexState } from "../rag/coverage.js";
8
9
  import { query } from "../rag/retriever.js";
9
10
  import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
10
11
  import { indexLibrary, LIBRARY_PROJECT } from "../rag/library.js";
@@ -171,3 +172,23 @@ export async function ragEvalCommand({ config, logger, flags = {} }) {
171
172
  db.close();
172
173
  }
173
174
  }
175
+
176
+ /**
177
+ * KJC-BUG-0216 (issue #1807) — el hecho comprobable que el gate rag-first
178
+ * necesita antes de exigir una consulta: PUEDE el RAG responder sobre este
179
+ * fichero. Exit 0 = si; exit 1 = no, y la razon (sin adapter, segmento
180
+ * excluido, indice vacio o por detras) va en la salida.
181
+ */
182
+ export async function ragCoversCommand({ file, config, flags = {} }) {
183
+ const projectDir = config?.projectDir || process.cwd();
184
+ const db = openDb(config);
185
+ try {
186
+ const state = fileIndexState(projectDir, file, { db });
187
+ if (flags.json) process.stdout.write(`${JSON.stringify(state)}\n`);
188
+ else process.stdout.write(`${state.state}: ${state.reason}\n`);
189
+ if (!state.canAnswer) process.exitCode = 1;
190
+ return state;
191
+ } finally {
192
+ db.close();
193
+ }
194
+ }
@@ -68,7 +68,10 @@ const DEFAULTS = {
68
68
  state_backend: "hu-board",
69
69
  // verify_cmd (KJC-TSK-0732): user adapter that checks a card ref against
70
70
  // the real tracker — prints {exists, live, status} JSON; {ref} substituted.
71
- board: { name: null, verify_cmd: null },
71
+ // unverifiable (KJC-TSK-0878): que hacer cuando el adaptador no puede
72
+ // responder (sin red, sin credenciales, JSON invalido). "warn" degrada a
73
+ // branch-ref diciendolo; "block" se niega a aprobar lo que no ha comprobado.
74
+ board: { name: null, verify_cmd: null, unverifiable: "warn" },
72
75
  // Method gates (KJC-PCS-0068): rules climb from playbook text to
73
76
  // deterministic enforcement. card_first: "auto" = block with hu-board
74
77
  // (fully verifiable), warn with planning-game/external (presence only);
@@ -91,6 +94,11 @@ const DEFAULTS = {
91
94
  coder_rules: "./.karajan/coder-rules.md",
92
95
  base_branch: "main",
93
96
  coder_options: { model: null, auto_approve: true, fallback_coder: null },
97
+ // KJC-BUG-0217: los flags con los que kj invoca el CLI de cada provider son
98
+ // los que su version acepta, y eso cambia con el CLI. Aqui se declaran por
99
+ // agente cuando los de serie no valen: { kimi: { task_args: [] } } deja el
100
+ // prompt solo, sin auto-aprobado, para una version que rechaza la combinacion.
101
+ agents: {},
94
102
  reviewer_options: {
95
103
  output_format: "json",
96
104
  require_schema: true,
@@ -256,6 +264,10 @@ const DEFAULTS = {
256
264
  enabled: true,
257
265
  patterns: [],
258
266
  protected_files: [],
267
+ // KJC-BUG-0215: cauce declarado para el falso positivo de las reglas que
268
+ // acusan por el NOMBRE del identificador. Entradas `NAME` (todo el repo)
269
+ // o `path/to/file:NAME` (solo ese fichero, que es lo preferible).
270
+ non_secret_assignments: [],
259
271
  on_violation: "block"
260
272
  },
261
273
  perf: {
@@ -12,7 +12,7 @@ const DESTRUCTIVE_PATTERNS = [
12
12
  const CREDENTIAL_PATTERNS = [
13
13
  { id: "aws-key", pattern: /AKIA[0-9A-Z]{16}/, severity: "critical", message: "AWS access key exposed" },
14
14
  { id: "private-key", pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, severity: "critical", message: "Private key exposed" },
15
- { 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" },
15
+ { id: "generic-secret", pattern: /(password|secret|token|api_key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i, severity: "critical", message: "Hardcoded secret detected. Move it to the environment or a secrets manager" },
16
16
  { id: "github-token", pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, severity: "critical", message: "GitHub token exposed" },
17
17
  { id: "npm-token", pattern: /npm_[A-Za-z0-9]{36,}/, severity: "critical", message: "npm token exposed" },
18
18
  { id: "openai-key", pattern: /sk-[A-Za-z0-9]{20,}/, severity: "critical", message: "OpenAI API key exposed" },
@@ -24,7 +24,7 @@ const CREDENTIAL_PATTERNS = [
24
24
  { id: "slack-token", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/, severity: "critical", message: "Slack token exposed" },
25
25
  { id: "jwt-secret", pattern: /jwt[_-]?secret\s*[:=]\s*["'][^"']{8,}["']/i, severity: "critical", message: "JWT secret hardcoded. Use .env file" },
26
26
  { id: "database-url", pattern: /(mongodb|postgres|mysql|redis):\/\/[^"'\s]+:[^"'\s]+@/i, severity: "critical", message: "Database URL with credentials exposed. Use .env file" },
27
- { 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" },
27
+ { 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 an assignment. Move it to the environment or a secrets manager" },
28
28
  ];
29
29
 
30
30
  // Default protected files (block if these appear in added/modified lines)
@@ -115,6 +115,49 @@ export function checkProtectedFiles(diff, protectedFiles) {
115
115
  return violations;
116
116
  }
117
117
 
118
+ /**
119
+ * KJC-BUG-0215 (issue #1733): estas dos reglas acusan por el NOMBRE del
120
+ * identificador, no por la forma del valor, asi que un prefijo de cache llamado
121
+ * EXPERIMENTS_HANDOFF_TOKENS_NAMESPACE bloqueo una ejecucion entera con
122
+ * severidad critica y sin salida. Las demas reglas miran el valor (AKIA…,
123
+ * sk_live_…) y esas aciertan.
124
+ *
125
+ * El arreglo NO es adivinar: la review tumbo tres heuristicas seguidas y con
126
+ * razon, porque `const dbPassword = "db-password"` tambien tiene pinta inocente
127
+ * y es una credencial. Lo que faltaba era CAUCE: que el proyecto pueda declarar
128
+ * la excepcion en su configuracion versionada, donde se revisa en la PR como
129
+ * cualquier otra linea, y que el guard diga como hacerlo cuando bloquea.
130
+ */
131
+ const NAME_BASED_RULES = new Set(["hardcoded-key-assignment", "generic-secret"]);
132
+
133
+ const HOW_TO_DECLARE = "If it is not a secret, declare it in guards.output.non_secret_assignments as NAME or path/to/file:NAME";
134
+
135
+ /** El identificador al que se asigna, cuando la linea deja leerlo. */
136
+ const assignedName = (content) => /([A-Za-z_$][\w$]*)\s*[:=]\s*["']/.exec(content)?.[1] ?? null;
137
+
138
+ /** Una entrada declarada se lee como `NAME` o `path/to/file:NAME`. */
139
+ const splitEntry = (entry) => {
140
+ const cut = entry.lastIndexOf(":");
141
+ return cut === -1 ? { path: null, name: entry } : { path: entry.slice(0, cut), name: entry.slice(cut + 1) };
142
+ };
143
+
144
+ /**
145
+ * Excepcion DECLARADA, no inferida: el proyecto afirma por escrito que esta
146
+ * asignacion no guarda un secreto. Sin `file` la declaracion vale para todo el
147
+ * repositorio; con el, solo para ese fichero, que es lo estrecho y preferible.
148
+ */
149
+ export function declaredNonSecret(configGuards, file, content) {
150
+ const declared = configGuards?.output?.non_secret_assignments;
151
+ if (!Array.isArray(declared) || declared.length === 0) return false;
152
+ const name = assignedName(content);
153
+ if (!name) return false;
154
+ return declared.some(entry => {
155
+ if (typeof entry !== "string") return false;
156
+ const target = splitEntry(entry.trim());
157
+ return target.name === name && (target.path === null || target.path === file);
158
+ });
159
+ }
160
+
118
161
  /**
119
162
  * Scan a diff for pattern violations.
120
163
  * Returns { pass: boolean, violations: Array<{id, severity, file, line, message, matchedContent}> }
@@ -133,8 +176,17 @@ export function scanDiff(diff, config = {}) {
133
176
  // Check patterns against added lines
134
177
  for (const { file, line, content } of addedLines) {
135
178
  for (const { id, pattern, severity, message } of patterns) {
136
- if (pattern.test(content)) {
137
- violations.push({ id, severity, file, line, message, matchedContent: content.trim().slice(0, 200) });
179
+ if (!pattern.test(content)) continue;
180
+ const matchedContent = content.trim().slice(0, 200);
181
+ // KJC-BUG-0215: una regla que acusa por el nombre necesita un cauce
182
+ // declarado para el falso positivo, y decirlo cuando bloquea.
183
+ if (!NAME_BASED_RULES.has(id)) {
184
+ violations.push({ id, severity, file, line, message, matchedContent });
185
+ } else if (declaredNonSecret(configGuards, file, content)) {
186
+ violations.push({ id, severity: "warning", file, line, matchedContent,
187
+ message: `${message}. Declared as non-secret in guards.output.non_secret_assignments` });
188
+ } else {
189
+ violations.push({ id, severity, file, line, matchedContent, message: `${message}. ${HOW_TO_DECLARE}` });
138
190
  }
139
191
  }
140
192
  }
@@ -971,8 +971,22 @@ process.stdin.on("end", () => {
971
971
  // Recorded ONCE per session: the escape is a conscious exception, not a per-edit tax.
972
972
  if (!(rs.escapes || []).includes("KJ_ALLOW_NO_RAG")) recordEscape(sid, "KJ_ALLOW_NO_RAG", tool);
973
973
  } else {
974
- console.error("karajan sentinel: rag-first — el RAG no ha respondido sobre " + rel + " en esta sesion; consulta antes de tocarlo: kj_rag_query / kj rag query <que hace " + rel + " y donde mas vive ese concepto>. (KJ_ALLOW_NO_RAG=1 = excepcion consciente, queda registrada)" + doc("rag-first"));
975
- process.exit(2);
974
+ // KJC-BUG-0216 (issue #1807): un .astro, un .php o cualquier fichero
975
+ // sin adapter NO puede estar en el indice, asi que exigir una
976
+ // respuesta del RAG sobre el es pedir lo imposible, y empujaba a
977
+ // dejar KJ_ALLOW_NO_RAG puesta para siempre. La exencion es SOLO
978
+ // esa: un indice vacio o por detras si tiene arreglo (kj rag index)
979
+ // y sigue bloqueando, porque si no el gate desapareceria en
980
+ // cualquier proyecto sin indexar. Nada legible = se bloquea.
981
+ const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd: ROOT, encoding: "utf8" });
982
+ let blind = null;
983
+ try { const c = JSON.parse(String(cov.stdout || "")); if (c.state === "not-indexable") blind = c.reason; } catch { blind = null; }
984
+ if (blind) {
985
+ console.error("karajan sentinel: rag-first — " + blind + " — no se te exige consultar lo que el indice no puede tener; el gate lo anota y sigue" + doc("rag-first"));
986
+ } else {
987
+ console.error("karajan sentinel: rag-first — el RAG no ha respondido sobre " + rel + " en esta sesion; consulta antes de tocarlo: kj_rag_query / kj rag query <que hace " + rel + " y donde mas vive ese concepto>. (KJ_ALLOW_NO_RAG=1 = excepcion consciente, queda registrada)" + doc("rag-first"));
988
+ process.exit(2);
989
+ }
976
990
  }
977
991
  }
978
992
  }
@@ -5,7 +5,7 @@
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { computeBaseRef, setSnapshot } from "../review/diff-generator.js";
8
- import { ensureContractBlockPresent } from "../review/gate-gitignore.js";
8
+ import { ensureContractBlockPresent, excludeLocalArtifacts } from "../review/gate-gitignore.js";
9
9
  import { revParse } from "../utils/git.js";
10
10
  import { buildCoderPrompt } from "../prompts/coder.js";
11
11
  import { buildReviewerPrompt } from "../prompts/reviewer.js";
@@ -55,26 +55,18 @@ export async function autoInit(projectDir, logger) {
55
55
  }
56
56
  }
57
57
 
58
- // Ensure .gitignore exists with universal entries only (stack-specific added after planner)
59
- // KJC-BUG-0123: `.karajan/` is NOT in this list on purpose — a bare
60
- // dir-exclude breaks the gate contract (git cannot re-include children);
61
- // the canonical block is appended below from its single source.
62
- const gitignorePath = path.join(projectDir, ".gitignore");
63
- const universalIgnores = [".env", "*.log", ".DS_Store", ".reviews/"];
58
+ // KJC-BUG-0213 (issue #1734): el .gitignore es del equipo y viaja con el repo.
59
+ // Ahi solo entra el contrato v4, que el equipo entero hereda; los artefactos
60
+ // de una ejecucion son nuestros y locales, asi que van a .git/info/exclude.
61
+ // Lo que kj metia antes (.env, *.log, .DS_Store) era opinion sobre un
62
+ // proyecto ajeno, y aparecia en el diff de otra persona sin haberlo pedido.
64
63
  try {
65
- let content = "";
66
- if (await exists(gitignorePath)) {
67
- content = await fs.readFile(gitignorePath, "utf8");
68
- }
69
- const missing = universalIgnores.filter(entry => !content.includes(entry));
70
- if (missing.length > 0) {
71
- const append = (content && !content.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
72
- await fs.appendFile(gitignorePath, append, "utf8");
73
- logger.info(`Created .gitignore with universal entries`);
74
- }
75
- await ensureContractBlockPresent(projectDir);
64
+ const contract = await ensureContractBlockPresent(projectDir);
65
+ if (contract.changed) logger.info("El contrato v4 se ha anadido a .gitignore (lo hereda quien clone)");
66
+ const local = await excludeLocalArtifacts(projectDir);
67
+ if (local.changed) logger.info(`Artefactos de kj excluidos en .git/info/exclude: ${local.added.join(", ")}`);
76
68
  } catch (err) {
77
- logger.warn(`Failed to create .gitignore: ${err.message}`);
69
+ logger.warn(`Failed to prepare .gitignore: ${err.message}`);
78
70
  }
79
71
 
80
72
  const karajanDir = path.join(projectDir, ".karajan");
@@ -125,89 +117,11 @@ export async function autoInit(projectDir, logger) {
125
117
  }
126
118
  }
127
119
 
128
- // Stack-specific .gitignore patterns keyed by language/framework
129
- const STACK_GITIGNORE = {
130
- javascript: ["node_modules/", "dist/", "build/", "coverage/", ".cache/", "*.tsbuildinfo"],
131
- typescript: ["node_modules/", "dist/", "build/", "coverage/", ".cache/", "*.tsbuildinfo"],
132
- python: ["__pycache__/", "*.pyc", ".venv/", "venv/", "*.egg-info/", ".pytest_cache/", "htmlcov/", ".mypy_cache/"],
133
- java: ["target/", "*.class", "*.jar", "*.war", ".gradle/", "build/", ".settings/", ".classpath", ".project"],
134
- kotlin: ["target/", "*.class", "build/", ".gradle/", ".kotlin/"],
135
- go: ["bin/", "*.exe", "vendor/"],
136
- rust: ["target/", "Cargo.lock"],
137
- ruby: ["vendor/bundle/", ".bundle/", "coverage/", "tmp/"],
138
- php: ["vendor/", ".phpunit.result.cache", "storage/logs/"],
139
- csharp: ["bin/", "obj/", "*.suo", "*.user", "packages/", ".vs/"],
140
- swift: [".build/", "Packages/", "*.xcodeproj/", "DerivedData/"],
141
- dart: [".dart_tool/", "build/", ".packages"],
142
- };
143
-
144
- /**
145
- * Update .gitignore with stack-specific entries after planner/architect decides the stack.
146
- * Detects stack from: triage taskType, architect output, planner output, or task keywords.
147
- */
148
- export async function updateGitignoreForStack(projectDir, { stageResults, task, logger }) {
149
- const gitignorePath = path.join(projectDir, ".gitignore");
150
- const detected = new Set();
151
-
152
- // From architect — if it chose layers/patterns, it may hint at the language
153
- const arch = stageResults?.architect?.architecture;
154
- if (arch) {
155
- const archText = JSON.stringify(arch).toLowerCase();
156
- for (const lang of Object.keys(STACK_GITIGNORE)) {
157
- if (archText.includes(lang)) detected.add(lang);
158
- }
159
- }
160
-
161
- // From planner — scan plan text for language keywords
162
- const planText = (stageResults?.planner?.plan || "").toLowerCase();
163
- // From task description
164
- const taskText = (task || "").toLowerCase();
165
- const combined = `${planText} ${taskText}`;
166
-
167
- const langKeywords = {
168
- javascript: ["node", "npm", "express", "react", "vue", "next", "vite", "vitest", "jest", "pnpm", "yarn", "javascript", "js"],
169
- typescript: ["typescript", "tsx", "tsc"],
170
- python: ["python", "django", "flask", "fastapi", "pip", "pytest", "poetry"],
171
- java: ["java", "spring", "maven", "gradle", "junit"],
172
- kotlin: ["kotlin", "ktor"],
173
- go: ["golang", "go mod", "gin", "fiber"],
174
- rust: ["rust", "cargo", "tokio"],
175
- ruby: ["ruby", "rails", "gem", "bundler", "rspec"],
176
- php: ["php", "laravel", "composer", "symfony"],
177
- csharp: ["c#", "csharp", "dotnet", ".net", "aspnet"],
178
- swift: ["swift", "swiftui", "vapor"],
179
- dart: ["dart", "flutter"],
180
- };
181
-
182
- for (const [lang, keywords] of Object.entries(langKeywords)) {
183
- if (keywords.some(kw => combined.includes(kw))) detected.add(lang);
184
- }
185
-
186
- if (detected.size === 0) return;
187
-
188
- // Collect all entries for detected stacks
189
- const entries = [];
190
- for (const lang of detected) {
191
- entries.push(...(STACK_GITIGNORE[lang] || []));
192
- }
193
- const unique = [...new Set(entries)];
194
-
195
- try {
196
- let content = "";
197
- if (await exists(gitignorePath)) {
198
- content = await fs.readFile(gitignorePath, "utf8");
199
- }
200
- const missing = unique.filter(entry => !content.includes(entry));
201
- if (missing.length > 0) {
202
- const header = `\n# ${[...detected].join(" + ")} project\n`;
203
- const append = header + missing.join("\n") + "\n";
204
- await fs.appendFile(gitignorePath, append, "utf8");
205
- logger.info(`Updated .gitignore for ${[...detected].join(" + ")}: ${missing.join(", ")}`);
206
- }
207
- } catch (err) {
208
- logger.warn(`Failed to update .gitignore for stack: ${err.message}`);
209
- }
210
- }
120
+ // KJC-BUG-0213 (issue #1734): aqui vivia updateGitignoreForStack, que deducia
121
+ // el stack buscando palabras como "ts" o "js" en el texto del plan y de la
122
+ // tarea. Cualquier frase las contiene, asi que un proyecto Laravel acabo con
123
+ // reglas de TypeScript en SU .gitignore. Un acierto por casualidad no vale para
124
+ // escribir en un fichero que no es nuestro: la funcion se retira entera.
211
125
 
212
126
  /**
213
127
  * Load product context from well-known file locations.
@@ -24,10 +24,17 @@ export async function runGuardStages({ config, logger, emitter, eventBase, sessi
24
24
 
25
25
  if (!outputEnabled && !perfEnabled) return { action: "ok" };
26
26
 
27
+ // KJC-BUG-0214 (issue #1733): los guards juzgan LO QUE HA HECHO ESTE RUN, no
28
+ // la rama entera. Con el merge-base contra la base, cualquier linea anadida
29
+ // en cualquier momento de la rama contaba como recien escrita, y una
30
+ // ejecucion se bloqueo por codigo preexistente que el coder no toco. La
31
+ // sesion ya guarda HEAD al arrancar precisamente para esto.
27
32
  const baseBranch = config.base_branch || "main";
33
+ const runStart = session?.head_at_start || session?.session_start_sha || null;
28
34
  let diff;
29
35
  try {
30
- const baseRef = await computeBaseRef({ baseBranch });
36
+ const baseRef = runStart || await computeBaseRef({ baseBranch });
37
+ if (!runStart) logger.warn("Guards: sin punto de arranque del run, se juzga la rama entera contra la base");
31
38
  diff = await generateDiff({ baseRef, projectDir: config?.projectDir || null });
32
39
  } catch {
33
40
  logger.warn("Guards: could not generate diff, skipping");
@@ -57,7 +57,7 @@ import { maybeGenerateAutoHuBatch } from "./pre-loop-phases/auto-hu-batch.js";
57
57
 
58
58
  import {
59
59
  applyTriageOverrides, applyAutoSimplify, applyFlagOverrides,
60
- resolvePipelinePolicies, updateGitignoreForStack,
60
+ resolvePipelinePolicies,
61
61
  } from "../config-init.js";
62
62
  import { tryCiComment } from "../ci-integration.js";
63
63
  import { getIntegration } from "../integrations.js";
@@ -350,7 +350,6 @@ export async function runPreLoopStages({ config, logger, emitter, eventBase, ses
350
350
 
351
351
  // --- Update .gitignore with stack-specific entries based on planner/architect output ---
352
352
  const projectDir = updatedConfig.projectDir || process.cwd();
353
- await updateGitignoreForStack(projectDir, { stageResults, task, logger });
354
353
 
355
354
  // --- Auto-HU: when triage recommended decomposition and no manual huFile, generate HU batch ---
356
355
  await maybeGenerateAutoHuBatch({
@@ -8,7 +8,7 @@
8
8
  * matchers are the single truth, here and in the indexer.
9
9
  */
10
10
  import { execa } from "execa";
11
- import { isAbsolute, relative } from "node:path";
11
+ import { extname, isAbsolute, normalize, relative } from "node:path";
12
12
  import { detectAdaptersForProject, buildMatchers } from "../lang/registry.js";
13
13
  import { getLastIndexedCommit, projectSlug } from "./vec-store.js";
14
14
 
@@ -46,3 +46,34 @@ export async function ragIndexCoverage(projectDir, { db }) {
46
46
 
47
47
  return { project, total: sources.length, indexed: sources.length - missing.length, missing, stale, lastIndexedCommit, absent: indexed.size === 0 };
48
48
  }
49
+
50
+ /**
51
+ * KJC-BUG-0216 (issue #1807) — POR QUE el RAG no puede responder sobre un
52
+ * fichero. El gate rag-first exigia una respuesta sobre cualquier fichero
53
+ * tocado, y para un .astro (o .php, o cualquier lenguaje sin adapter) eso es
54
+ * imposible: el indexador nunca lo toma. La unica salida era KJ_ALLOW_NO_RAG
55
+ * puesta por costumbre en todos los comandos, y con ella puesta el gate deja de
56
+ * informar y borra la diferencia entre "no pude consultar" y "no quise".
57
+ *
58
+ * `canAnswer` es la respuesta que el gate necesita: solo cuando es true tiene
59
+ * sentido exigir que el RAG haya hablado de ese fichero.
60
+ *
61
+ * @returns {{state: "indexed"|"stale"|"not-indexable"|"index-empty", rel: string, canAnswer: boolean, reason: string}}
62
+ */
63
+ export function fileIndexState(projectDir, file, { db }) {
64
+ const project = projectSlug(projectDir);
65
+ // `./src/a.js` y `src/a.js` son el mismo fichero: sin normalizar, el segundo
66
+ // salia "stale" estando indexado (catch de la review).
67
+ const rel = normalize(isAbsolute(file) ? relative(projectDir, file) : file);
68
+ const matchers = buildMatchers(detectAdaptersForProject(projectDir));
69
+ const rows = db.prepare("SELECT DISTINCT source FROM chunks WHERE project_slug = ?").all(project);
70
+ const indexed = new Set(rows.map((r) => normalize(isAbsolute(r.source) ? relative(projectDir, r.source) : r.source)));
71
+
72
+ if (indexed.has(rel)) return { state: "indexed", rel, canAnswer: true, reason: `the index holds chunks for ${rel}` };
73
+ if (indexed.size === 0) return { state: "index-empty", rel, canAnswer: false, reason: `nothing of this project is indexed yet: kj rag index` };
74
+ if (matchers.shouldSkip(rel)) return { state: "not-indexable", rel, canAnswer: false, reason: `${rel} lives under a path the indexer always skips` };
75
+ if (!matchers.isCodeFile(rel) && extname(rel).toLowerCase() !== ".md") {
76
+ return { state: "not-indexable", rel, canAnswer: false, reason: `no language adapter covers ${extname(rel) || "a file with no extension"}, so the indexer never takes ${rel}` };
77
+ }
78
+ return { state: "stale", rel, canAnswer: false, reason: `${rel} is indexable but absent from the index: kj rag index --since auto` };
79
+ }
@@ -93,6 +93,18 @@ export async function checkCardFirst({ config = {}, projectDir = process.cwd(),
93
93
  ? { ok: false, mode: "block", reason, level: v.level }
94
94
  : { ok: true, mode: "warn", reason, level: v.level };
95
95
  }
96
+ // KJC-TSK-0878 (issue #1371): "no he podido verificar" y "he verificado y
97
+ // esta mal" no son lo mismo, y ambas pueden merecer una parada. El proyecto
98
+ // decide si la primera para, y el mensaje dice cual de las dos es: un gate
99
+ // que aprueba lo que no ha comprobado da una confianza que no ha ganado.
100
+ // Sin adaptador declarado no hay nada que bloquear: la opcion no castiga a
101
+ // quien aun no tiene verify_cmd.
102
+ if (v.verified === null && config.board?.unverifiable === "block" && config.board?.verify_cmd) {
103
+ return {
104
+ ok: false, mode: "block", level: v.level,
105
+ reason: `branch references ${ref} but the card could not be verified against ${boardName} — ${v.note} (board.unverifiable: block refuses to approve a check kj did not make)`,
106
+ };
107
+ }
96
108
  return { ok: true, mode: "pass", ref, level: v.level, ...(v.note ? { note: v.note } : {}) };
97
109
  }
98
110
  const reason = `no card reference in branch "${branch}" — card-first on ${boardName}: create the card there and name the branch after it (e.g. feat/ABC-123-summary)`;
@@ -12,6 +12,8 @@
12
12
  import fs from "node:fs/promises";
13
13
  import path from "node:path";
14
14
 
15
+ import { runCommand } from "../utils/process.js";
16
+
15
17
  // KJC-BUG-0123 (issue #1268): exported as the SINGLE source of truth — kj
16
18
  // init and the orchestrator's autoInit write this same block instead of a
17
19
  // bare `.karajan/` exclude (which git cannot re-include children of).
@@ -48,6 +50,37 @@ export async function ensureContractBlockPresent(projectDir) {
48
50
  return { changed: true };
49
51
  }
50
52
 
53
+ /**
54
+ * KJC-BUG-0213 (issue #1734): lo que kj deja en el repo durante una ejecucion
55
+ * es suyo y es local, asi que se excluye donde no molesta a nadie:
56
+ * `.git/info/exclude` no viaja con el repo ni aparece en el diff de otro.
57
+ * La ruta se pregunta a git para que funcione tambien en un worktree, donde
58
+ * `.git` es un fichero y no un directorio.
59
+ */
60
+ const LOCAL_ARTIFACTS = [".reviews/", ".kj/", ".kj-ready.json"];
61
+
62
+ export async function excludeLocalArtifacts(projectDir, run = runCommand) {
63
+ let file = path.join(projectDir, ".git", "info", "exclude");
64
+ try {
65
+ const res = await run("git", ["-C", projectDir, "rev-parse", "--git-path", "info/exclude"]);
66
+ const out = res?.stdout?.trim();
67
+ if (res?.exitCode === 0 && out) file = path.isAbsolute(out) ? out : path.join(projectDir, out);
68
+ } catch { /* sin git utilizable: se intenta la ruta por defecto */ }
69
+ let text = "";
70
+ try { text = (await fs.readFile(file, "utf8")) || ""; } catch { /* aun no existe */ }
71
+ const lines = new Set(text.split("\n").map((l) => l.trim()));
72
+ const missing = LOCAL_ARTIFACTS.filter((a) => !lines.has(a));
73
+ if (missing.length === 0) return { changed: false, added: [] };
74
+ try {
75
+ await fs.mkdir(path.dirname(file), { recursive: true });
76
+ const sep = text && !text.endsWith("\n") ? "\n" : "";
77
+ await fs.appendFile(file, `${sep}# Karajan run artifacts (local only, never shared)\n${missing.join("\n")}\n`, "utf8");
78
+ return { changed: true, added: missing };
79
+ } catch {
80
+ return { changed: false, added: [] };
81
+ }
82
+ }
83
+
51
84
  export async function ensureGateTrackable(projectDir) {
52
85
  const file = path.join(projectDir, ".gitignore");
53
86
  let text;