karajan-code 4.32.0 → 4.33.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/aider-agent.js +2 -12
- package/src/agents/base-agent.js +9 -20
- package/src/agents/claude-agent.js +2 -12
- package/src/agents/codex-agent.js +2 -12
- package/src/agents/dead-models.js +74 -0
- package/src/agents/gemini-agent.js +2 -12
- package/src/agents/model-errors.js +35 -0
- package/src/agents/opencode-agent.js +2 -12
- package/src/brain/agent-error-classifier.js +19 -1
- package/src/brain/role-fallback-chain.js +100 -0
- package/src/brain/with-brain-recovery.js +59 -3
- package/src/checks/action-pins.js +131 -0
- package/src/checks/project-checks.js +6 -0
- package/src/cli/register-pipeline.js +11 -1
- package/src/commands/check.js +5 -0
- package/src/commands/code.js +57 -4
- package/src/commands/env.js +2 -1
- package/src/commands/harden.js +3 -1
- package/src/commands/review-gate.js +12 -4
- package/src/environment/panel.js +52 -0
- package/src/environment/playbook.js +7 -6
- package/src/harden/config-templates.js +7 -1
- package/src/harden/guidelines-engine.js +7 -3
- package/src/harden/guidelines-templates.js +60 -10
- package/src/harden/hook-templates.js +4 -1
- package/src/privacy/diff-scope.js +58 -0
- package/src/privacy/scan.js +18 -0
- package/src/prompts/card-context.js +57 -0
- package/src/prompts/session-context.js +68 -0
- package/src/roles/agent-role.js +11 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which part of a diff the privacy gate reads, and how (KJC-BUG-0203).
|
|
3
|
+
*
|
|
4
|
+
* The gate used to concatenate every added line of the diff and scan the lump.
|
|
5
|
+
* Two consequences: a finding could not name the file it came from, and build
|
|
6
|
+
* output got judged as if a person had written it. A minified Starlight page
|
|
7
|
+
* warned twice for `[phone]` over an Astro class hash, which is the kind of
|
|
8
|
+
* false alarm that teaches people to skip the gate.
|
|
9
|
+
*
|
|
10
|
+
* The exemption is by SEVERITY, not by file. Generic heuristics (a shape that
|
|
11
|
+
* could be a phone, a card, an id) are silenced on generated output, where
|
|
12
|
+
* nobody typed anything. A denylist hit is NOT: the incident that created this
|
|
13
|
+
* scanner was personal emails published on a landing, which is exactly a
|
|
14
|
+
* denylist datum inside a build artifact.
|
|
15
|
+
*
|
|
16
|
+
* `kj privacy scan <dir>` is untouched. Scanning an artifact you are about to
|
|
17
|
+
* publish must keep looking at everything, generated or not.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Paths that are build output in this project's own vocabulary. */
|
|
21
|
+
const GENERATED = [
|
|
22
|
+
/(^|\/)dist\//,
|
|
23
|
+
/(^|\/)build\//,
|
|
24
|
+
/(^|\/)coverage\//,
|
|
25
|
+
/(^|\/)node_modules\//,
|
|
26
|
+
/(^|\/)\.astro\//,
|
|
27
|
+
/(^|\/)public\/docs\//,
|
|
28
|
+
/\.min\.(js|css)$/,
|
|
29
|
+
/\.map$/,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** @param {string} file @returns {boolean} */
|
|
33
|
+
export function isGeneratedPath(file) {
|
|
34
|
+
if (!file) return false;
|
|
35
|
+
return GENERATED.some((re) => re.test(file));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Split a unified diff into its added lines, per file.
|
|
40
|
+
* @param {string} diff
|
|
41
|
+
* @returns {Array<{file: string, added: string}>} files with at least one added line
|
|
42
|
+
*/
|
|
43
|
+
export function splitAddedByFile(diff) {
|
|
44
|
+
const out = [];
|
|
45
|
+
let current = null;
|
|
46
|
+
for (const line of String(diff || "").split("\n")) {
|
|
47
|
+
const header = /^diff --git a\/(?:.+) b\/(.+)$/.exec(line);
|
|
48
|
+
if (header) {
|
|
49
|
+
current = { file: header[1], lines: [] };
|
|
50
|
+
out.push(current);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!current) continue;
|
|
54
|
+
// `+++ b/x` is the header, not content; `+` alone is an added blank line.
|
|
55
|
+
if (line.startsWith("+") && !line.startsWith("+++")) current.lines.push(line.slice(1));
|
|
56
|
+
}
|
|
57
|
+
return out.filter((f) => f.lines.length > 0).map((f) => ({ file: f.file, added: f.lines.join("\n") }));
|
|
58
|
+
}
|
package/src/privacy/scan.js
CHANGED
|
@@ -73,6 +73,24 @@ const FIREBASE_KEY_HINT = "si es la clave web de Firebase (publica por diseno):
|
|
|
73
73
|
const CONTEXT_DISCARDS = [
|
|
74
74
|
{ type: "git-sha", re: /\b(?:[0-9a-f]{64}|[0-9a-f]{40})\b/g },
|
|
75
75
|
{ type: "doc-domain-email", re: /\b[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)*(?:example\.(?:com|org|net)|test|invalid|localhost|example)\b/g },
|
|
76
|
+
// KJC-BUG-0202: an HU Board id is `HU-<Date.now()>-<n>`, and Date.now() is
|
|
77
|
+
// 13 digits — exactly a card's length, so every project using the default
|
|
78
|
+
// board got a credit-card warning over its own card ids. The rule covers
|
|
79
|
+
// the family (an alphabetic prefix, a dash, a long digit run: HU-…, KJC-…,
|
|
80
|
+
// any tracker's) and NOTHING else.
|
|
81
|
+
//
|
|
82
|
+
// It deliberately does not go further, and it took three rejections to get
|
|
83
|
+
// this narrow:
|
|
84
|
+
// 1. discarding every card-shaped run that fails Luhn — wrong, a
|
|
85
|
+
// truncated or mistyped card also fails Luhn and is still sensitive;
|
|
86
|
+
// 2. any alphabetic prefix — `card-4111111111111111` swallowed;
|
|
87
|
+
// 3. any uppercase prefix with any digits — `CARD-4111111111111111-1`
|
|
88
|
+
// swallowed.
|
|
89
|
+
// So the middle segment must be an actual millisecond timestamp: exactly 13
|
|
90
|
+
// digits starting 16-19 (years 2022-2033). No card range begins with a 1
|
|
91
|
+
// (Visa 4, Mastercard 2/5, Amex 3, Discover 6), so a card number cannot
|
|
92
|
+
// occupy that slot at any length.
|
|
93
|
+
{ type: "tracker-id", re: /\b[A-Z]{2,6}-1[6-9]\d{11}-\d{1,4}\b/g },
|
|
76
94
|
];
|
|
77
95
|
|
|
78
96
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The coder gets the CARD, not just a sentence (KJC-TSK-0864).
|
|
3
|
+
*
|
|
4
|
+
* `kj code` handed the agent a bare task string: no card, no acceptance
|
|
5
|
+
* criteria, not even the project boundary the pipeline always passed. That is
|
|
6
|
+
* the gap the user hit, because when the host orchestrates instead of
|
|
7
|
+
* `kj run`, `kj code` IS the way the declared coder gets invoked, and a coder
|
|
8
|
+
* without the card writes to a sentence instead of to a contract.
|
|
9
|
+
*
|
|
10
|
+
* A local HU is read from the board. A reference kj cannot read (an external
|
|
11
|
+
* board lives in the host's own MCP, not here) travels as the reference it is
|
|
12
|
+
* and says so: the coder learns the card exists and where to ask for it,
|
|
13
|
+
* instead of receiving a silently empty context.
|
|
14
|
+
*/
|
|
15
|
+
import { getHu } from "../hu/store.js";
|
|
16
|
+
|
|
17
|
+
/** Ids minted by the HU Board; anything else belongs to an external board. */
|
|
18
|
+
const LOCAL_HU = /^HU-/i;
|
|
19
|
+
|
|
20
|
+
const externalSection = (ref) =>
|
|
21
|
+
[
|
|
22
|
+
`## Card ${ref}`,
|
|
23
|
+
"",
|
|
24
|
+
`This work is tracked as ${ref} on the project's own board, which kj cannot read from here.`,
|
|
25
|
+
"Treat the task below as the card's statement, and if something the card should answer is missing, ASK instead of guessing.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
const localSection = (hu) => {
|
|
29
|
+
const lines = [`## Card ${hu.id} — ${hu.title}`, ""];
|
|
30
|
+
if (hu.description?.trim()) lines.push(hu.description.trim(), "");
|
|
31
|
+
lines.push(`Status on the board: ${hu.status}. The card is the contract: done means its statement is literally true.`);
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{projectDir: string, ref: string|null, deps?: {getHu?: Function}}} args
|
|
37
|
+
* @returns {Promise<null|{huId: string, title: string|null, section: string,
|
|
38
|
+
* acceptanceTests: Array|null, external: boolean}>}
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveCardContext({ projectDir, ref, deps = {} }) {
|
|
41
|
+
if (!ref?.trim()) return null;
|
|
42
|
+
const id = ref.trim();
|
|
43
|
+
if (!LOCAL_HU.test(id)) {
|
|
44
|
+
return { huId: id, title: null, section: externalSection(id), acceptanceTests: null, external: true };
|
|
45
|
+
}
|
|
46
|
+
// A missing local HU is an error, never an empty context: the caller asked
|
|
47
|
+
// for a card by id and kj either delivers it or says it does not exist.
|
|
48
|
+
const hu = await (deps.getHu || getHu)(projectDir, id);
|
|
49
|
+
const criteria = hu.acceptanceCriteria?.trim();
|
|
50
|
+
return {
|
|
51
|
+
huId: hu.id,
|
|
52
|
+
title: hu.title,
|
|
53
|
+
section: localSection(hu),
|
|
54
|
+
acceptanceTests: criteria ? [{ type: "gherkin", content: criteria }] : null,
|
|
55
|
+
external: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the coder knows before it writes (KJC-TSK-0864).
|
|
3
|
+
*
|
|
4
|
+
* The method's first invariant is "the RAG answers before you assume", and
|
|
5
|
+
* `kj run` honours it through the researcher stage. `kj code` did not: the
|
|
6
|
+
* declared coder started from a sentence and guessed the codebase. So the
|
|
7
|
+
* session asks the index on the coder's behalf and hands it what came back.
|
|
8
|
+
*
|
|
9
|
+
* Retrieval is best-effort and LOUD: an empty or unreachable index warns and
|
|
10
|
+
* the coder works without project context, because blocking here would stop
|
|
11
|
+
* work in a repo that simply has no index yet. What it never does is stay
|
|
12
|
+
* quiet about it.
|
|
13
|
+
*/
|
|
14
|
+
import { ragQueryCommand } from "../commands/rag.js";
|
|
15
|
+
|
|
16
|
+
/** Hits to carry and how much of each: the prompt pays for every line. */
|
|
17
|
+
const TOP_K = 5;
|
|
18
|
+
const EXCERPT = 400;
|
|
19
|
+
|
|
20
|
+
const label = (hit) =>
|
|
21
|
+
hit.metadata?.symbol || hit.metadata?.hu_id || hit.metadata?.headingPath?.join(" > ") || hit.kind || "block";
|
|
22
|
+
|
|
23
|
+
/** @returns {string|null} the agent-facing section, or null when there is nothing to say. */
|
|
24
|
+
export function ragSection(hits) {
|
|
25
|
+
if (!Array.isArray(hits) || hits.length === 0) return null;
|
|
26
|
+
const lines = [
|
|
27
|
+
"## What the project's RAG index answers about this task",
|
|
28
|
+
"",
|
|
29
|
+
"Retrieved for you, so you do not guess what the codebase does. These are excerpts: open the file before changing it.",
|
|
30
|
+
"",
|
|
31
|
+
];
|
|
32
|
+
for (const hit of hits) {
|
|
33
|
+
const text = hit.text || "";
|
|
34
|
+
lines.push(`### ${hit.source} · ${label(hit)}`);
|
|
35
|
+
lines.push("```", text.length > EXCERPT ? `${text.slice(0, EXCERPT)}…` : text, "```", "");
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @returns {Promise<null|{section: string|null, sources: string[]}>}
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveRagContext({ task, config, logger, deps = {} }) {
|
|
44
|
+
const run = deps.ragQueryCommand || ragQueryCommand;
|
|
45
|
+
// The index warns on stdout in CLI mode; here the hits are the product, so
|
|
46
|
+
// its narration is swallowed and only OUR verdict reaches the user.
|
|
47
|
+
const quiet = { info: () => {}, warn: () => {}, error: () => {} };
|
|
48
|
+
let hits;
|
|
49
|
+
try {
|
|
50
|
+
hits = await run({ text: task, config, logger: quiet, flags: { topK: TOP_K } });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger?.warn?.(`rag context unavailable (${err.message}) — the coder writes without project context`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!hits?.length) {
|
|
56
|
+
logger?.warn?.("the RAG index returned nothing for this task — the coder writes without project context");
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const sources = [...new Set(hits.map((h) => h.source))];
|
|
60
|
+
logger?.info?.(`RAG context: ${sources.length} file(s) — ${sources.join(", ")}`);
|
|
61
|
+
return { section: ragSection(hits), sources };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The task the coder reads: everything it was given, then its own statement. */
|
|
65
|
+
export function composeTask(task, sections = []) {
|
|
66
|
+
const kept = sections.filter(Boolean);
|
|
67
|
+
return kept.length ? `${kept.join("\n\n")}\n\n## Task\n\n${task}` : task;
|
|
68
|
+
}
|
package/src/roles/agent-role.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { BaseRole } from "./base-role.js";
|
|
17
17
|
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
18
18
|
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
19
|
+
import { buildRoleFallbackChain } from "../brain/role-fallback-chain.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Silence timeout (ms) for an agent subprocess, from
|
|
@@ -154,10 +155,18 @@ export class AgentRole extends BaseRole {
|
|
|
154
155
|
// reviewer usa reviewTask) también funcionan: el wrapper sólo necesita
|
|
155
156
|
// un objeto con la firma `runTask(args) → { ok, output, error, exitCode }`.
|
|
156
157
|
const result = await withBrainRecovery({
|
|
157
|
-
agent: { runTask: (args) => agent[this.agentMethod](args), provider },
|
|
158
|
+
agent: { runTask: (args) => agent[this.agentMethod](args), provider, model: this.config?.roles?.[this.name]?.model ?? null },
|
|
158
159
|
taskArgs: runArgs,
|
|
159
160
|
role: this.name,
|
|
160
161
|
provider,
|
|
162
|
+
// KJC-TSK-0859: the chain the user declared under roles.<role>.fallback.
|
|
163
|
+
// The schema and the walker existed since KJC-TSK-0415; nobody built it,
|
|
164
|
+
// so every declared chain was dead config. This is the single point that
|
|
165
|
+
// covers every role inheriting from AgentRole.
|
|
166
|
+
fallback: buildRoleFallbackChain({
|
|
167
|
+
config: this.config, role: this.name, agentMethod: this.agentMethod,
|
|
168
|
+
createAgentFn: this._createAgent, logger: this.logger,
|
|
169
|
+
}),
|
|
161
170
|
emitter: this.emitter,
|
|
162
171
|
logger: this.logger,
|
|
163
172
|
// KJC: sessionState lets withBrainRecovery persist a hibernating run
|
|
@@ -171,7 +180,7 @@ export class AgentRole extends BaseRole {
|
|
|
171
180
|
const recoveryNote = result.recovery?.class ? ` [${result.recovery.class}]` : "";
|
|
172
181
|
return {
|
|
173
182
|
ok: false,
|
|
174
|
-
result: { error: result.error || result.output || `${this.name} failed${recoveryNote}`, provider, recovery: result.recovery, action: result.action, standbyFile: result.standbyFile || null },
|
|
183
|
+
result: { error: result.error || result.output || `${this.name} failed${recoveryNote}`, provider, recovery: result.recovery, action: result.action, standbyFile: result.standbyFile || null, tried: result.tried || null },
|
|
175
184
|
summary: `${this.name} failed${recoveryNote}: ${result.recovery?.message || result.error || "unknown error"}`,
|
|
176
185
|
usage: result.usage
|
|
177
186
|
};
|