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 +1 -1
- package/src/agents/base-agent.js +22 -0
- package/src/agents/kimi-agent.js +12 -3
- package/src/cli/register-meta.js +11 -1
- package/src/commands/rag.js +21 -0
- package/src/config/defaults.js +13 -1
- package/src/guards/output-guard.js +56 -4
- package/src/harden/sentinel-hooks.js +16 -2
- package/src/orchestrator/config-init.js +16 -102
- package/src/orchestrator/drivers/iteration-phases/guards.js +8 -1
- package/src/orchestrator/drivers/pre-loop.js +1 -2
- package/src/rag/coverage.js +32 -1
- package/src/review/card-first.js +12 -0
- package/src/review/gate-gitignore.js +33 -0
package/package.json
CHANGED
package/src/agents/base-agent.js
CHANGED
|
@@ -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
|
package/src/agents/kimi-agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/cli/register-meta.js
CHANGED
|
@@ -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")
|
package/src/commands/rag.js
CHANGED
|
@@ -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
|
+
}
|
package/src/config/defaults.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
975
|
-
|
|
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
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
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
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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,
|
|
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({
|
package/src/rag/coverage.js
CHANGED
|
@@ -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
|
+
}
|
package/src/review/card-first.js
CHANGED
|
@@ -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;
|