karajan-code 4.41.0 → 4.42.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.41.0",
3
+ "version": "4.42.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",
@@ -26,6 +26,7 @@ import { detectStackRoots } from "../harden/stack-roots.js";
26
26
  import { installWorkflows } from "../harden/workflow-engine.js";
27
27
  import { detectTestFramework } from "../utils/project-detect.js";
28
28
  import { ensureIdentity } from "../identity/bootstrap.js";
29
+ import { commitContract, contractChanges } from "../environment/contract-commit.js";
29
30
 
30
31
  const JS_TEST_CMD = {
31
32
  vitest: "npx vitest run",
@@ -104,8 +105,12 @@ export async function hardenCommand({
104
105
  exclude = [],
105
106
  commitSupervisor = false,
106
107
  kjVersion = null,
108
+ // KJC-BUG-0289: the contract files already dirty before anything was generated
109
+ // (kj init captures them before writing its own). What is dirty NOW is the person's.
110
+ contractBefore = null,
107
111
  logger = console,
108
112
  } = {}) {
113
+ const dirtyBefore = contractBefore ?? contractChanges(projectDir);
109
114
  const repoCfg = readHardenConfig(projectDir);
110
115
  const onlyDirs = [...(repoCfg.only ?? []), ...only];
111
116
  const excludeDirs = [...(repoCfg.exclude ?? []), ...exclude];
@@ -200,6 +205,15 @@ export async function hardenCommand({
200
205
  workflows: wf?.workflows ?? [],
201
206
  guidelines: gl?.guidelines ?? [],
202
207
  };
208
+ // KJC-BUG-0289 (#1984): what kj harden generates, kj commits (KJC-BUG-0273's
209
+ // path): only that, never on the base branch. Left to the agent, 3.2k generated
210
+ // lines exceeded its own PR size and stayed untracked. The seal (--commit, ADR
211
+ // 0009) stays a human act over the supervisor alone.
212
+ if (!dryRun) {
213
+ out.contract = commitContract({ projectDir, before: dirtyBefore, baseBranch });
214
+ if (out.contract.committed) logger.info?.(`kj harden: contract committed by kj, ${out.contract.files.length} generated file(s)`);
215
+ else if (!out.contract.reason.startsWith("nothing")) logger.warn?.(`kj harden: the contract kj generated is NOT committed (${out.contract.reason})`);
216
+ }
203
217
 
204
218
  if (json) {
205
219
  logger.info?.(JSON.stringify(out));
@@ -26,6 +26,7 @@ import { installQmd } from "../utils/qmd-install.js";
26
26
  import { registerQmdCollections } from "../utils/qmd-collection.js";
27
27
  import { isGitRepo } from "../harden/harden-engine.js";
28
28
  import { hardenCommand } from "./harden.js";
29
+ import { contractChanges } from "../environment/contract-commit.js";
29
30
  import { detectProjectStack } from "../utils/stack-detect.js";
30
31
  import { bootstrapSonarToken } from "../sonar/token-bootstrap.js";
31
32
 
@@ -771,6 +772,11 @@ export async function initCommand({ logger, flags = {} }) {
771
772
  const toStderr = (...args) => console.error(...args);
772
773
  logger = { ...logger, info: toStderr, warn: toStderr, error: toStderr };
773
774
  }
775
+ // KJC-BUG-0289: what is dirty NOW is the person's; kj harden commits only what
776
+ // init and harden generate below (skills, settings, hooks, configs, guidelines).
777
+ // Outside a repository (ensureGitRepo runs later) git cannot answer and the
778
+ // baseline is empty: everything generated afterwards is kj's.
779
+ const contractBefore = contractChanges(process.cwd());
774
780
  const karajanHome = getKarajanHome();
775
781
  await ensureDir(karajanHome);
776
782
  logger.info(`Ensured ${karajanHome} exists`);
@@ -928,7 +934,7 @@ export async function initCommand({ logger, flags = {} }) {
928
934
  logger.info("Quality harness skipped (--no-harden).");
929
935
  } else if (ensureGitRepo({ projectDir: process.cwd(), logger })) {
930
936
  logger.info("Installing quality harness (kj harden)...");
931
- await hardenCommand({ projectDir: process.cwd(), logger });
937
+ await hardenCommand({ projectDir: process.cwd(), logger, contractBefore });
932
938
  hardened = true;
933
939
  } else {
934
940
  logger.warn("Quality harness NOT installed: this is not a git repository and `git init` failed — Karajan's guarantees live in git hooks. Fix git, then run `kj harden`.");
@@ -8,6 +8,9 @@
8
8
  * seal (ADR 0009), which stays a human act.
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
+ import { basename } from "node:path";
12
+
13
+ import { GENERATED_CONFIG_FILES } from "../harden/config-templates.js";
11
14
 
12
15
  /** What kj generates and the whole team must inherit by cloning (prefixes). */
13
16
  export const CONTRACT_PATHS = [
@@ -23,13 +26,20 @@ export const CONTRACT_PATHS = [
23
26
  "GEMINI.md",
24
27
  // KJC-TSK-0879: in a Rulesync repo kj's rules live in .rulesync/rules/karajan.md.
25
28
  ".rulesync/",
29
+ // KJC-BUG-0289 (#1984): the governance the whole team inherits (ADR 0016, KJC-BUG-0269).
30
+ ".karajan/rules.yml",
31
+ ".karajan/policy-anchor.json",
32
+ ".karajan/supervisor-signers.json",
26
33
  ];
27
34
 
28
35
  const FRESH_MESSAGE = "chore(bootstrap): el contrato del método, para que quien clone lo herede";
29
36
  const REGEN_MESSAGE = "chore(kj): el contrato del método, generado por kj";
30
37
 
31
38
  const runner = (projectDir, env) => (args) => execFileSync("git", ["-C", projectDir, ...args], { encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] });
32
- const isContract = (file) => CONTRACT_PATHS.some((p) => (p.endsWith("/") || p.endsWith("-") ? file.startsWith(p) : file === p));
39
+ // KJC-BUG-0289: the lint/format/commit configs harden generates sit at the root
40
+ // or under a stack root (a fullstack monorepo is hardened on every side).
41
+ const isContract = (file) =>
42
+ CONTRACT_PATHS.some((p) => (p.endsWith("/") || p.endsWith("-") ? file.startsWith(p) : file === p)) || GENERATED_CONFIG_FILES.has(basename(file));
33
43
 
34
44
  /** The contract files git sees as changed, with their porcelain code (`??` untracked). */
35
45
  function contractStatus(git) {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * KJC-BUG-0285 (#1982): the coder commits nothing; the pipeline does, once the
3
+ * review approves. With --auto-commit the coder ran git commit inside its
4
+ * iteration, before the reviewer, and the rejected commit stayed on the branch.
5
+ * The stage reads HEAD before the coder runs and, when HEAD moved on top of it,
6
+ * undoes those commits SOFTLY: the changes stay in the tree and the index for
7
+ * the review. A rewritten branch (HEAD no longer descends from the start) is
8
+ * left alone. Plain git, no injected runner: it never throws, it says.
9
+ */
10
+ import { spawnSync } from "node:child_process";
11
+
12
+ const git = (args, cwd) => spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13
+
14
+ /** @returns {string|null} the sha HEAD points at, or null outside a repo with commits */
15
+ export function headSha(cwd = process.cwd()) {
16
+ try {
17
+ const res = git(["rev-parse", "HEAD"], cwd);
18
+ return res.status === 0 ? res.stdout.trim() : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /** @returns {{undone: number, reason?: string}} */
25
+ export function undoCommitsSince(sha, cwd = process.cwd()) {
26
+ if (!sha) return { undone: 0, reason: "no starting commit" };
27
+ try {
28
+ const head = headSha(cwd);
29
+ if (!head || head === sha) return { undone: 0 };
30
+ if (git(["merge-base", "--is-ancestor", sha, "HEAD"], cwd).status !== 0) {
31
+ return { undone: 0, reason: "HEAD no longer descends from the iteration's start" };
32
+ }
33
+ const undone = Number(git(["rev-list", "--count", `${sha}..HEAD`], cwd).stdout.trim());
34
+ const reset = git(["reset", "--soft", sha], cwd);
35
+ if (reset.status !== 0) return { undone: 0, reason: `git reset --soft failed: ${reset.stderr.trim()}` };
36
+ return { undone };
37
+ } catch (err) {
38
+ return { undone: 0, reason: err.message };
39
+ }
40
+ }
@@ -64,10 +64,14 @@ export const headIndex = (words, heads) => {
64
64
  return k < 0 ? words.length : i + k;
65
65
  };
66
66
 
67
+ // KJC-BUG-0286: a $ expands only when something expandable follows it ($x, ${x},
68
+ // $(cmd), $1, $@...). A lone "$ " in a grep pattern is a literal dollar sign.
69
+ const LIVE_EXPANSION = /\$(?=[A-Za-z0-9_{(@*#?$!-])|`/;
70
+
67
71
  /**
68
72
  * KJC-BUG-0243: blank the quoted text that cannot run: single-quoted spans, and
69
- * double-quoted spans with no $ or backtick. What remains is what the shell can
70
- * still expand or execute, so operator and substitution checks read only that.
73
+ * double-quoted spans with no live $ or backtick. What remains is what the shell
74
+ * can still expand or execute, so operator and substitution checks read only that.
71
75
  * @param {string} cmd
72
76
  */
73
77
  export const stripInertQuotes = (cmd) => {
@@ -79,7 +83,7 @@ export const stripInertQuotes = (cmd) => {
79
83
  while (j < cmd.length && cmd[j] !== q) j += q === '"' && cmd[j] === "\\" ? 2 : 1;
80
84
  if (j >= cmd.length) return out + cmd.slice(i); // unclosed: left as is, for the caller to deny
81
85
  const body = cmd.slice(i + 1, j);
82
- out += q === "'" || !/[$`]/.test(body) ? q + q : q + body + q;
86
+ out += q === "'" || !LIVE_EXPANSION.test(body) ? q + q : q + body + q;
83
87
  i = j;
84
88
  }
85
89
  return out;
@@ -135,7 +135,8 @@ const POST_BODY = `#!/usr/bin/env node
135
135
  // a tool call (PostToolUse, always exit 0).
136
136
  import console from "node:console";
137
137
  import process from "node:process";
138
- import { relative } from "node:path";
138
+ import { realpathSync } from "node:fs";
139
+ import { basename, dirname, join, relative } from "node:path";
139
140
  import { spawnSync } from "node:child_process";
140
141
  import { CODE, TESTS, ROOT, CARD, branchOf, load, save, session } from "./sentinel-lib.mjs";
141
142
  import { remindersFor } from "./sentinel-reminders.mjs";
@@ -170,20 +171,37 @@ process.stdin.on("end", () => {
170
171
  // every question with silence, so the gate would demand what no query can
171
172
  // deliver. kj says so explicitly, in both its human and json forms.
172
173
  const EMPTY_INDEX = /No chunks indexed yet|"empty"\\s*:\\s*true/;
174
+ // KJC-BUG-0293 (#1996, #1995): the index names its sources through whatever
175
+ // spelling of the project directory indexed them (a symlinked workspace, an
176
+ // alias). Compared as TEXT against the harness root, every hit fell outside
177
+ // the tree and was dropped in silence: rag_hits stayed empty and only new
178
+ // files passed. Both sides are compared by REAL path, and what still falls
179
+ // outside is written down for the gate to name.
180
+ // A path whose file is gone (or not yet there) resolves through its deepest
181
+ // existing ancestor, so an alias of the directory still lands in the tree.
182
+ const realOf = (p) => {
183
+ try { return realpathSync(p); } catch { /* resolve the parent instead */ }
184
+ const d = dirname(p);
185
+ return !d || d === p ? p : join(realOf(d), basename(p));
186
+ };
187
+ const ROOT_REAL = realOf(ROOT);
173
188
  const recordRag = (query, sources, answer = "") => {
174
189
  const state = load();
175
190
  const s = session(state, sid);
176
191
  s.at = Date.now();
177
192
  if (EMPTY_INDEX.test(String(answer))) s.rag_index_empty = true;
178
193
  const hits = [];
194
+ const dropped = [];
179
195
  for (const src of sources) {
180
- const rel = relative(ROOT, String(src)).split(String.fromCharCode(92)).join("/");
181
- if (!rel || rel.startsWith("..") || rel.startsWith("/") || hits.includes(rel)) continue;
182
- hits.push(rel);
196
+ const rel = relative(ROOT_REAL, realOf(String(src))).split(String.fromCharCode(92)).join("/");
197
+ if (!rel || rel === ".." || rel.startsWith("../") || rel.startsWith("/")) { dropped.push(String(src)); continue; }
198
+ if (!hits.includes(rel)) hits.push(rel);
183
199
  }
184
200
  (s.rag_queries ||= []).push({ ts: Date.now(), text: String(query || "").slice(0, 200), hits });
185
201
  const all = (s.rag_hits ||= []);
186
202
  for (const h of hits) if (!all.includes(h)) all.push(h);
203
+ if (hits.length === 0 && dropped.length > 0) s.rag_dropped = { root: ROOT_REAL, count: dropped.length, sample: dropped.slice(0, 3) };
204
+ else if (hits.length > 0) delete s.rag_dropped; // an answer inside the tree ends the diagnosis
187
205
  save(state);
188
206
  };
189
207
  // Sources are read STRUCTURALLY, never through the stringified response:
@@ -195,7 +213,9 @@ process.stdin.on("end", () => {
195
213
  };
196
214
  const parseJson = (t) => { try { return JSON.parse(t); } catch { return null; } };
197
215
  if (/__kj_rag_query$/.test(String(tool)) && /^mcp__/.test(String(tool))) {
198
- const parts = Array.isArray(response?.content) ? response.content : [];
216
+ // KJC-BUG-0293: the host may hand the answer as {content:[parts]}, as the
217
+ // bare array of parts, or as the text itself; all three are the same answer.
218
+ const parts = Array.isArray(response?.content) ? response.content : Array.isArray(response) ? response : [{ text: typeof response === "string" ? response : "" }];
199
219
  recordRag(input.text, parts.flatMap((p) => sourcesOf(parseJson(String(p?.text || "")))), parts.map((p) => String(p?.text || "")).join(String.fromCharCode(10)));
200
220
  process.exit(0);
201
221
  }
@@ -292,7 +312,9 @@ process.stdin.on("end", () => {
292
312
  const s = session(state, sid);
293
313
  s.at = Date.now();
294
314
  const rel = relative(ROOT, file).replaceAll("\\\\", "/");
295
- const bucket = TESTS.test(rel) ? s.edited_tests : CODE.test(rel) ? s.edited_sources : null;
315
+ // KJC-BUG-0279 (#1981): a file outside the repository is not one of its sources.
316
+ const inside = rel && rel !== ".." && !rel.startsWith("../") && !rel.startsWith("/");
317
+ const bucket = !inside ? null : TESTS.test(rel) ? s.edited_tests : CODE.test(rel) ? s.edited_sources : null;
296
318
  if (bucket && !bucket.includes(rel)) bucket.push(rel);
297
319
  // KJC-TSK-0910: the branch size while it is written, with the CI budget
298
320
  // (kj pr-size), said once per threshold crossed. Context, never a block.
@@ -747,8 +769,15 @@ process.stdin.on("end", () => {
747
769
  // (find fuera: -delete/-exec mutan — reviewer catch; sus tokens de
748
770
  // carril los caza el escaner de abajo.)
749
771
  const READONLY = /^[ \\t]*(grep|rg|cat|head|tail|less|ls|wc|diff|stat|file|du|tree|git (log|show|diff|status|blame))\\b[^;|&<>$\`(){}\\n\\r]*$/;
772
+ // KJC-BUG-0286 (#1982): a command whose EVERY segment only reads mutates
773
+ // nothing, however it is chained (grep | grep, git show | grep, cd && git
774
+ // status). A cd with a literal target is one such segment: the paths after
775
+ // it need no verifying when nothing after it writes. One mutating segment
776
+ // and the whole command is judged below, as before.
777
+ const CD_LITERAL = /^[ \\t]*(cd|pushd)([ \\t]+[^ \\t;|&<>$\`(){}\\n\\r]*)?[ \\t]*$/;
778
+ const readsOnly = (s) => s.split(/&&|\\|\\||[;|]/).every((seg) => READONLY.test(seg) || CD_LITERAL.test(seg));
750
779
  // KJC-BUG-0243: operators inside inert quotes (grep -e "a|b") do not chain anything.
751
- if (!READONLY.test(stripInertQuotes(cmd))) {
780
+ if (!readsOnly(stripInertQuotes(cmd))) {
752
781
  // cd/pushd invalida TODO razonamiento textual de rutas posteriores
753
782
  // (carrera de bypasses confirmada en review: destino bare, con $,
754
783
  // relativas post-cd...): en un comando NO-read-only, cambiar de
@@ -960,7 +989,8 @@ process.stdin.on("end", () => {
960
989
  } catch { /* no report or unreadable — the Steward is not adopted here */ }
961
990
  const file = input.file_path || input.notebook_path;
962
991
  const rel = file ? relative(ROOT, String(file)).replaceAll("\\\\", "/") : "";
963
- if (rel && CODE.test(rel) && !TESTS.test(rel)) {
992
+ // KJC-BUG-0279 (#1981): outside the repository there is no source to govern.
993
+ if (rel && rel !== ".." && !rel.startsWith("../") && !rel.startsWith("/") && CODE.test(rel) && !TESTS.test(rel)) {
964
994
  const branch = branchOf();
965
995
  const why = !branch ? null : BASE_BRANCHES.has(branch) ? "base" : !CARD.test(branch) ? "nocard" : null;
966
996
  // ADR 0015 (KJC-TSK-0925): no escape. Without a card there is no work.
@@ -1017,7 +1047,13 @@ process.stdin.on("end", () => {
1017
1047
  console.error("karajan sentinel: rag-first — el indice aun no tiene " + rel + " (" + absent + "); indexalo y consulta despues: kj_rag_query / kj rag query <que hace " + rel + ">" + doc("rag-first"));
1018
1048
  process.exit(2);
1019
1049
  } else {
1020
- 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>." + doc("rag-first"));
1050
+ // KJC-BUG-0293 (#1996): hits that fell outside the tree are named, so the
1051
+ // person learns the index was built from another path instead of querying again.
1052
+ const dropped = rs.rag_dropped;
1053
+ const outside = dropped?.count
1054
+ ? " La ultima consulta devolvio " + dropped.count + " hit(s) fuera de este arbol (" + dropped.root + "): " + (dropped.sample || []).join(", ") + " — el indice se construyo desde otra ruta; reindexa desde esta: kj rag index --with-sources."
1055
+ : "";
1056
+ 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>." + outside + doc("rag-first"));
1021
1057
  process.exit(2);
1022
1058
  }
1023
1059
  }
@@ -1034,7 +1070,10 @@ process.stdin.on("end", () => {
1034
1070
  // bandera no puede apagar el gate de commit, y mover core.hooksPath lo
1035
1071
  // apaga entero. El --no-verify que kj harden --commit usa por dentro no
1036
1072
  // pasa por aqui: no es una tool call. Escape humano, sellado como el resto.
1037
- const words = wordsOf(cmd);
1073
+ // KJC-BUG-0286 (#1982): judged on the git commit SEGMENT alone, read by the
1074
+ // shell reader: a "commit" inside a grep pattern piped after git show, or
1075
+ // the -n of a grep in the same pipeline, is not a skipped hook.
1076
+ const commitSegments = shellSegments(cmd).map((ws) => ws.map((w) => w.slice(w.lastIndexOf("/") + 1))).filter((ws) => inOrder(["git", "commit"], ws));
1038
1077
  // git acepta abreviaturas no ambiguas de las opciones largas, asi que
1039
1078
  // --no-ver salta el hook igual que --no-verify (catch de la review).
1040
1079
  const shortWithN = (w) => w.length > 1 && w[0] === "-" && w[1] !== "-" && w.includes("n");
@@ -1051,7 +1090,7 @@ process.stdin.on("end", () => {
1051
1090
  // --config-env=core.hooksPath=VAR) y una propiedad de seguridad no puede
1052
1091
  // depender de como parta las palabras el separador (catch de la review).
1053
1092
  const movesHooks = cmd.toLowerCase().includes("core.hookspath");
1054
- if ((inOrder(["git", "commit"], words) && words.some(skipsVerify)) || movesHooks) {
1093
+ if (commitSegments.some((ws) => ws.some(skipsVerify)) || movesHooks) {
1055
1094
  // ADR 0015 (KJC-TSK-0926): no escape. A broken hook is fixed; skipping it,
1056
1095
  // if it ever must happen, is the user's act in their own terminal.
1057
1096
  console.error("karajan sentinel: el gate de commit no se apaga con una bandera — el review cruzado y la policy corren en el hook, y saltarselos deja el diff sin veredicto. Si el hook esta roto, arreglalo; si de verdad hace falta saltarlo, lo hace tu usuario en su terminal." + doc("escapes"));
@@ -409,8 +409,8 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
409
409
  /**
410
410
  * Run the doctor-style complementary checks and merge their outcomes into
411
411
  * the existing preflight result shape. Runs with yes:true (non-interactive)
412
- * and auto-remediation enabled. FAIL/TIMEOUT are appended to errors (blocking),
413
- * WARN are appended to warnings (non-blocking), FIXED status is recorded as a
412
+ * and auto-remediation enabled. FAIL is appended to errors (blocking), WARN and
413
+ * TIMEOUT to warnings (non-blocking: a timeout is "not checked"), FIXED status is recorded as a
414
414
  * remediation, and runtime overrides from auto-fixes are merged into
415
415
  * configOverrides.
416
416
  */
@@ -437,11 +437,12 @@ async function runExtendedPreflight({ config, result, emitter, eventBase, logger
437
437
 
438
438
  for (const c of report.checks) {
439
439
  result.checks.push({ name: c.name, ok: c.status === STATUS.OK || c.status === STATUS.FIXED || c.status === STATUS.SKIPPED, detail: c.detail });
440
- const eventStatus = c.status === STATUS.FAIL || c.status === STATUS.TIMEOUT
441
- ? "fail"
442
- : c.status === STATUS.WARN
443
- ? "warn"
444
- : "ok";
440
+ // KJC-BUG-0287 (#1982): a timeout is "not checked", never "broken". GitHub
441
+ // unreachable for the SHA-pinned actions blocked a docs-only run that needed
442
+ // no network at all; the runner itself calls TIMEOUT warn-level.
443
+ let eventStatus = "ok";
444
+ if (c.status === STATUS.FAIL) eventStatus = "fail";
445
+ else if (c.status === STATUS.WARN || c.status === STATUS.TIMEOUT) eventStatus = "warn";
445
446
  emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
446
447
  status: eventStatus,
447
448
  message: `${c.label}: ${c.detail}`,
@@ -449,9 +450,11 @@ async function runExtendedPreflight({ config, result, emitter, eventBase, logger
449
450
  }));
450
451
  if (c.status === STATUS.FIXED) {
451
452
  result.remediations.push(`${c.label}: ${c.detail}`);
452
- } else if (c.status === STATUS.FAIL || c.status === STATUS.TIMEOUT) {
453
+ } else if (c.status === STATUS.FAIL) {
453
454
  result.ok = false;
454
455
  result.errors.push({ check: c.name, message: `${c.label}: ${c.detail}`, fix: c.fix });
456
+ } else if (c.status === STATUS.TIMEOUT) {
457
+ result.warnings.push(`${c.label}: not checked, it did not answer in time (${c.detail}); the run goes on`);
455
458
  } else if (c.status === STATUS.WARN) {
456
459
  result.warnings.push(`${c.label}: ${c.detail}`);
457
460
  }
@@ -17,6 +17,7 @@ import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js"
17
17
  import { runCoderWithFallback } from "../agent-fallback.js";
18
18
  import { invokeSolomon } from "../solomon-escalation.js";
19
19
  import { detectRateLimit } from "../../utils/rate-limit-detector.js";
20
+ import { headSha, undoCommitsSince } from "../../git/coder-commit-guard.js";
20
21
  import { createStallDetector } from "../../utils/stall-detector.js";
21
22
  import { buildStandbyState } from "../../brain/standby-store.js";
22
23
  import { applyQuotaSimulation } from "../../utils/quota-simulator.js";
@@ -54,6 +55,10 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
54
55
  // Claude run `cd /home/manu/assistant && pnpm init …` and 36 MB
55
56
  // of code landed outside projectDir, outside any git repo.
56
57
  const homeSnapshotBeforeCoder = snapshotHomeTopLevel();
58
+ // KJC-BUG-0285 (#1982): where HEAD stands before the coder runs. The coder
59
+ // commits nothing; the pipeline does, once the review approves.
60
+ const stageDir = config?.projectDir || process.cwd();
61
+ const headBefore = headSha(stageDir);
57
62
  let coderExecResult;
58
63
  try {
59
64
  coderExecResult = await coderRoleInstance.execute({
@@ -84,6 +89,17 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
84
89
  }
85
90
  trackBudget({ role: "coder", provider: coderRole.provider, model: coderRole.model, result: coderExecResult.result, duration_ms: Date.now() - coderStart });
86
91
  applyQuotaSimulation(coderExecResult, { iteration, agent: coderRole.provider });
92
+ // KJC-BUG-0285 (#1982): a commit the coder made on its own carried the error the
93
+ // reviewer then rejected, and stayed on the branch. Undone softly: the changes
94
+ // stay in the tree for the review; the pipeline commits what it approves.
95
+ if (headBefore) {
96
+ const undone = undoCommitsSince(headBefore, stageDir);
97
+ if (undone.undone > 0) {
98
+ const message = `Coder committed on its own (${undone.undone} commit(s)); undone softly, the changes stay in the tree: the pipeline commits once the review approves`;
99
+ logger.warn(message);
100
+ emitProgress(emitter, makeEvent("coder:commit-undone", { ...eventBase, stage: "coder" }, { message, detail: { commits: undone.undone, headBefore } }));
101
+ }
102
+ }
87
103
 
88
104
  if (!coderExecResult.ok) {
89
105
  const details = coderExecResult.result?.error || coderExecResult.summary || "unknown error";
@@ -335,18 +335,20 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
335
335
  confidence: reviewResult.confidence ?? 0
336
336
  });
337
337
  } catch (error_) {
338
+ // KJC-BUG-0284 (#1982): a verdict kj cannot read is the reviewer's failure,
339
+ // never a PARSE_ERROR defect handed to the coder (it sent one run in circles).
340
+ const details = `the verdict could not be read: ${error_.message}`;
338
341
  logger.warn(`Reviewer output validation failed: ${error_.message}`);
339
- review = {
340
- approved: false,
341
- blocking_issues: [{
342
- id: "PARSE_ERROR",
343
- severity: "high",
344
- description: `Reviewer output could not be parsed: ${error_.message}`
345
- }],
346
- non_blocking_suggestions: [],
347
- summary: `Parse error: ${error_.message}`,
348
- confidence: 0
349
- };
342
+ await markSessionStatus(session, "failed");
343
+ emitProgress(
344
+ emitter,
345
+ makeEvent("reviewer:end", { ...eventBase, stage: "reviewer" }, {
346
+ status: "fail",
347
+ message: `Reviewer failed: ${details}`,
348
+ detail: { provider: reviewerRole.provider, executorType: "agent" }
349
+ })
350
+ );
351
+ throw new Error(`Reviewer failed: ${details}`, { cause: error_ });
350
352
  }
351
353
  // --- Scope filter: auto-defer out-of-scope blocking issues ---
352
354
  const { review: filteredReview, demoted, deferred, allDemoted } = filterReviewScope(review, diff);
@@ -251,7 +251,9 @@ export async function buildCoderPromptLayout({ task, reviewerFeedback = null, so
251
251
  // .gitignore, a rogue verify worktree). Anchor the scope hard.
252
252
  section("SCOPE: this is an EXISTING project. Implement ONLY the current HU against the code already here. NEVER scaffold, bootstrap or initialize project structure, never add a generic multi-stack .gitignore, and never create git worktrees (`git worktree add`) — your workspace is already prepared. If a step ever reads like 'set up the project', treat it as a misread of an existing repo: re-read the HU and make the real change.", STABLE),
253
253
  section("Follow SOLID principles. Write small, focused functions (< 30 lines).", STABLE),
254
- section("Make atomic commits: 1 logical change = 1 commit. Keep PRs small and reviewable.", STABLE),
254
+ // KJC-BUG-0285 (#1982): "atomic commits" read as "commit": the coder committed
255
+ // before the review and the rejected commit stayed on the branch.
256
+ section("One logical change per task; keep it small and reviewable. NEVER run git commit, git push or open a PR: the pipeline commits once the cross-AI review approves.", STABLE),
255
257
  section("Security: validate all input, parameterize queries, never expose secrets. Use the auth pattern that matches the stack (e.g. httpOnly cookies for web sessions).", STABLE),
256
258
  section("Use the project's standard logger and type system (no console.log when a structured logger exists; use the project's type idiom — TS types, JSDoc, Python type hints, Go interfaces…).", STABLE),
257
259
  section(SUBPROCESS_CONSTRAINTS, STABLE),
@@ -11,7 +11,7 @@ import { createHash } from "node:crypto";
11
11
  import { execa } from "execa";
12
12
 
13
13
  import { chunkMarkdown, chunkPlan, chunkSource } from "./chunker.js";
14
- import { insertChunk, deleteChunksBySource, findChunkByHash } from "./vec-store.js";
14
+ import { insertChunk, deleteChunksBySource, findChunkByHash, getEmbeddingsByIds } from "./vec-store.js";
15
15
  import { detectAdaptersForProject, getAllCodeExtensions } from "../lang/registry.js";
16
16
  import { indexableReason, listProjectFiles } from "./indexable.js";
17
17
 
@@ -71,15 +71,17 @@ export async function indexFile(path, { db, embedder, logger = console, project
71
71
  let skipped = 0;
72
72
  for (const ch of chunks) {
73
73
  // KJC-TSK-0484 — Skip re-embedding identical bodies for the same project.
74
- // The chunks row is still rewritten under the new source so deletions of
75
- // an alias don't orphan a row whose owner was the dedup target.
74
+ // KJC-BUG-0281 (#1980): the row is STILL written under the new source, with
75
+ // the stored vector: a file whose every chunk duplicates another (CLAUDE.md
76
+ // next to the AGENTS.md kj harden writes) was left with no rows at all, and
77
+ // the coverage check reported it missing after every reindex.
76
78
  const contentHash = createHash("sha256").update(ch.text).digest("hex");
77
79
  try {
78
80
  const dup = findChunkByHash(db, contentHash, project);
79
- if (dup) { skipped += 1; continue; }
80
- const embedding = await embedder.embed(ch.text);
81
+ const stored = dup ? getEmbeddingsByIds(db, [dup.id]).get(Number(dup.id)) : null;
82
+ const embedding = stored ?? await embedder.embed(ch.text);
81
83
  insertChunk(db, { source: path, kind: ch.metadata.kind || kind, text: ch.text, metadata: ch.metadata, embedding, project, contentHash });
82
- indexed += 1;
84
+ if (stored) skipped += 1; else indexed += 1;
83
85
  } catch (err) {
84
86
  failed += 1;
85
87
  logger.warn?.(`[rag-indexer] embed failed for ${path} (${chunkLabel(ch)}): ${err.message}`);
@@ -1,6 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import { AgentRole } from "./agent-role.js";
2
4
  import { buildRtkInstructions } from "../prompts/rtk-snippet.js";
3
5
  import { extractFirstJson } from "../utils/json-extract.js";
6
+ import { reportUnparseableVerdict } from "../review/unparseable-verdict.js";
4
7
  import { section, buildPromptLayout, joinLayout, STABLE, VOLATILE } from "../prompts/prompt-layout.js";
5
8
  import { clipDiff } from "../prompts/diff-clip.js";
6
9
  import { buildSplitSignal } from "../prompts/split-signal.js";
@@ -66,7 +69,13 @@ export class ReviewerRole extends AgentRole {
66
69
  parseOutput(raw) {
67
70
  const parsed = extractFirstJson(raw);
68
71
  if (!parsed) throw new Error("Failed to parse reviewer output: no JSON found");
69
- return parsed;
72
+ // KJC-BUG-0284 (#1982): a reviewer wrapped its verdict in the role envelope
73
+ // {ok, result: {approved…}, summary}; the envelope is not the verdict.
74
+ const verdict = typeof parsed.approved !== "boolean" && typeof parsed.result?.approved === "boolean"
75
+ ? { ...parsed.result, summary: parsed.result.summary ?? parsed.summary }
76
+ : parsed;
77
+ if (typeof verdict.approved !== "boolean") throw new Error("Failed to parse reviewer output: no boolean approved");
78
+ return verdict;
70
79
  }
71
80
 
72
81
  buildSuccessResult(parsed, provider, agentResult) {
@@ -87,18 +96,20 @@ export class ReviewerRole extends AgentRole {
87
96
  : `Rejected: ${blockingIssues.length} blocking issue(s) — ${parsed.summary || ""}`;
88
97
  }
89
98
 
90
- handleParseError(err, agentResult, _provider) {
99
+ // KJC-BUG-0284 (#1982): an unreadable answer is the reviewer's failure, not
100
+ // the coder's. Returned as ok=false so the retry and the fallback reviewer
101
+ // engage (reviewer-fallback.js); the raw answer is saved for inspection.
102
+ // Before, it came back as a PARSE_ERROR rejection: the coder read it as a
103
+ // defect of its code and one run went in circles for twenty minutes.
104
+ async handleParseError(err, agentResult, provider) {
105
+ const output = agentResult?.output;
106
+ const hash = createHash("sha256").update(String(output ?? "")).digest("hex").slice(0, 12);
107
+ const error = await reportUnparseableVerdict({ projectDir: this.config?.projectDir ?? process.cwd(), reviewer: provider, output, hash });
91
108
  return {
92
- ok: true,
93
- result: {
94
- ...agentResult,
95
- approved: false,
96
- blocking_issues: [{ id: "PARSE_ERROR", severity: "high", description: `Reviewer output could not be parsed: ${err.message}` }],
97
- non_blocking_suggestions: [],
98
- confidence: 0,
99
- raw_summary: `Parse error: ${err.message}`
100
- },
101
- summary: `Reviewer output parse error: ${err.message}`
109
+ ok: false,
110
+ result: { ...agentResult, error: `${err.message}\n${error}`, provider },
111
+ summary: `Reviewer output parse error: ${err.message}`,
112
+ usage: agentResult?.usage
102
113
  };
103
114
  }
104
115
  }
@@ -240,7 +240,12 @@ export function respectRepoProperties(scanner = {}, props = {}) {
240
240
  return kept;
241
241
  }
242
242
 
243
- /** @returns {Promise<{existed: boolean, declaredKey: string|null}>} */
243
+ // KJC-BUG-0280 (#1979): the scanner aborts, with a stack trace, on a declared
244
+ // folder that does not exist. Only the folders the repo HAS are declared.
245
+ const SOURCE_DIRS = ["src", "lib", "app"];
246
+ const TEST_DIRS = ["tests", "test"];
247
+
248
+ /** @returns {Promise<{existed: boolean, declaredKey: string|null, written?: boolean, note?: string, nothingToScan?: string}>} */
244
249
  export async function ensureSonarProjectProperties(cwd = process.cwd()) {
245
250
  const propsPath = path.join(cwd, "sonar-project.properties");
246
251
  try {
@@ -257,17 +262,26 @@ export async function ensureSonarProjectProperties(cwd = process.cwd()) {
257
262
  } catch {
258
263
  // no package.json or invalid JSON — use defaults
259
264
  }
265
+ const existing = (dirs) => dirs.filter((d) => fs.existsSync(path.join(cwd, d)));
266
+ const sources = existing(SOURCE_DIRS);
267
+ const tests = existing(TEST_DIRS);
268
+ if (sources.length === 0) {
269
+ const wanted = SOURCE_DIRS.map((d) => `${d}/`).join(", ");
270
+ return { existed: false, declaredKey: null, written: false, nothingToScan: `no ${wanted} folder to analyse: write a sonar-project.properties declaring sonar.sources when the code arrives` };
271
+ }
260
272
  const projectKey = (pkg.name || path.basename(cwd)).replaceAll(/[^a-zA-Z0-9_.-]/g, "_");
273
+ const layout = [`sonar.sources=${sources.join(",")}`, ...(tests.length ? [`sonar.tests=${tests.join(",")}`] : [])];
261
274
  const props = [
262
275
  `sonar.projectKey=${projectKey}`,
263
276
  `sonar.projectName=${pkg.name || path.basename(cwd)}`,
264
- `sonar.sources=src`,
265
- `sonar.tests=tests`,
277
+ ...layout,
266
278
  `sonar.javascript.lcov.reportPaths=coverage/lcov.info`,
267
279
  `sonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/coverage/**`,
268
280
  ].join("\n");
269
281
  await fsPromises.writeFile(propsPath, props + "\n", "utf8");
270
- return { existed: false, declaredKey: null };
282
+ // Said out loud: the file is new in the working tree and the team should see it.
283
+ const note = `sonar: wrote sonar-project.properties (${layout.join(", ")}): review it and commit it`;
284
+ return { existed: false, declaredKey: null, written: true, note };
271
285
  }
272
286
  }
273
287
 
@@ -277,6 +291,9 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
277
291
  // directory under the key its properties declare — so the proof of
278
292
  // coverage is about that package, not the root's `sonar.sources`.
279
293
  const repoProps = await ensureSonarProjectProperties(scanCwd);
294
+ if (repoProps.nothingToScan) {
295
+ return { ok: false, skipped: true, projectKey: null, stdout: "", stderr: `sonar: ${repoProps.nothingToScan}`, exitCode: 0 };
296
+ }
280
297
  let effectiveProjectKey;
281
298
  if (cwd && !projectKey && repoProps.declaredKey) {
282
299
  effectiveProjectKey = repoProps.declaredKey;
@@ -403,7 +420,7 @@ export async function runSonarScan(config, projectKey = null, { verbose = false,
403
420
  projectKey: effectiveProjectKey,
404
421
  cwd: scanCwd,
405
422
  scanner: pick.type,
406
- note: [note, analysisNote].filter(Boolean).join("; ") || null,
423
+ note: [repoProps.note, note, analysisNote].filter(Boolean).join("; ") || null,
407
424
  stdout: result.stdout,
408
425
  // A scan that failed on the server's side says so, not with the scanner's own log.
409
426
  stderr: !ok && result.exitCode === 0 ? analysisNote : result.stderr,
@@ -267,6 +267,14 @@ export function readRunLog(projectDir, maxLines = 50) {
267
267
  const total = lines.length;
268
268
  const shown = lines.slice(-maxLines);
269
269
  const status = parseRunStatus(lines);
270
+ // KJC-BUG-0288 (#1982): a run that died with its host session never logs
271
+ // "finished", so the log said "running" forever. The lock names the run's
272
+ // process (KJC-BUG-0250): no live holder, no run.
273
+ if (status.isRunning && !isPidAlive(readLock(path.join(resolveLogDir(projectDir), LOCK_FILENAME))?.pid)) {
274
+ status.isRunning = false;
275
+ status.died = true;
276
+ status.note = "the run's process is gone (it died with its host session, or was killed): kj resume continues it";
277
+ }
270
278
  const MAX_LINE_CHARS = 2000;
271
279
  const truncated = shown.map(l => l.length > MAX_LINE_CHARS ? l.slice(0, MAX_LINE_CHARS) + "… [truncated]" : l);
272
280
  return {
@@ -5,7 +5,7 @@
5
5
  - NEVER overwrite existing files entirely. Always make targeted, minimal edits.
6
6
  - When adding new code to an existing file, insert only the new lines at the correct location.
7
7
  - After each edit, verify with `git diff` that ONLY the intended lines changed.
8
- - If unintended changes are detected, revert immediately with `git checkout -- <file>`.
8
+ - If unintended changes are detected, undo ONLY those hunks with a targeted edit. Never revert the whole file: it may hold edits that are not yours.
9
9
  - Pay special attention to CSS, HTML, and config files where full rewrites destroy prior work (brand colors, layouts, styles).
10
10
 
11
11
  ## Multi-agent / multi-developer environment
@@ -44,13 +44,13 @@ An incomplete implementation is worse than an error — never report success if
44
44
  <file_safety>
45
45
  - NEVER overwrite a file wholesale. Make targeted edits only.
46
46
  - After each edit, inspect `git diff` to confirm only the intended lines changed.
47
- - If unintended changes appear, revert immediately with `git checkout -- <file>`.
47
+ - If unintended changes appear, undo ONLY those hunks with a targeted edit. Never revert the whole file: it may hold edits that are not yours.
48
48
  - CSS, HTML, config files are especially high-risk for full-rewrite damage.
49
49
  </file_safety>
50
50
 
51
51
  <quality>
52
52
  - SOLID principles. Functions < 30 lines, single responsibility.
53
- - Atomic commits: 1 logical change = 1 commit.
53
+ - One logical change per task. NEVER run git commit or git push: the pipeline commits once the review approves.
54
54
  - No `console.log` in production — use a structured logger.
55
55
  - No `any` types — use JSDoc `@typedef` / `@param` / `@returns`.
56
56
  </quality>
@@ -42,13 +42,13 @@ Before reporting done, verify that ALL parts of the task are addressed:
42
42
  - NEVER overwrite existing files entirely. Always make targeted, minimal edits.
43
43
  - When adding new code to an existing file, insert only the new lines at the correct location.
44
44
  - After each edit, verify with `git diff` that ONLY the intended lines changed.
45
- - If unintended changes are detected, revert immediately with `git checkout -- <file>`.
45
+ - If unintended changes are detected, undo ONLY those hunks with a targeted edit. Never revert the whole file: it may hold edits that are not yours.
46
46
  - Pay special attention to CSS, HTML, and config files where full rewrites destroy prior work.
47
47
 
48
48
  ## Code Quality Rules
49
49
 
50
50
  - Follow SOLID principles. Write small, focused functions (< 30 lines).
51
- - Make atomic commits: 1 logical change = 1 commit. Keep PRs small and reviewable.
51
+ - One logical change per task; keep it small and reviewable. NEVER run git commit or git push: the pipeline commits once the review approves.
52
52
  - Security: use httpOnly cookies for auth tokens, validate all input, parameterize queries, never expose secrets.
53
53
  - No console.log in production code -- use a structured logger. No 'any' types -- use JSDoc annotations.
54
54
 
@@ -56,11 +56,11 @@ An incomplete implementation is worse than an error — don't claim success if p
56
56
  ## File safety
57
57
 
58
58
  - NEVER overwrite a file entirely. Targeted edits only.
59
- - Verify each edit with `git diff`. Unintended lines → revert with `git checkout -- <file>`.
59
+ - Verify each edit with `git diff`. Unintended lines → undo only those hunks, never the whole file (it may hold edits that are not yours).
60
60
 
61
61
  ## Quality
62
62
 
63
- - SOLID. Small functions (< 30 lines). Atomic commits. No `console.log` in production, no `any` types (JSDoc instead).
63
+ - SOLID. Small functions (< 30 lines). Never commit or push (the pipeline commits after review). No `console.log` in production, no `any` types (JSDoc instead).
64
64
 
65
65
  ## Output
66
66
 
@@ -32,13 +32,13 @@ An incomplete implementation is worse than an error. Do NOT claim success if any
32
32
 
33
33
  - NEVER overwrite a file wholesale. Make targeted edits.
34
34
  - Verify after each edit with `git diff` that ONLY intended lines changed.
35
- - If unintended changes appear, revert with `git checkout -- <file>`.
35
+ - If unintended changes appear, undo ONLY those hunks with a targeted edit. Never revert the whole file: it may hold edits that are not yours.
36
36
  - CSS/HTML/config files are high-risk for destructive rewrites.
37
37
 
38
38
  ## QUALITY
39
39
 
40
40
  - SOLID. Small focused functions (< 30 lines).
41
- - Atomic commits (1 logical change = 1 commit).
41
+ - One logical change per task. NEVER run git commit or git push: the pipeline commits once the review approves.
42
42
  - No `console.log` in production. Use a structured logger.
43
43
  - No `any` types. Use JSDoc annotations.
44
44
 
@@ -12,7 +12,7 @@ Role: write code and tests for the given task.
12
12
  6. Code must compile. Tests must pass.
13
13
  7. Never hardcode secrets. Use environment variables (`process.env` in Node, `os.environ` in Python).
14
14
  8. Never overwrite a file entirely. Make targeted edits only.
15
- 9. Small functions (< 30 lines). Atomic commits. No `console.log` in production. No `any` types.
15
+ 9. Small functions (< 30 lines). Never commit or push (the pipeline commits after review). No `console.log` in production. No `any` types.
16
16
 
17
17
  ## Before reporting done
18
18
 
@@ -119,13 +119,13 @@ When modifying an existing project:
119
119
  - NEVER overwrite existing files entirely. Always make targeted, minimal edits.
120
120
  - When adding new code to an existing file, insert only the new lines at the correct location.
121
121
  - After each edit, verify with `git diff` that ONLY the intended lines changed.
122
- - If unintended changes are detected, revert immediately with `git checkout -- <file>`.
122
+ - If unintended changes are detected, undo ONLY those hunks with a targeted edit. Never revert the whole file: it may hold edits that are not yours.
123
123
  - Pay special attention to CSS, HTML, and config files where full rewrites destroy prior work.
124
124
 
125
125
  ## Code Quality Rules
126
126
 
127
127
  - Follow SOLID principles. Write small, focused functions (< 30 lines).
128
- - Make atomic commits: 1 logical change = 1 commit. Keep PRs small and reviewable.
128
+ - One logical change per task; keep it small and reviewable. NEVER run git commit or git push: the pipeline commits once the review approves.
129
129
  - Security: use httpOnly cookies for auth tokens, validate all input, parameterize queries, never expose secrets.
130
130
  - No console.log in production code -- use a structured logger. No 'any' types -- use JSDoc annotations.
131
131
 
@@ -22,7 +22,7 @@ You are the **Refactorer** in a multi-role AI pipeline. Your job is to improve c
22
22
 
23
23
  - NEVER overwrite existing files entirely. Always make targeted, minimal edits.
24
24
  - After each edit, verify with `git diff` that ONLY the intended lines changed.
25
- - If unintended changes are detected, revert immediately with `git checkout -- <file>`.
25
+ - If unintended changes are detected, undo ONLY those hunks with a targeted edit. Never revert the whole file: it may hold edits that are not yours.
26
26
 
27
27
  ## Output format
28
28
 
@@ -40,7 +40,7 @@ After writing code, verify ALL of these before reporting done:
40
40
 
41
41
  - NEVER overwrite existing files entirely — make targeted edits
42
42
  - After each edit, verify with `git diff` that ONLY intended lines changed
43
- - If unintended changes detected, revert immediately with `git checkout -- <file>`
43
+ - If unintended changes are detected, undo ONLY those hunks with a targeted edit. Never revert the whole file: it may hold edits you did not make, and an agent never discards changes that are not its own
44
44
 
45
45
  ## Completeness check
46
46
 
@@ -8,7 +8,7 @@ Review the changes in the current branch: $ARGUMENTS
8
8
 
9
9
  ## Steps
10
10
 
11
- 1. Run `git diff main...HEAD` (or appropriate base branch) to see all changes
11
+ 1. See ALL the work: `git diff --cached` (staged), `git diff` (unstaged) and `git diff main...HEAD` (or the appropriate base branch) for what is already committed
12
12
  2. Review each changed file against the priorities below
13
13
  3. Report findings clearly
14
14
 
@@ -13,14 +13,15 @@ $ARGUMENTS
13
13
  ```bash
14
14
  npx @sonar/scan -Dsonar.host.url=http://localhost:9000 -Dsonar.projectKey=<project-key>
15
15
  ```
16
- 3. Check quality gate status:
16
+ 3. Check quality gate status (the token comes from `KJ_SONAR_TOKEN`, set by `kj init`; a token authenticates as `token:` with an empty password):
17
17
  ```bash
18
- curl -s -u admin:admin "http://localhost:9000/api/qualitygates/project_status?projectKey=<project-key>"
18
+ curl -s -u "$KJ_SONAR_TOKEN:" "http://localhost:9000/api/qualitygates/project_status?projectKey=<project-key>"
19
19
  ```
20
20
  4. List issues:
21
21
  ```bash
22
- curl -s -u admin:admin "http://localhost:9000/api/issues/search?projectKeys=<project-key>&statuses=OPEN&ps=50"
22
+ curl -s -u "$KJ_SONAR_TOKEN:" "http://localhost:9000/api/issues/search?projectKeys=<project-key>&statuses=OPEN&ps=50"
23
23
  ```
24
+ Never authenticate with the default `admin` account: if the server still accepts its default password, tell the user to change it.
24
25
 
25
26
  ## If SonarQube is not available
26
27