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 +1 -1
- package/src/commands/harden.js +14 -0
- package/src/commands/init.js +7 -1
- package/src/environment/contract-commit.js +11 -1
- package/src/git/coder-commit-guard.js +40 -0
- package/src/harden/sentinel/sentinel-shell.mjs +7 -3
- package/src/harden/sentinel-hooks.js +50 -11
- package/src/orchestrator/preflight-checks.js +11 -8
- package/src/orchestrator/stages/coder-stage.js +16 -0
- package/src/orchestrator/stages/reviewer-stage.js +13 -11
- package/src/prompts/coder.js +3 -1
- package/src/rag/indexer.js +8 -6
- package/src/roles/reviewer-role.js +23 -12
- package/src/sonar/scanner.js +22 -5
- package/src/utils/run-log.js +8 -0
- package/templates/coder-rules.md +1 -1
- package/templates/roles/coder/anthropic.md +2 -2
- package/templates/roles/coder/default.md +2 -2
- package/templates/roles/coder/google.md +2 -2
- package/templates/roles/coder/openai.md +2 -2
- package/templates/roles/coder/opencode.md +1 -1
- package/templates/roles/coder.md +2 -2
- package/templates/roles/refactorer.md +1 -1
- package/templates/skills/kj-code.md +1 -1
- package/templates/skills/kj-review.md +1 -1
- package/templates/skills/kj-sonar.md +4 -3
package/package.json
CHANGED
package/src/commands/harden.js
CHANGED
|
@@ -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));
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 === "'" ||
|
|
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 {
|
|
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(
|
|
181
|
-
if (!rel || rel.startsWith("
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ((
|
|
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
|
|
413
|
-
*
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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);
|
package/src/prompts/coder.js
CHANGED
|
@@ -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
|
-
|
|
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),
|
package/src/rag/indexer.js
CHANGED
|
@@ -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
|
-
//
|
|
75
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
93
|
-
result: {
|
|
94
|
-
|
|
95
|
-
|
|
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
|
}
|
package/src/sonar/scanner.js
CHANGED
|
@@ -240,7 +240,12 @@ export function respectRepoProperties(scanner = {}, props = {}) {
|
|
|
240
240
|
return kept;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/utils/run-log.js
CHANGED
|
@@ -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 {
|
package/templates/coder-rules.md
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
-
|
|
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,
|
|
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
|
-
-
|
|
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 →
|
|
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).
|
|
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,
|
|
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
|
-
-
|
|
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).
|
|
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
|
|
package/templates/roles/coder.md
CHANGED
|
@@ -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,
|
|
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
|
-
-
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|