karajan-code 3.12.3 → 3.13.1
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/config/defaults.js +4 -1
- package/src/config/overrides.js +8 -2
- package/src/harden/advisory.js +23 -4
- package/src/harden/alternatives.js +50 -0
- package/src/harden/check.js +11 -0
- package/src/harden/config-engine.js +19 -3
- package/src/orchestrator/flow-control.js +5 -1
- package/templates/kj.config.yml +3 -1
package/package.json
CHANGED
package/src/config/defaults.js
CHANGED
|
@@ -55,7 +55,10 @@ const DEFAULTS = {
|
|
|
55
55
|
review_mode: "standard",
|
|
56
56
|
max_iterations: 5,
|
|
57
57
|
hu_max_iterations: 3,
|
|
58
|
-
|
|
58
|
+
// Hard per-run spend ceiling, ON by default (KJC-TSK-0621): a stuck or
|
|
59
|
+
// runaway run must never drain a subscription quota unattended. Explicit
|
|
60
|
+
// null in the user's config opts out (no cap).
|
|
61
|
+
max_budget_usd: 5,
|
|
59
62
|
review_rules: "./.karajan/review-rules.md",
|
|
60
63
|
coder_rules: "./.karajan/coder-rules.md",
|
|
61
64
|
base_branch: "main",
|
package/src/config/overrides.js
CHANGED
|
@@ -172,8 +172,14 @@ export function applyRunOverrides(config, flags) {
|
|
|
172
172
|
out.git = out.git || {};
|
|
173
173
|
out.development = out.development || {};
|
|
174
174
|
out.sonarqube = out.sonarqube || {};
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// Precedence: explicit top-level value (including null = opt-out) wins;
|
|
176
|
+
// the legacy session.max_budget_usd location next; the shipped default
|
|
177
|
+
// (KJC-TSK-0621: 5) last. When top-level still carries the default, a
|
|
178
|
+
// session-level value — including an explicit null — takes over.
|
|
179
|
+
if (out.max_budget_usd === undefined) {
|
|
180
|
+
out.max_budget_usd = out.session.max_budget_usd !== undefined ? out.session.max_budget_usd : DEFAULTS.max_budget_usd;
|
|
181
|
+
} else if (out.max_budget_usd === DEFAULTS.max_budget_usd && out.session.max_budget_usd !== undefined) {
|
|
182
|
+
out.max_budget_usd = out.session.max_budget_usd;
|
|
177
183
|
}
|
|
178
184
|
out.budget = mergeDeep(DEFAULTS.budget, out.budget || {});
|
|
179
185
|
out.roles = mergeDeep(DEFAULTS.roles, out.roles || {});
|
package/src/harden/advisory.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
|
|
14
14
|
import { findManagedBlock } from "../utils/managed-markers.js";
|
|
15
|
+
import { findAlternative } from "./alternatives.js";
|
|
15
16
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
16
17
|
import { detectStackRoots } from "./stack-roots.js";
|
|
17
18
|
|
|
@@ -98,6 +99,13 @@ function readArtifactContent(searchDir, foundAt) {
|
|
|
98
99
|
|
|
99
100
|
/** Map a classified state to a recommendation + human rationale. */
|
|
100
101
|
function recommend(state) {
|
|
102
|
+
if (state.status === "SATISFIED_BY_ALTERNATIVE") {
|
|
103
|
+
return {
|
|
104
|
+
recommendation: "keep",
|
|
105
|
+
rationale: `covered by ${state.foundAt} (${state.alternativeTool}) — kj won't add a second linter/formatter`,
|
|
106
|
+
mergeable: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
101
109
|
if (state.status === "MISSING") return { recommendation: "install", rationale: `kj would add ${state.file}`, mergeable: true };
|
|
102
110
|
if (state.status === "USER_OWNED") {
|
|
103
111
|
const n = state.improvements?.length ?? 0;
|
|
@@ -109,11 +117,20 @@ function recommend(state) {
|
|
|
109
117
|
return { recommendation: "update", rationale: `kj v${state.currentVersion} available (you have v${state.managedVersion})`, mergeable: true };
|
|
110
118
|
}
|
|
111
119
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Classify a single artifact found (or not) under `searchDir`. `altDirs`
|
|
122
|
+
* widens the cross-tool alternative lookup (a root-level biome.json covers a
|
|
123
|
+
* monorepo's language roots); the user's OWN config of the same tool still
|
|
124
|
+
* wins over an alternative.
|
|
125
|
+
*/
|
|
126
|
+
export function classifyArtifact(searchDir, artifact, altDirs = [searchDir]) {
|
|
114
127
|
const foundAt = findArtifactFile(searchDir, artifact);
|
|
115
128
|
const base = { foundAt, managedVersion: null, upToDate: null, improvements: [] };
|
|
116
129
|
let state = { ...base, status: "MISSING" };
|
|
130
|
+
if (!foundAt) {
|
|
131
|
+
const alt = findAlternative(altDirs, artifact.id);
|
|
132
|
+
if (alt) state = { ...base, status: "SATISFIED_BY_ALTERNATIVE", foundAt: alt.foundAt, alternativeTool: alt.tool };
|
|
133
|
+
}
|
|
117
134
|
if (foundAt) {
|
|
118
135
|
const content = readArtifactContent(searchDir, foundAt);
|
|
119
136
|
if (artifact.blockId && content.includes(`kj:managed:${artifact.blockId}`)) {
|
|
@@ -139,8 +156,10 @@ export function compareHarden({ projectDir = process.cwd(), profile = "standard"
|
|
|
139
156
|
for (const { dir, language } of detectStackRoots(projectDir, { only, exclude })) {
|
|
140
157
|
const searchDir = dir === "." ? projectDir : join(projectDir, dir);
|
|
141
158
|
for (const cfg of CONFIGS_BY_LANGUAGE[language] ?? []) {
|
|
142
|
-
const res = classifyArtifact(searchDir, toArtifact(cfg));
|
|
143
|
-
|
|
159
|
+
const res = classifyArtifact(searchDir, toArtifact(cfg), [searchDir, projectDir]);
|
|
160
|
+
// Alternative matches may live at the repo root — keep their path unprefixed.
|
|
161
|
+
const prefix = res.foundAt && dir !== "." && res.status !== "SATISFIED_BY_ALTERNATIVE";
|
|
162
|
+
artifacts.push({ dir, ...res, foundAt: prefix ? join(dir, res.foundAt) : res.foundAt });
|
|
144
163
|
}
|
|
145
164
|
}
|
|
146
165
|
return { artifacts };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tool alternatives for `kj harden` (KJC-TSK-0614).
|
|
3
|
+
*
|
|
4
|
+
* A user's Biome config replaces BOTH eslint and prettier — seeding kj's
|
|
5
|
+
* configs next to biome.json installs a second, conflicting linter/formatter
|
|
6
|
+
* that fights the user's own hooks. The advisory's EQUIVALENTS map only
|
|
7
|
+
* covers same-tool filename variants; this map covers whole-tool
|
|
8
|
+
* substitutions, and the three surfaces (advisory, config-engine, check)
|
|
9
|
+
* consult it before treating an artifact as missing.
|
|
10
|
+
*
|
|
11
|
+
* Detection is config-file based on purpose: deterministic, and a false
|
|
12
|
+
* negative just means kj proposes its default (the pre-0614 behaviour),
|
|
13
|
+
* never that it overwrites the user's tool.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
const ALTERNATIVES = {
|
|
20
|
+
eslint: [{ tool: "biome", files: ["biome.json", "biome.jsonc"] }],
|
|
21
|
+
prettier: [{ tool: "biome", files: ["biome.json", "biome.jsonc"] }],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* First alternative tool that satisfies `artifactId`, searching each dir in
|
|
26
|
+
* order (a language root first, then the repo root — a root-level biome.json
|
|
27
|
+
* covers the whole monorepo). Null when none applies.
|
|
28
|
+
*
|
|
29
|
+
* @param {string[]} searchDirs
|
|
30
|
+
* @param {string} artifactId - "eslint" | "prettier" | ...
|
|
31
|
+
* @returns {{ tool: string, foundAt: string }|null}
|
|
32
|
+
*/
|
|
33
|
+
export function findAlternative(searchDirs, artifactId) {
|
|
34
|
+
for (const alt of ALTERNATIVES[artifactId] ?? []) {
|
|
35
|
+
for (const dir of searchDirs) {
|
|
36
|
+
for (const rel of alt.files) {
|
|
37
|
+
if (existsSync(join(dir, rel))) return { tool: alt.tool, foundAt: rel };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Artifact id for a config-template entry — same convention as the
|
|
46
|
+
* advisory's toArtifact: prettier is the only marker-less JSON config.
|
|
47
|
+
*/
|
|
48
|
+
export function artifactIdForConfig(cfg) {
|
|
49
|
+
return cfg.blockId ?? "prettier";
|
|
50
|
+
}
|
package/src/harden/check.js
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
13
|
import { runCommand } from "../utils/process.js";
|
|
14
|
+
import { artifactIdForConfig, findAlternative } from "./alternatives.js";
|
|
14
15
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
15
16
|
import { PROFILE_HOOKS } from "./hook-templates.js";
|
|
16
17
|
import { detectStackRoots } from "./stack-roots.js";
|
|
@@ -53,8 +54,18 @@ export async function checkHarden({ projectDir = process.cwd(), profile = "stand
|
|
|
53
54
|
checks.push(presence(projectDir, cfg.file, `config:${cfg.file}`, "missing — run kj harden"));
|
|
54
55
|
}
|
|
55
56
|
for (const { dir, language } of roots) {
|
|
57
|
+
const rootDir = dir === "." ? projectDir : join(projectDir, dir);
|
|
56
58
|
for (const cfg of CONFIGS_BY_LANGUAGE[language] ?? []) {
|
|
57
59
|
const rel = dir === "." ? cfg.file : join(dir, cfg.file);
|
|
60
|
+
// A config covered by the user's own tool (biome.json ⇒ eslint+prettier)
|
|
61
|
+
// is not drift — kj must not demand a second linter/formatter (KJC-TSK-0614).
|
|
62
|
+
if (!existsSync(join(projectDir, rel))) {
|
|
63
|
+
const alt = findAlternative([rootDir, projectDir], artifactIdForConfig(cfg));
|
|
64
|
+
if (alt) {
|
|
65
|
+
checks.push({ id: `config:${rel}`, ok: true, detail: `covered by ${alt.foundAt} (${alt.tool})` });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
58
69
|
checks.push(presence(projectDir, rel, `config:${rel}`, `missing for ${language} — run kj harden`));
|
|
59
70
|
}
|
|
60
71
|
}
|
|
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { upsertManagedBlock } from "../utils/managed-markers.js";
|
|
12
|
+
import { artifactIdForConfig, findAlternative } from "./alternatives.js";
|
|
12
13
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
13
14
|
|
|
14
15
|
const BLOCK_VERSION = 1;
|
|
@@ -18,13 +19,26 @@ function writeFile(target, content) {
|
|
|
18
19
|
writeFileSync(target, content);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Seed a list of config entries into `targetDir`, labelling each by `prefix`.
|
|
24
|
+
* `altDirs` widens the cross-tool alternative lookup (KJC-TSK-0614): a config
|
|
25
|
+
* covered by the user's own tool (biome.json ⇒ eslint+prettier) is never
|
|
26
|
+
* seeded — kj must not install a second, conflicting linter/formatter.
|
|
27
|
+
*/
|
|
28
|
+
function seedInto(targetDir, configs, dryRun, prefix, results, altDirs = [targetDir]) {
|
|
23
29
|
for (const cfg of configs) {
|
|
24
30
|
const target = join(targetDir, cfg.file);
|
|
25
31
|
const exists = existsSync(target);
|
|
26
32
|
const file = prefix === "." ? cfg.file : join(prefix, cfg.file);
|
|
27
33
|
|
|
34
|
+
if (!exists) {
|
|
35
|
+
const alt = findAlternative(altDirs, artifactIdForConfig(cfg));
|
|
36
|
+
if (alt) {
|
|
37
|
+
results.push({ file, action: "covered", by: alt.foundAt });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
if (cfg.json) {
|
|
29
43
|
const action = exists ? "skipped" : "inserted";
|
|
30
44
|
if (!dryRun && !exists) writeFile(target, `${cfg.body}\n`);
|
|
@@ -69,7 +83,9 @@ export function installConfigsForRoots({ projectDir = process.cwd(), roots = [],
|
|
|
69
83
|
const results = [];
|
|
70
84
|
seedInto(projectDir, UNIVERSAL_CONFIGS, dryRun, ".", results);
|
|
71
85
|
for (const { dir, language } of roots) {
|
|
72
|
-
|
|
86
|
+
const rootDir = join(projectDir, dir);
|
|
87
|
+
// A root-level biome.json covers every language root of the monorepo.
|
|
88
|
+
seedInto(rootDir, CONFIGS_BY_LANGUAGE[language] ?? [], dryRun, dir, results, [rootDir, projectDir]);
|
|
73
89
|
}
|
|
74
90
|
return { dryRun, configs: results };
|
|
75
91
|
}
|
|
@@ -153,7 +153,11 @@ export async function checkBudgetExceeded({ budgetTracker, config, session, emit
|
|
|
153
153
|
|
|
154
154
|
await markSessionStatus(session, "failed");
|
|
155
155
|
const totalCost = budgetTracker.total().cost_usd;
|
|
156
|
-
const
|
|
156
|
+
const limit = Number(budgetLimit ?? config?.max_budget_usd);
|
|
157
|
+
const message =
|
|
158
|
+
`Budget exceeded: $${totalCost.toFixed(2)} > $${limit.toFixed(2)}. ` +
|
|
159
|
+
`Continue consciously with \`kj resume ${session.id}\`, or raise the cap ` +
|
|
160
|
+
`(max_budget_usd in .karajan/kj.config.yml; null removes it).`;
|
|
157
161
|
emitProgress(
|
|
158
162
|
emitter,
|
|
159
163
|
makeEvent("session:end", { ...eventBase, iteration: i, stage: "budget" }, {
|
package/templates/kj.config.yml
CHANGED
|
@@ -114,5 +114,7 @@ output:
|
|
|
114
114
|
session:
|
|
115
115
|
max_iteration_minutes: 15
|
|
116
116
|
max_total_minutes: 120
|
|
117
|
-
|
|
117
|
+
# Hard per-run spend ceiling (USD-equivalent). The run stops with a summary
|
|
118
|
+
# when exceeded; resume with `kj resume`. Set to null to remove the cap.
|
|
119
|
+
max_budget_usd: 5
|
|
118
120
|
fail_fast_repeats: 2
|