karajan-code 4.33.0 → 4.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/base-agent.js +22 -0
- package/src/agents/kimi-agent.js +12 -3
- package/src/brain/one-shot-policy.js +26 -0
- package/src/brain/with-brain-recovery.js +4 -1
- package/src/checks/release-check.js +51 -5
- package/src/cli/register-meta.js +53 -3
- package/src/commands/code.js +18 -16
- package/src/commands/harden.js +9 -2
- package/src/commands/policy.js +21 -14
- package/src/commands/rag.js +21 -0
- package/src/commands/report.js +28 -0
- package/src/commands/review-gate.js +13 -11
- package/src/config/defaults.js +13 -1
- package/src/environment/panel.js +33 -0
- package/src/guards/output-guard.js +56 -4
- package/src/harden/guidelines-engine.js +7 -1
- package/src/harden/guidelines-templates.js +10 -1
- package/src/harden/harness-hooks.js +6 -0
- package/src/harden/sentinel-hooks.js +243 -26
- package/src/harden/supervisor-commit.js +41 -1
- package/src/harden/workflow-templates.js +9 -3
- package/src/orchestrator/config-init.js +16 -102
- package/src/orchestrator/drivers/iteration-phases/guards.js +8 -1
- package/src/orchestrator/drivers/pre-loop.js +1 -2
- package/src/policy/supervisor-verify.js +45 -11
- package/src/rag/coverage.js +32 -1
- package/src/review/board-pending.js +68 -0
- package/src/review/card-first.js +12 -0
- package/src/review/gate-gitignore.js +33 -0
- package/src/review/loc-budget.js +97 -0
- package/src/roles/audit-role.js +28 -2
package/package.json
CHANGED
package/src/agents/base-agent.js
CHANGED
|
@@ -34,6 +34,28 @@ export class BaseAgent {
|
|
|
34
34
|
this.environment = environment;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* KJC-BUG-0217 (issue #1409) — los flags con los que kj invoca el CLI de un
|
|
39
|
+
* provider eran una constante del codigo, y hay versiones que rechazan la
|
|
40
|
+
* combinacion: kimi-code 0.27.0 responde "Cannot combine --prompt with
|
|
41
|
+
* --yolo" y dejaba `kj agent run` inutilizable, sin mas salida que parchear
|
|
42
|
+
* kj. Ahora el proyecto puede declararlos:
|
|
43
|
+
*
|
|
44
|
+
* agents: { kimi: { task_args: [], review_args: [] } }
|
|
45
|
+
*
|
|
46
|
+
* Sin declaracion valen los de siempre. Una declaracion que no sea una lista
|
|
47
|
+
* de cadenas se ignora: la config no puede romper la invocacion.
|
|
48
|
+
*
|
|
49
|
+
* @param {"task"|"review"} kind
|
|
50
|
+
* @param {string[]} fallback
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
declaredArgs(kind, fallback) {
|
|
54
|
+
const declared = this.config?.agents?.[this.name]?.[`${kind}_args`];
|
|
55
|
+
if (!Array.isArray(declared) || declared.some((a) => typeof a !== "string")) return fallback;
|
|
56
|
+
return declared;
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
/**
|
|
38
60
|
* Run a shell command through the injected CommandRunner. Subclasses should
|
|
39
61
|
* call this instead of importing runCommand directly so tests can swap in
|
package/src/agents/kimi-agent.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
3
3
|
|
|
4
|
+
/** El rechazo de kimi-code 0.27.0 a `-p` junto con `-y`, en su forma literal. */
|
|
5
|
+
const REJECTS_COMBINATION = /cannot combine .* with /i;
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Kimi Code (Moonshot, binary `kimi`) — KJC-TSK-0729 fase 2: the eighth
|
|
6
9
|
* built-in agent and the free-tier reserve of the reviewer/solomon panel.
|
|
@@ -17,7 +20,7 @@ import { resolveBin } from "./resolve-bin.js";
|
|
|
17
20
|
export class KimiAgent extends BaseAgent {
|
|
18
21
|
async runTask(task) {
|
|
19
22
|
// Coder mode IS an agentic run: file edits and shell need approval.
|
|
20
|
-
return this._exec(task, this.getRoleModel(task.role || "coder"), ["-y"]);
|
|
23
|
+
return this._exec(task, this.getRoleModel(task.role || "coder"), this.declaredArgs("task", ["-y"]));
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
async reviewTask(task) {
|
|
@@ -27,7 +30,7 @@ export class KimiAgent extends BaseAgent {
|
|
|
27
30
|
return this._exec(
|
|
28
31
|
{ ...task, prompt: `Do NOT use any tools. Answer directly from this prompt only.\n\n${task.prompt}` },
|
|
29
32
|
this.getRoleModel(task.role || "reviewer"),
|
|
30
|
-
[],
|
|
33
|
+
this.declaredArgs("review", []),
|
|
31
34
|
);
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -40,6 +43,12 @@ export class KimiAgent extends BaseAgent {
|
|
|
40
43
|
timeout: task.timeoutMs,
|
|
41
44
|
cwd: task.cwd,
|
|
42
45
|
});
|
|
43
|
-
|
|
46
|
+
// KJC-BUG-0217 (issue #1409): el CLI puede rechazar la COMBINACION de flags
|
|
47
|
+
// (kimi-code 0.27.0: "Cannot combine --prompt with --yolo"). Ese error crudo
|
|
48
|
+
// no dice que hacer; el de kj si.
|
|
49
|
+
const error = REJECTS_COMBINATION.test(String(res.stderr || ""))
|
|
50
|
+
? `${res.stderr}\n\nkimi rechaza la combinacion de flags con la que kj lo invoca. Declara los que acepta esta version: agents.kimi.task_args (una lista vacia deja el prompt solo, sin auto-aprobado).`
|
|
51
|
+
: res.stderr;
|
|
52
|
+
return { ok: res.exitCode === 0, output: res.stdout, error, exitCode: res.exitCode };
|
|
44
53
|
}
|
|
45
54
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery for a command that runs once (KJC-BUG-0194).
|
|
3
|
+
*
|
|
4
|
+
* The pipeline can afford to wait: a quota wall means hibernate, persist the
|
|
5
|
+
* session and resume when the window reopens. A one-shot command cannot.
|
|
6
|
+
* Sleeping five hours inside `kj code` or `kj audit` is worse than the failure
|
|
7
|
+
* it is handling, and an MCP caller gets no answer at all.
|
|
8
|
+
*
|
|
9
|
+
* So here a quota wall takes the next declared candidate AT ONCE, and with
|
|
10
|
+
* none left it stops and says what it tried. Everything else keeps the
|
|
11
|
+
* pipeline's behaviour: a retired model still moves on immediately, provider
|
|
12
|
+
* 500s still back off, an auth failure still aborts.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_RECOVERY_POLICY } from "./with-brain-recovery.js";
|
|
15
|
+
import { ERROR_CLASS } from "./agent-error-classifier.js";
|
|
16
|
+
|
|
17
|
+
const takeTheNextOne = { mode: "abort", maxRetries: 0, fallbackEligible: true, fallbackImmediate: true };
|
|
18
|
+
|
|
19
|
+
export const ONE_SHOT_POLICY = Object.freeze({
|
|
20
|
+
...DEFAULT_RECOVERY_POLICY,
|
|
21
|
+
classes: {
|
|
22
|
+
...DEFAULT_RECOVERY_POLICY.classes,
|
|
23
|
+
[ERROR_CLASS.QUOTA_EXHAUSTED_DAILY]: takeTheNextOne,
|
|
24
|
+
[ERROR_CLASS.QUOTA_EXHAUSTED_MONTHLY]: takeTheNextOne,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -121,7 +121,10 @@ export async function withBrainRecovery({
|
|
|
121
121
|
|
|
122
122
|
while (true) {
|
|
123
123
|
const result = await effectiveAgent.runTask(taskArgs);
|
|
124
|
-
|
|
124
|
+
// KJC-TSK-0873: quien HA CORRIDO viaja con el resultado. Tras una caída al
|
|
125
|
+
// fallback, el llamante creía que había escrito el provider declarado, y
|
|
126
|
+
// con eso el recuento del panel contaba trabajo ajeno como propio.
|
|
127
|
+
if (result?.ok) return { ...result, provider: effectiveProvider };
|
|
125
128
|
|
|
126
129
|
const cls = classifyAgentError({
|
|
127
130
|
provider: effectiveProvider,
|
|
@@ -106,6 +106,42 @@ async function packPrivacyCheck(projectDir, pkg) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* KJC-BUG-0204 — an item may declare WHICH command repairs it (`remedied_by`),
|
|
111
|
+
* so the gate stops blocking the one action that would turn it green. It was
|
|
112
|
+
* the landing: the check demanded a deployed site and the guard blocked the
|
|
113
|
+
* deploy, leaving `KJ_ALLOW_RELEASE=1` — switching the gate off entirely — as
|
|
114
|
+
* the only way to reach the publish that came next.
|
|
115
|
+
*/
|
|
116
|
+
const remedy = (item) => (typeof item?.remedied_by === "string" && item.remedied_by.trim() ? { remediedBy: item.remedied_by.trim() } : {});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The words of a verb must appear in the command IN ORDER, as whole words:
|
|
120
|
+
* `firebase deploy` covers `firebase --account a@b --project p deploy --only x`,
|
|
121
|
+
* and `deploy firebase` is a different command. Separators (`;`, `|`, `&`,
|
|
122
|
+
* parentheses) split words, so `(npm publish)` is still a publication.
|
|
123
|
+
* A word is read by its basename, so `/usr/bin/npm publish` is the same
|
|
124
|
+
* publication as `npm publish` (review catch: an absolute path hid one).
|
|
125
|
+
*/
|
|
126
|
+
const wordsOf = (text) => String(text).split(/[^A-Za-z0-9_.@:/-]+/).filter(Boolean).map((w) => w.slice(w.lastIndexOf("/") + 1)).filter(Boolean);
|
|
127
|
+
|
|
128
|
+
export function remediesCommand(remediedBy, command) {
|
|
129
|
+
const want = wordsOf(remediedBy);
|
|
130
|
+
if (want.length === 0) return false;
|
|
131
|
+
let i = 0;
|
|
132
|
+
for (const word of wordsOf(command)) if (word === want[i] && ++i === want.length) return true;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A publication is irreversible: no declared item may excuse one. Detected with
|
|
138
|
+
* the SAME tolerance as a remedy (review catch): matching only adjacent words
|
|
139
|
+
* left `npm --registry https://r.example publish` undetected, so an item could
|
|
140
|
+
* have lifted a red check while the publication went ahead.
|
|
141
|
+
*/
|
|
142
|
+
const PUBLICATION_VERBS = ["npm publish", "gh release create"];
|
|
143
|
+
const isPublication = (command) => PUBLICATION_VERBS.some((verb) => remediesCommand(verb, command));
|
|
144
|
+
|
|
109
145
|
async function declaredItems(projectDir, config, version) {
|
|
110
146
|
const items = config?.release_check?.items;
|
|
111
147
|
if (!Array.isArray(items)) return [];
|
|
@@ -116,13 +152,16 @@ async function declaredItems(projectDir, config, version) {
|
|
|
116
152
|
const p = isAbsolute(item.file_contains.path) ? item.file_contains.path : join(projectDir, item.file_contains.path);
|
|
117
153
|
const needle = String(item.file_contains.pattern).replaceAll("{version}", version ?? "");
|
|
118
154
|
const ok = existsSync(p) && readFileSync(p, "utf8").includes(needle);
|
|
119
|
-
checks.push({ name, ok, detail: ok ? `"${needle}" found in ${item.file_contains.path}` : `"${needle}" NOT found in ${item.file_contains.path}` });
|
|
155
|
+
checks.push({ name, ok, ...remedy(item), detail: ok ? `"${needle}" found in ${item.file_contains.path}` : `"${needle}" NOT found in ${item.file_contains.path}` });
|
|
120
156
|
} else if (item?.command) {
|
|
121
157
|
try {
|
|
122
158
|
const res = await runCommand("sh", ["-c", String(item.command).replaceAll("{version}", version ?? "")], { cwd: projectDir });
|
|
123
|
-
checks.push({ name, ok: res.exitCode === 0, detail: res.exitCode === 0 ? "command exited 0" : `command exited ${res.exitCode}` });
|
|
159
|
+
checks.push({ name, ok: res.exitCode === 0, ...remedy(item), detail: res.exitCode === 0 ? "command exited 0" : `command exited ${res.exitCode}` });
|
|
124
160
|
} catch (err) {
|
|
125
|
-
|
|
161
|
+
// El remedio viaja tambien cuando el item no se pudo ni ejecutar
|
|
162
|
+
// (catch de la review): si no, el comando que lo repara queda
|
|
163
|
+
// bloqueado justo cuando el check no ha podido comprobar nada.
|
|
164
|
+
checks.push({ name, ok: false, ...remedy(item), detail: `command failed to run: ${err.message}` });
|
|
126
165
|
}
|
|
127
166
|
} else {
|
|
128
167
|
checks.push({ name, ok: false, detail: "unknown item shape — use file_contains {path, pattern} or command" });
|
|
@@ -160,10 +199,17 @@ export async function dualPublishCheck(projectDir, pkg, run = runCommand) {
|
|
|
160
199
|
}
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
export async function runReleaseCheck({ projectDir = process.cwd(), config = {} } = {}) {
|
|
202
|
+
export async function runReleaseCheck({ projectDir = process.cwd(), config = {}, forCommand = null } = {}) {
|
|
164
203
|
const { checks, version, pkg } = await genericChecks(projectDir);
|
|
165
204
|
const pack = await packPrivacyCheck(projectDir, pkg);
|
|
166
205
|
const dual = await dualPublishCheck(projectDir, pkg);
|
|
167
206
|
checks.push(...(pack ? [pack] : []), ...(dual ? [dual] : []), await policyRangeCheck(projectDir), ...await declaredItems(projectDir, config, version));
|
|
168
|
-
return { ok: checks.every((c) => c.ok), version, checks };
|
|
207
|
+
if (forCommand === null) return { ok: checks.every((c) => c.ok), version, checks };
|
|
208
|
+
// A lifted check stays RED in the report: the fact is not falsified, it just
|
|
209
|
+
// stops blocking the command that exists to repair it. The mark travels ON
|
|
210
|
+
// the check (review catch): two items may share a name, and a name-keyed
|
|
211
|
+
// list would have exempted a red check nothing remedies.
|
|
212
|
+
const liftable = !isPublication(forCommand);
|
|
213
|
+
const marked = checks.map((c) => (liftable && !c.ok && c.remediedBy && remediesCommand(c.remediedBy, forCommand) ? { ...c, lifted: true } : c));
|
|
214
|
+
return { ok: marked.every((c) => c.ok || c.lifted === true), version, checks: marked, lifted: marked.filter((c) => c.lifted).map((c) => c.name) };
|
|
169
215
|
}
|
package/src/cli/register-meta.js
CHANGED
|
@@ -11,7 +11,7 @@ import { identityCommand } from "../commands/identity.js";
|
|
|
11
11
|
import { policyCommand } from "../commands/policy.js";
|
|
12
12
|
import { claimsCommand, claimsGateCommand } from "../commands/claims.js";
|
|
13
13
|
import { stewardSweepCommand } from "../commands/steward.js";
|
|
14
|
-
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand } from "../commands/rag.js";
|
|
14
|
+
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand, ragCoversCommand } from "../commands/rag.js";
|
|
15
15
|
import { qmdQueryCommand } from "../commands/qmd.js";
|
|
16
16
|
import { ragMcpCommand } from "../commands/rag-mcp.js";
|
|
17
17
|
import { watchStartCommand, watchStopCommand, watchStatusCommand } from "../commands/watch.js";
|
|
@@ -40,6 +40,9 @@ import { existsSync } from "node:fs";
|
|
|
40
40
|
import { join } from "node:path";
|
|
41
41
|
import { spawnSync } from "node:child_process";
|
|
42
42
|
import { verifySentinelScripts, resolveSentinelRoot } from "../harden/sentinel-hooks.js";
|
|
43
|
+
import { boardGate } from "../review/board-pending.js";
|
|
44
|
+
import { panelDeviation } from "../environment/panel.js";
|
|
45
|
+
import { detectHostAgent } from "../utils/agent-detect.js";
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Register the "meta" / single-role / housekeeping commands: pre-pipeline
|
|
@@ -337,12 +340,16 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
337
340
|
release.command("check")
|
|
338
341
|
.description("Verify the release checklist deterministically (manifest vs CHANGELOG vs tags, privacy scan of publishable files, plus the project's release_check.items); exit 1 lists exactly what is missing")
|
|
339
342
|
.option("--json", "Machine-readable result")
|
|
343
|
+
// KJC-BUG-0204: el guard pregunta POR un comando. Un item que declara
|
|
344
|
+
// remedied_by no bloquea el comando que lo repara; publicar nunca se exime.
|
|
345
|
+
.option("--for-command <cmd>", "Evaluate the checklist for this command: red items whose remedied_by is this command do not block it")
|
|
340
346
|
.action(async (flags) => {
|
|
341
347
|
await withConfig(pkgVersion, "release-check", flags, async ({ config }) => {
|
|
342
|
-
const res = await runReleaseCheck({ projectDir: config?.projectDir || process.cwd(), config });
|
|
348
|
+
const res = await runReleaseCheck({ projectDir: config?.projectDir || process.cwd(), config, forCommand: flags.forCommand ?? null });
|
|
343
349
|
if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
|
|
344
350
|
else {
|
|
345
351
|
for (const c of res.checks) console.log(` ${c.ok ? "✓" : "✗"} ${c.name}: ${c.detail}`);
|
|
352
|
+
if (res.lifted?.length) console.log(` ℓ no bloquean este comando porque es su remedio: ${res.lifted.join(", ")}`);
|
|
346
353
|
console.log(res.ok ? `release check: ready to release ${res.version ?? ""}`.trim() : "release check: NOT ready — fix the red items first");
|
|
347
354
|
}
|
|
348
355
|
process.exitCode = res.ok ? 0 : 1;
|
|
@@ -419,8 +426,12 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
419
426
|
});
|
|
420
427
|
policyCmd.command("seal")
|
|
421
428
|
.description("Sella en el decision log un escape KJ_ALLOW_* usado en tool-time (exempt, chokepoint=tool, identidad declarada del clon) — lo invoca el Sentinel, GOV-F")
|
|
422
|
-
.
|
|
429
|
+
.option("--escape <name>", "Escape usado (p.ej. KJ_ALLOW_BOARD)")
|
|
423
430
|
.option("--tool <tool>", "Tool que lo usó (Bash, Edit…)")
|
|
431
|
+
// KJC-TSK-0868: el panel se sella igual, con su propio chokepoint.
|
|
432
|
+
.option("--panel <coder>", "Coder declarado cuando el anfitrión escribió el código")
|
|
433
|
+
.option("--host <agent>", "Anfitrión que escribió, para el asiento del panel")
|
|
434
|
+
.option("--honoured", "El panel se CUMPLIÓ: escribió el coder declarado (KJC-TSK-0873)")
|
|
424
435
|
.action(async (flags) => {
|
|
425
436
|
await withConfig(pkgVersion, "policy-seal", flags, async ({ config }) => {
|
|
426
437
|
process.exitCode = await policyCommand({ action: "seal", config, flags });
|
|
@@ -466,9 +477,38 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
466
477
|
.action((flags) => {
|
|
467
478
|
const res = verifySentinelScripts();
|
|
468
479
|
if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
|
|
480
|
+
// KJC-BUG-0197: manipulación y desfase son hechos distintos y se leen
|
|
481
|
+
// distinto. Un desfase no es un defecto: los guardias que corren están
|
|
482
|
+
// sellados, solo son más viejos que las plantillas de este árbol.
|
|
483
|
+
else if (res.drift?.length && res.ok) console.log(`sentinel verify: scripts sellados, con desfase (${res.drift.join(", ")}) — ${res.reason}`);
|
|
469
484
|
else console.log(res.ok ? "sentinel verify: scripts intactos" : `sentinel verify: modificados fuera de kj harden: ${res.mismatched.join(", ")} — restaura con kj harden`);
|
|
470
485
|
process.exitCode = res.ok ? 0 : 1;
|
|
471
486
|
});
|
|
487
|
+
// KJC-TSK-0868: el anfitrión escribiendo con otro coder declarado no se
|
|
488
|
+
// bloquea, se registra. El Stop gate pregunta una vez por sesión.
|
|
489
|
+
sentinel.command("panel-check")
|
|
490
|
+
.description("Record in the decision log that the host wrote code while another coder is declared (chokepoint panel); prints nothing and records nothing when the panel was honoured")
|
|
491
|
+
.requiredOption("--session <id>", "Host session id, for the record")
|
|
492
|
+
.action(async (flags) => {
|
|
493
|
+
await withConfig(pkgVersion, "sentinel-panel-check", flags, async ({ config }) => {
|
|
494
|
+
const dev = panelDeviation(config, detectHostAgent());
|
|
495
|
+
if (!dev) return;
|
|
496
|
+
process.exitCode = await policyCommand({ action: "seal", config, flags: { panel: dev.coder, host: dev.host }, logger: { info: () => {}, error: console.error } });
|
|
497
|
+
if (process.exitCode === 0) console.log(`ℓ panel: el coder declarado es ${dev.coder} y ha escrito ${dev.host} — registrado como excepción consciente (kj report lo cuenta)`);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
// KJC-BUG-0198: el guard pregunta, kj decide con un hecho comprobable (otra
|
|
501
|
+
// PR abierta de la card), y exit 2 = bloquea.
|
|
502
|
+
sentinel.command("board-gate")
|
|
503
|
+
.description("Decide which pending board moves actually block: a card still being delivered in another open PR is carried, everything else blocks; exit 2 = blocked")
|
|
504
|
+
.requiredOption("--session <id>", "Host session id whose pending moves are evaluated")
|
|
505
|
+
.option("--json", "Machine-readable result")
|
|
506
|
+
.action((flags) => {
|
|
507
|
+
const res = boardGate({ projectDir: resolveSentinelRoot(), sessionId: flags.session });
|
|
508
|
+
if (flags.json) process.stdout.write(`${JSON.stringify(res)}\n`);
|
|
509
|
+
else for (const c of res.carried) console.log(`ℓ board-sync: ${c.card} ${c.why} — se moverá al cerrarla`);
|
|
510
|
+
process.exitCode = res.blocking.length > 0 ? 2 : 0;
|
|
511
|
+
});
|
|
472
512
|
|
|
473
513
|
// KJC-TSK-0704 — the outbound privacy boundary: audit before anything ships.
|
|
474
514
|
const privacy = program.command("privacy").description("Personal-data (PII) auditing of outbound boundaries: staged diffs, build outputs, docs trees");
|
|
@@ -492,6 +532,16 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
492
532
|
await ragIndexCommand({ config, logger, flags });
|
|
493
533
|
});
|
|
494
534
|
});
|
|
535
|
+
// KJC-BUG-0216: el gate rag-first no puede pedir lo imposible. Esto responde
|
|
536
|
+
// si el RAG PUEDE hablar de un fichero, y si no, por que.
|
|
537
|
+
rag.command("covers <file>")
|
|
538
|
+
.description("Whether the RAG can answer about a file: exit 0 when indexed, 1 with the reason it cannot (no adapter, skipped path, empty or stale index)")
|
|
539
|
+
.option("--json", "Emit the state as JSON")
|
|
540
|
+
.action(async (file, flags) => {
|
|
541
|
+
await withConfig(pkgVersion, "rag-covers", flags, async ({ config, logger }) => {
|
|
542
|
+
await ragCoversCommand({ file, config, logger, flags });
|
|
543
|
+
});
|
|
544
|
+
});
|
|
495
545
|
rag.command("install-hooks")
|
|
496
546
|
.description("Install the post-merge git hook so the RAG index auto-refreshes after pulls/merges (KJC-TSK-0455)")
|
|
497
547
|
.option("--json", "Output the result as JSON")
|
package/src/commands/code.js
CHANGED
|
@@ -5,23 +5,13 @@ import { buildCoderPrompt } from "../prompts/coder.js";
|
|
|
5
5
|
import { resolveCardContext } from "../prompts/card-context.js";
|
|
6
6
|
import { composeTask, resolveRagContext } from "../prompts/session-context.js";
|
|
7
7
|
import { resolveRole } from "../config.js";
|
|
8
|
-
import { withBrainRecovery, DEFAULT_RECOVERY_POLICY } from "../brain/with-brain-recovery.js";
|
|
9
|
-
import { buildRoleFallbackChain } from "../brain/role-fallback-chain.js";
|
|
10
|
-
import { ERROR_CLASS } from "../brain/agent-error-classifier.js";
|
|
11
|
-
|
|
12
8
|
// KJC-TSK-0859: the agents no longer substitute a dead model behind your back,
|
|
13
|
-
// so the command needs the declared chain
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
classes: {
|
|
20
|
-
...DEFAULT_RECOVERY_POLICY.classes,
|
|
21
|
-
[ERROR_CLASS.QUOTA_EXHAUSTED_DAILY]: { mode: "abort", maxRetries: 0, fallbackEligible: true, fallbackImmediate: true },
|
|
22
|
-
[ERROR_CLASS.QUOTA_EXHAUSTED_MONTHLY]: { mode: "abort", maxRetries: 0, fallbackEligible: true, fallbackImmediate: true },
|
|
23
|
-
},
|
|
24
|
-
});
|
|
9
|
+
// so the command needs the declared chain, with the one-shot policy a command
|
|
10
|
+
// deserves (KJC-BUG-0194 moved it to its own module: audit needs the same).
|
|
11
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
12
|
+
import { buildRoleFallbackChain } from "../brain/role-fallback-chain.js";
|
|
13
|
+
import { ONE_SHOT_POLICY } from "../brain/one-shot-policy.js";
|
|
14
|
+
import { sealPanel } from "../environment/panel.js";
|
|
25
15
|
import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
26
16
|
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
27
17
|
|
|
@@ -89,6 +79,18 @@ export async function codeCommand({ task, config, logger, flags = {} }) {
|
|
|
89
79
|
logger.warn(result.error);
|
|
90
80
|
}
|
|
91
81
|
logger.info(`Coder completed (exit ${result.exitCode})`);
|
|
82
|
+
// KJC-TSK-0873: aqui es donde el panel SE CUMPLE — el coder declarado ha
|
|
83
|
+
// escrito. Queda asentado junto a las desviaciones para que el recuento de
|
|
84
|
+
// kj report signifique algo: casi siempre y casi nunca no se ven igual.
|
|
85
|
+
// Solo cuenta como cumplido si el coder declarado TERMINO bien: ni una
|
|
86
|
+
// salida distinta de cero ni una caida al fallback son "escribio el coder
|
|
87
|
+
// que elegiste" (dos catches de la review). Si escribio otro, se asienta
|
|
88
|
+
// como desviacion nombrandolo.
|
|
89
|
+
const ranBy = result.provider || coderRole.provider;
|
|
90
|
+
if (result.exitCode === 0) {
|
|
91
|
+
try { sealPanel({ projectDir: config.projectDir, coder: coderRole.provider, host: ranBy, honoured: ranBy === coderRole.provider }); }
|
|
92
|
+
catch (err) { logger.warn(`panel: no se pudo sellar quién escribió (${err.message}) — el trabajo del coder no se toca`); }
|
|
93
|
+
}
|
|
92
94
|
// The work is in the tree, not committed: the gate is the next step and a
|
|
93
95
|
// DIFFERENT AI runs it. Saying so here is what keeps the panel honest.
|
|
94
96
|
logger.info(`The work is in the tree. Review it before committing: kj review --staged (a different AI than ${coderRole.provider}).`);
|
package/src/commands/harden.js
CHANGED
|
@@ -153,7 +153,10 @@ export async function hardenCommand({
|
|
|
153
153
|
result.harnessHooks = hh.wired ? "wired" : "script-only";
|
|
154
154
|
// KJC-TSK-0713 — the Sentinel: method state + Stop gate (turn cannot
|
|
155
155
|
// end red). Same Claude-only harness surface as the tool gate.
|
|
156
|
-
|
|
156
|
+
// KJC-BUG-0193: solo un humano avanza lo que un humano selló. Sin
|
|
157
|
+
// --commit, un guardia sellado se queda como está en lugar de quedarse
|
|
158
|
+
// sin sello, que es como una sesión se dejaba sin herramientas.
|
|
159
|
+
const sh = installSentinelHooks({ projectDir, logger, human: commitSupervisor });
|
|
157
160
|
result.sentinelHooks = sh.wired ? "wired" : "script-only";
|
|
158
161
|
}
|
|
159
162
|
} catch (err) {
|
|
@@ -195,7 +198,11 @@ export async function hardenCommand({
|
|
|
195
198
|
logger.info?.(` • ${c.file}: ${c.action}${note}`);
|
|
196
199
|
}
|
|
197
200
|
for (const w of out.workflows) logger.info?.(` • ${w.file}: ${w.action}`);
|
|
198
|
-
for (const g of out.guidelines)
|
|
201
|
+
for (const g of out.guidelines) {
|
|
202
|
+
logger.info?.(` • ${g.file}: ${g.action}`);
|
|
203
|
+
// KJC-BUG-0206: quitar reglas no puede leerse como "updated" y nada más.
|
|
204
|
+
if (g.shrank) logger.warn?.(` ⚠ ${g.file} queda con MENOS reglas que antes — revisa si el lenguaje detectado es el que crees (kj check lo dice)`);
|
|
205
|
+
}
|
|
199
206
|
if (!dryRun) logger.info?.("core.hooksPath set. Verify later with `kj check`.");
|
|
200
207
|
// IDN-A (KJC-TSK-0762): a hardened clone declares who works it. Captured
|
|
201
208
|
// only with a human confirming; headless runs get the pending command.
|
package/src/commands/policy.js
CHANGED
|
@@ -15,6 +15,8 @@ import { checkStagedDiff, evalToolCall, loadPolicy } from "../policy/engine.js";
|
|
|
15
15
|
import { liftSealedSupervisorViolations } from "../policy/supervisor-verify.js";
|
|
16
16
|
import { loadExceptionRecords, loadGlobalExceptionRecords, loadStandingExceptions, recordPolicyException } from "../policy/exceptions.js";
|
|
17
17
|
import { buildPolicyReport } from "../policy/report.js";
|
|
18
|
+
import { budgetedNet } from "../review/loc-budget.js";
|
|
19
|
+
import { sealPanel } from "../environment/panel.js";
|
|
18
20
|
import { policyFileHash, recordGateDecision } from "../policy/decisions.js";
|
|
19
21
|
import { readIdentity } from "../identity/store.js";
|
|
20
22
|
import { verifyDecisionChain } from "@karajan-family/governance";
|
|
@@ -32,12 +34,10 @@ async function stagedFacts(projectDir, gitFn, range = null) {
|
|
|
32
34
|
.split("\n")
|
|
33
35
|
.map((s) => s.trim())
|
|
34
36
|
.filter(Boolean);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (r && r !== "-") net -= Number(r) || 0;
|
|
40
|
-
}
|
|
37
|
+
// KJC-BUG-0208: el presupuesto lo decide un solo modulo. Este sitio sumaba el
|
|
38
|
+
// numstat entero, asi que el job policy-check de CI contaba la documentacion
|
|
39
|
+
// y el gate bloqueante no: la misma regla con dos respuestas.
|
|
40
|
+
const { net } = budgetedNet(await run([...base, "--numstat"]));
|
|
41
41
|
return { files, netLinesAdded: net };
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -209,18 +209,23 @@ export async function policyCommand({ action, config = {}, flags = {}, logger =
|
|
|
209
209
|
// identidad DECLARADA del clon (identity.local.yml), para que el informe
|
|
210
210
|
// y el anchor la vean. Lo llama el Sentinel; cualquiera puede auditarlo.
|
|
211
211
|
if (action === "seal") {
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
// KJC-TSK-0868: el mismo asiento sirve para el panel — el anfitrión escribió
|
|
213
|
+
// teniendo otro coder declarado. Mismo log encadenado, otro chokepoint.
|
|
214
|
+
if (!flags.escape && !flags.panel) {
|
|
215
|
+
logger.error?.("policy seal: --escape <KJ_ALLOW_X> o --panel <coder> es obligatorio — un asiento sin nombre no es auditable");
|
|
214
216
|
return 1;
|
|
215
217
|
}
|
|
216
218
|
try {
|
|
217
219
|
const who = readIdentity(projectDir);
|
|
218
|
-
const rec =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
const rec = flags.panel
|
|
221
|
+
? sealPanel({ projectDir, coder: flags.panel, host: flags.host ?? null, honoured: Boolean(flags.honoured) })
|
|
222
|
+
: recordGateDecision(projectDir, {
|
|
223
|
+
decision: "exempt", chokepoint: "tool", escape: flags.escape, tool: flags.tool ?? null,
|
|
224
|
+
who: who ? { gh: who.gh_user ?? null, git: who.git_email ?? null, grade: "declarada" } : null,
|
|
225
|
+
policy_hash: policyFileHash(projectDir),
|
|
226
|
+
});
|
|
227
|
+
if (rec.chokepoint === "panel") logger.info?.(`✓ panel sellado: el coder declarado es ${rec.coder} y escribió ${rec.host ?? "el anfitrión"}`);
|
|
228
|
+
else logger.info?.(`✓ escape ${rec.escape} sellado (chokepoint tool${rec.tool ? `, ${rec.tool}` : ""})`);
|
|
224
229
|
return 0;
|
|
225
230
|
} catch (err) {
|
|
226
231
|
logger.error?.(`policy seal: ${err.message}`);
|
|
@@ -238,6 +243,8 @@ export async function policyCommand({ action, config = {}, flags = {}, logger =
|
|
|
238
243
|
const sup = liftSealedSupervisorViolations({ projectDir, violations });
|
|
239
244
|
if (sup.lifted > 0) {
|
|
240
245
|
logger.info?.(`✓ policy: ${sup.lifted} fichero(s) de supervisor verificados contra la provenance sellada (ADR 0009)`);
|
|
246
|
+
// KJC-BUG-0212: lo que este sitio no ha podido juzgar se dice, no se calla.
|
|
247
|
+
if (sup.note) logger.info?.(`ℓ policy: ${sup.note}`);
|
|
241
248
|
violations = sup.violations;
|
|
242
249
|
}
|
|
243
250
|
const hard = flags.strict ? violations.filter((v) => v.enforcement === "deny") : [];
|
package/src/commands/rag.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { openVecStore, countChunks, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
|
|
6
6
|
import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
|
|
7
7
|
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
8
|
+
import { fileIndexState } from "../rag/coverage.js";
|
|
8
9
|
import { query } from "../rag/retriever.js";
|
|
9
10
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
10
11
|
import { indexLibrary, LIBRARY_PROJECT } from "../rag/library.js";
|
|
@@ -171,3 +172,23 @@ export async function ragEvalCommand({ config, logger, flags = {} }) {
|
|
|
171
172
|
db.close();
|
|
172
173
|
}
|
|
173
174
|
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* KJC-BUG-0216 (issue #1807) — el hecho comprobable que el gate rag-first
|
|
178
|
+
* necesita antes de exigir una consulta: PUEDE el RAG responder sobre este
|
|
179
|
+
* fichero. Exit 0 = si; exit 1 = no, y la razon (sin adapter, segmento
|
|
180
|
+
* excluido, indice vacio o por detras) va en la salida.
|
|
181
|
+
*/
|
|
182
|
+
export async function ragCoversCommand({ file, config, flags = {} }) {
|
|
183
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
184
|
+
const db = openDb(config);
|
|
185
|
+
try {
|
|
186
|
+
const state = fileIndexState(projectDir, file, { db });
|
|
187
|
+
if (flags.json) process.stdout.write(`${JSON.stringify(state)}\n`);
|
|
188
|
+
else process.stdout.write(`${state.state}: ${state.reason}\n`);
|
|
189
|
+
if (!state.canAnswer) process.exitCode = 1;
|
|
190
|
+
return state;
|
|
191
|
+
} finally {
|
|
192
|
+
db.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/commands/report.js
CHANGED
|
@@ -226,6 +226,26 @@ export function formatSentinelEscapes(state) {
|
|
|
226
226
|
return `Sentinel escapes used (${events.length}):\n${lines.join("\n")}`;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* KJC-TSK-0873 — the panel tally. The user chose who writes; this says how
|
|
231
|
+
* often it happened. One number alone would not: a project where the panel is
|
|
232
|
+
* honoured almost always and one where it almost never is look identical until
|
|
233
|
+
* both sides are counted.
|
|
234
|
+
*/
|
|
235
|
+
export function formatPanelTally(lines) {
|
|
236
|
+
let honoured = 0;
|
|
237
|
+
let host = 0;
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
let rec;
|
|
240
|
+
try { rec = JSON.parse(line); } catch { continue; }
|
|
241
|
+
if (rec?.chokepoint !== "panel") continue;
|
|
242
|
+
if (rec.decision === "ok") honoured += 1;
|
|
243
|
+
else host += 1;
|
|
244
|
+
}
|
|
245
|
+
if (honoured + host === 0) return null;
|
|
246
|
+
return `Panel (who wrote): declared coder ${honoured}, host ${host}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
229
249
|
// The sentinel state is a PROJECT artifact (<project>/.karajan/harness), not
|
|
230
250
|
// a session artifact: the `dir` the report handlers receive is the GLOBAL
|
|
231
251
|
// sessions root (~/.karajan/sessions) and never contains it. `kj report` is
|
|
@@ -241,6 +261,14 @@ async function printSentinelEscapes({ projectDir = process.cwd(), format } = {})
|
|
|
241
261
|
console.log(text);
|
|
242
262
|
}
|
|
243
263
|
} catch { /* no sentinel state in this project — nothing to report */ }
|
|
264
|
+
try {
|
|
265
|
+
const log = await fs.readFile(path.join(projectDir, ".karajan", "policy-decisions.jsonl"), "utf8");
|
|
266
|
+
const tally = formatPanelTally(log.split("\n").filter((l) => l.trim()));
|
|
267
|
+
if (tally) {
|
|
268
|
+
console.log("");
|
|
269
|
+
console.log(tally);
|
|
270
|
+
}
|
|
271
|
+
} catch { /* no decision log — no panel to tally */ }
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
function formatDuration(ms) {
|
|
@@ -23,6 +23,7 @@ import { liftSealedSupervisorViolations } from "../policy/supervisor-verify.js";
|
|
|
23
23
|
import { checkTestsWithCode } from "../review/tests-with-code.js";
|
|
24
24
|
import { loadPrivacyList, scanText } from "../privacy/scan.js";
|
|
25
25
|
import { isGeneratedPath, splitAddedByFile } from "../privacy/diff-scope.js";
|
|
26
|
+
import { budgetedAdded, budgetedNet } from "../review/loc-budget.js";
|
|
26
27
|
import { checkStagedDiff, loadPolicy } from "../policy/engine.js";
|
|
27
28
|
import { loadStandingExceptions, recordPolicyException } from "../policy/exceptions.js";
|
|
28
29
|
import { policyFileHash, recordGateDecision } from "../policy/decisions.js";
|
|
@@ -203,14 +204,17 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
203
204
|
const sizeSource = locInv ? `policy [${locInv.id}]` : "method_gates";
|
|
204
205
|
if (sizeWarn > 0) {
|
|
205
206
|
const numstat = await rawDiff(flags.range, ["--numstat"]);
|
|
206
|
-
|
|
207
|
+
// KJC-BUG-0205: the budget caps CODE growth, so generated output and human
|
|
208
|
+
// documentation are exempt — the same rule CI applies. Counting everything
|
|
209
|
+
// here warned 234 lines on a doc-only PR that CI counted at 104, and two
|
|
210
|
+
// implementations of one rule that disagree teach people to trust neither.
|
|
211
|
+
const { added, exempt, testAdded } = budgetedAdded(numstat);
|
|
212
|
+
// Said out loud: an exemption nobody sees is indistinguishable from a gate
|
|
213
|
+
// that is not looking.
|
|
214
|
+
if (exempt > 0) console.log(`ℓ pr-size: ${exempt} line(s) exempt from the budget (generated output and human docs; AI-rule files always count)`);
|
|
207
215
|
if (added > sizeWarn) {
|
|
208
216
|
// KJC-TSK-0795 AC5: say how much of the weight is the module's OWN tests —
|
|
209
217
|
// partitioning is a decision, and it must never split code from its tests.
|
|
210
|
-
const testAdded = numstat.split("\n").reduce((acc, l) => {
|
|
211
|
-
const [a, , ...f] = l.split("\t");
|
|
212
|
-
return acc + (/\/tests?\/|__tests__\/|\.test\.|\.spec\./.test(`/${f.join("\t")}`) ? Number(a) || 0 : 0);
|
|
213
|
-
}, 0);
|
|
214
218
|
const split = testAdded > 0 ? ` (${added - testAdded} source + ${testAdded} accompanying tests — partition by feature, never code from its tests)` : "";
|
|
215
219
|
const sizePolicy = config?.method_gates?.pr_size || "warn";
|
|
216
220
|
if (process.env.KJ_ALLOW_LARGE_PR === "1") {
|
|
@@ -289,12 +293,10 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
289
293
|
// hooks. enforcement=warn avisa; deny cierra salvo excepción probatoria
|
|
290
294
|
// (KJ_ALLOW_POLICY=1 + KJ_POLICY_REASON, registrada con identidad y hash
|
|
291
295
|
// del diff); class=security cierra sin escape y sin arbitraje.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (r && r !== "-") net -= Number(r) || 0;
|
|
297
|
-
}
|
|
296
|
+
// KJC-BUG-0208: el mismo modulo que decide el presupuesto decide el metrico
|
|
297
|
+
// del invariante. Sumar aqui el numstat entero daba dos cifras para la misma
|
|
298
|
+
// regla en dos lineas seguidas del mismo comando.
|
|
299
|
+
const { net } = budgetedNet(await rawDiff(flags.range, ["--numstat"]));
|
|
298
300
|
// GOV-B (KJC-TSK-0746): las permanentes concedidas (kj policy grant)
|
|
299
301
|
// eximen su regla mientras viven; el descarte de líneas corruptas se dice.
|
|
300
302
|
const std = loadStandingExceptions(projectDir);
|
package/src/config/defaults.js
CHANGED
|
@@ -68,7 +68,10 @@ const DEFAULTS = {
|
|
|
68
68
|
state_backend: "hu-board",
|
|
69
69
|
// verify_cmd (KJC-TSK-0732): user adapter that checks a card ref against
|
|
70
70
|
// the real tracker — prints {exists, live, status} JSON; {ref} substituted.
|
|
71
|
-
|
|
71
|
+
// unverifiable (KJC-TSK-0878): que hacer cuando el adaptador no puede
|
|
72
|
+
// responder (sin red, sin credenciales, JSON invalido). "warn" degrada a
|
|
73
|
+
// branch-ref diciendolo; "block" se niega a aprobar lo que no ha comprobado.
|
|
74
|
+
board: { name: null, verify_cmd: null, unverifiable: "warn" },
|
|
72
75
|
// Method gates (KJC-PCS-0068): rules climb from playbook text to
|
|
73
76
|
// deterministic enforcement. card_first: "auto" = block with hu-board
|
|
74
77
|
// (fully verifiable), warn with planning-game/external (presence only);
|
|
@@ -91,6 +94,11 @@ const DEFAULTS = {
|
|
|
91
94
|
coder_rules: "./.karajan/coder-rules.md",
|
|
92
95
|
base_branch: "main",
|
|
93
96
|
coder_options: { model: null, auto_approve: true, fallback_coder: null },
|
|
97
|
+
// KJC-BUG-0217: los flags con los que kj invoca el CLI de cada provider son
|
|
98
|
+
// los que su version acepta, y eso cambia con el CLI. Aqui se declaran por
|
|
99
|
+
// agente cuando los de serie no valen: { kimi: { task_args: [] } } deja el
|
|
100
|
+
// prompt solo, sin auto-aprobado, para una version que rechaza la combinacion.
|
|
101
|
+
agents: {},
|
|
94
102
|
reviewer_options: {
|
|
95
103
|
output_format: "json",
|
|
96
104
|
require_schema: true,
|
|
@@ -256,6 +264,10 @@ const DEFAULTS = {
|
|
|
256
264
|
enabled: true,
|
|
257
265
|
patterns: [],
|
|
258
266
|
protected_files: [],
|
|
267
|
+
// KJC-BUG-0215: cauce declarado para el falso positivo de las reglas que
|
|
268
|
+
// acusan por el NOMBRE del identificador. Entradas `NAME` (todo el repo)
|
|
269
|
+
// o `path/to/file:NAME` (solo ese fichero, que es lo preferible).
|
|
270
|
+
non_secret_assignments: [],
|
|
259
271
|
on_violation: "block"
|
|
260
272
|
},
|
|
261
273
|
perf: {
|