karajan-code 4.33.0 → 4.34.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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * What counts toward the PR size budget (KJC-BUG-0205).
3
+ *
4
+ * The budget exists to cap CODE growth. Two implementations of that rule had
5
+ * drifted apart: CI excluded lockfiles, build output and the docs tree, while
6
+ * the local gate summed every line of the diff. A doc-only PR was warned at 234
7
+ * lines locally and counted 104 in CI, and a gate that cries wolf in one place
8
+ * and not the other teaches people to believe neither.
9
+ *
10
+ * Two rules decide what is exempt, and they are not the same rule:
11
+ * - GENERATED output never counts: nobody wrote it, and the reviewer reads the
12
+ * source that produced it.
13
+ * - HUMAN documentation never counts either. Rationing docs is how a project
14
+ * ends up shipping features nobody can discover (which is exactly what
15
+ * happened in 4.33.0, see KJC-TSK-0871).
16
+ *
17
+ * And one thing that DOES count, on purpose: AI-rule files (CLAUDE.md,
18
+ * AGENTS.md, templates/**). They enter the agent's context on every run, so
19
+ * unbounded growth there dilutes the signal the agent receives.
20
+ */
21
+
22
+ const EXEMPT = [
23
+ // Generated or vendored: nobody typed it.
24
+ /(^|\/)dist\//,
25
+ /(^|\/)build\//,
26
+ /(^|\/)coverage\//,
27
+ /(^|\/)node_modules\//,
28
+ /(^|\/)\.astro\//,
29
+ /(^|\/)public\/docs\//,
30
+ /\.min\.(js|css)$/,
31
+ /\.map$/,
32
+ /\.snap(shot)?$/,
33
+ /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|npm-shrinkwrap\.json)$/,
34
+ /\.lock$/,
35
+ /(^|\/)tests\/_diet\//,
36
+ // Human documentation, wherever the project keeps it. The landing's docs
37
+ // moved into the monorepo (MONO-3) and the old root-anchored `docs/` rule
38
+ // stopped matching them, which is how documenting started costing budget.
39
+ /(^|\/)docs\/.*\.(md|mdx|txt|rst)$/,
40
+ /^(CHANGELOG|README|CODE_OF_CONDUCT|CONTRIBUTING|SECURITY)(\.[a-z-]+)?\.md$/,
41
+ /^(MIGRATION|TODO).*\.md$/,
42
+ ];
43
+
44
+ // The exceptions to the exception: these ARE the agent's context.
45
+ const AI_RULES = [/^CLAUDE\.md$/, /^AGENTS\.md$/, /^GEMINI\.md$/, /(^|\/)templates\//];
46
+
47
+ /** @param {string} file @returns {boolean} */
48
+ export function countsTowardBudget(file) {
49
+ if (!file) return false;
50
+ if (AI_RULES.some((re) => re.test(file))) return true;
51
+ return !EXEMPT.some((re) => re.test(file));
52
+ }
53
+
54
+ /**
55
+ * KJC-BUG-0208 — the policy invariant `net_lines_added` summed the whole
56
+ * numstat, so the same diff printed 174 in the size gate and 202 in the policy,
57
+ * two lines apart. One rule, one number: the metric is netted over the files
58
+ * that count, and what does not count is still said out loud.
59
+ *
60
+ * @param {string} numstat output of `git diff --numstat`
61
+ * @returns {{net: number, exempt: number}}
62
+ */
63
+ export function budgetedNet(numstat) {
64
+ let net = 0;
65
+ let exempt = 0;
66
+ for (const line of String(numstat || "").split("\n")) {
67
+ if (!line.trim()) continue;
68
+ const [a, r, ...rest] = line.split("\t");
69
+ const added = Number(a);
70
+ const removed = Number(r);
71
+ if (!Number.isFinite(added) || !Number.isFinite(removed)) continue; // binary
72
+ if (!countsTowardBudget(rest.join("\t"))) { exempt += added; continue; }
73
+ net += added - removed;
74
+ }
75
+ return { net, exempt };
76
+ }
77
+
78
+ /**
79
+ * @param {string} numstat output of `git diff --numstat`
80
+ * @returns {{added: number, exempt: number, testAdded: number}}
81
+ */
82
+ export function budgetedAdded(numstat) {
83
+ let added = 0;
84
+ let exempt = 0;
85
+ let testAdded = 0;
86
+ for (const line of String(numstat || "").split("\n")) {
87
+ if (!line.trim()) continue;
88
+ const [a, , ...rest] = line.split("\t");
89
+ const n = Number(a);
90
+ if (!Number.isFinite(n)) continue; // binary files emit '-'
91
+ const file = rest.join("\t");
92
+ if (!countsTowardBudget(file)) { exempt += n; continue; }
93
+ added += n;
94
+ if (/\/tests?\/|__tests__\/|\.test\.|\.spec\./.test(`/${file}`)) testAdded += n;
95
+ }
96
+ return { added, exempt, testAdded };
97
+ }
@@ -1,6 +1,9 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { AgentRole } from "./agent-role.js";
4
+ import { withBrainRecovery } from "../brain/with-brain-recovery.js";
5
+ import { buildRoleFallbackChain } from "../brain/role-fallback-chain.js";
6
+ import { ONE_SHOT_POLICY } from "../brain/one-shot-policy.js";
4
7
  import { securityAuditMarkerPath } from "../steward/invariants.js";
5
8
  import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
6
9
  import { measureBasalCost, loadPreviousAudit, saveAuditSnapshot, computeGrowthDelta } from "../audit/basal-cost.js";
@@ -193,13 +196,36 @@ export class AuditRole extends AgentRole {
193
196
  const runArgs = { prompt, role: "audit" };
194
197
  if (onOutput) runArgs.onOutput = onOutput;
195
198
  const startedAt = Date.now();
196
- const result = await agent.runTask(runArgs);
199
+ // KJC-BUG-0194: this used to call agent.runTask() directly, which made
200
+ // audit the ONE role that bypassed the brain. No classification (so an MCP
201
+ // caller saw a quota wall as category "unknown") and no declared chain
202
+ // (so roles.audit.fallback was ignored even after KJC-TSK-0859 wired it,
203
+ // because the wiring lives in AgentRole.execute and audit overrides it).
204
+ // The one-shot policy matters here: `kj audit` must take the next
205
+ // candidate at once rather than hibernate for hours inside a command.
206
+ const result = await withBrainRecovery({
207
+ agent: { runTask: (args) => agent.runTask(args), provider, model: this.config?.roles?.audit?.model ?? null },
208
+ taskArgs: runArgs,
209
+ role: "audit",
210
+ provider,
211
+ logger: this.logger,
212
+ emitter: this.emitter,
213
+ policy: ONE_SHOT_POLICY,
214
+ fallback: buildRoleFallbackChain({ config: this.config, role: "audit", createAgentFn: this._createAgent, logger: this.logger }),
215
+ });
197
216
  const durationMs = Date.now() - startedAt;
198
217
 
199
218
  const usage = extractUsage(result, { provider, durationMs });
200
219
 
201
220
  if (!result.ok) {
202
- return { ok: false, result: { error: result.error || result.output || "Audit failed", provider }, summary: `Audit failed: ${result.error || "unknown error"}`, usage };
221
+ // The classification and the itinerary travel with the failure, so a
222
+ // caller (CLI or MCP) can say WHY it stopped and what was tried.
223
+ return {
224
+ ok: false,
225
+ result: { error: result.error || result.output || "Audit failed", provider, recovery: result.recovery || null, tried: result.tried || null },
226
+ summary: `Audit failed: ${result.recovery?.class ? `${result.recovery.class} — ` : ""}${result.error || "unknown error"}`,
227
+ usage,
228
+ };
203
229
  }
204
230
 
205
231
  try {