karajan-code 1.44.0 → 1.45.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
package/src/config.js CHANGED
@@ -166,6 +166,11 @@ const DEFAULTS = {
166
166
  backoff_multiplier: 2,
167
167
  jitter_factor: 0.1
168
168
  },
169
+ webperf: {
170
+ enabled: true,
171
+ devtools_mcp: false,
172
+ thresholds: { lcp: 2500, cls: 0.1, inp: 200 }
173
+ },
169
174
  guards: {
170
175
  output: {
171
176
  enabled: true,
@@ -1,5 +1,6 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
+ import { isDevToolsMcpAvailable, ensureWebPerfSkills } from "../webperf/devtools-detect.js";
3
4
 
4
5
  const SUBAGENT_PREAMBLE = [
5
6
  "IMPORTANT: You are running as a Karajan sub-agent.",
@@ -64,14 +65,32 @@ export class ImpeccableRole extends BaseRole {
64
65
  }
65
66
 
66
67
  async execute(input) {
67
- const { task, diff } = typeof input === "string"
68
- ? { task: input, diff: null }
69
- : { task: input?.task || this.context?.task || "", diff: input?.diff || null };
68
+ const { task, diff, projectDir } = typeof input === "string"
69
+ ? { task: input, diff: null, projectDir: null }
70
+ : { task: input?.task || this.context?.task || "", diff: input?.diff || null, projectDir: input?.projectDir || null };
71
+
72
+ // Auto-install WebPerf skills when DevTools MCP is configured
73
+ if (isDevToolsMcpAvailable(this.config)) {
74
+ const dir = projectDir || process.cwd();
75
+ try {
76
+ const webperfResult = await ensureWebPerfSkills(dir, this.logger);
77
+ if (webperfResult.installed.length > 0) {
78
+ this.logger?.info?.(`WebPerf skills installed: ${webperfResult.installed.join(", ")}`);
79
+ }
80
+ } catch (err) {
81
+ this.logger?.warn?.(`WebPerf skill installation failed: ${err.message}`);
82
+ }
83
+ } else {
84
+ this.logger?.info?.("Chrome DevTools MCP not configured — skipping WebPerf skills");
85
+ }
70
86
 
71
87
  const provider = resolveProvider(this.config);
72
88
  const agent = this._createAgent(provider, this.config, this.logger);
73
89
 
74
- const prompt = buildPrompt({ task, diff, instructions: this.instructions });
90
+ const webperfNote = isDevToolsMcpAvailable(this.config)
91
+ ? "\n\nNote: WebPerf domain skills are available. Use them for Core Web Vitals analysis if the audit involves frontend performance."
92
+ : "";
93
+ const prompt = buildPrompt({ task, diff, instructions: this.instructions }) + webperfNote;
75
94
  const result = await agent.runTask({ prompt, role: "impeccable" });
76
95
 
77
96
  if (!result.ok) {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Core Web Vitals quality gate.
3
+ *
4
+ * Evaluates LCP, CLS and INP against Google's recommended thresholds
5
+ * and returns a structured verdict usable by the pipeline.
6
+ */
7
+
8
+ export const CWV_THRESHOLDS = {
9
+ lcp: { good: 2500, poor: 4000 },
10
+ cls: { good: 0.1, poor: 0.25 },
11
+ inp: { good: 200, poor: 500 }
12
+ };
13
+
14
+ /**
15
+ * Deep-merge custom thresholds on top of the defaults.
16
+ * Only overrides metrics/boundaries that the caller provides.
17
+ *
18
+ * @param {object} defaults - Base thresholds (e.g. CWV_THRESHOLDS)
19
+ * @param {object} [custom] - Partial overrides
20
+ * @returns {object} Merged thresholds
21
+ */
22
+ export function mergeThresholds(defaults, custom) {
23
+ if (!custom || typeof custom !== "object") return { ...defaults };
24
+
25
+ const merged = {};
26
+ for (const metric of Object.keys(defaults)) {
27
+ const base = defaults[metric];
28
+ const over = custom[metric];
29
+ if (over && typeof over === "object") {
30
+ merged[metric] = {
31
+ good: over.good !== undefined ? over.good : base.good,
32
+ poor: over.poor !== undefined ? over.poor : base.poor
33
+ };
34
+ } else {
35
+ merged[metric] = { ...base };
36
+ }
37
+ }
38
+
39
+ // Include any extra metrics the caller added that are not in defaults
40
+ for (const metric of Object.keys(custom)) {
41
+ if (!merged[metric] && typeof custom[metric] === "object") {
42
+ merged[metric] = { ...custom[metric] };
43
+ }
44
+ }
45
+
46
+ return merged;
47
+ }
48
+
49
+ function rateMetric(value, good, poor) {
50
+ if (value <= good) return "good";
51
+ if (value >= poor) return "poor";
52
+ return "needs-improvement";
53
+ }
54
+
55
+ /**
56
+ * Evaluate Core Web Vitals metrics against thresholds.
57
+ *
58
+ * @param {{ lcp?: number, cls?: number, inp?: number }} metrics
59
+ * @param {object} [customThresholds] - Optional per-metric overrides
60
+ * @returns {{
61
+ * pass: boolean,
62
+ * scores: Record<string, { value: number, rating: string }>,
63
+ * blocking: Array<{ metric: string, value: number, threshold: number, rating: string }>,
64
+ * advisory: Array<{ metric: string, value: number, threshold: number, rating: string }>
65
+ * }}
66
+ */
67
+ export function evaluateCwv(metrics, customThresholds) {
68
+ const thresholds = mergeThresholds(CWV_THRESHOLDS, customThresholds);
69
+
70
+ const scores = {};
71
+ const blocking = [];
72
+ const advisory = [];
73
+
74
+ for (const [metric, bounds] of Object.entries(thresholds)) {
75
+ const value = metrics[metric];
76
+ if (value === undefined || value === null) continue;
77
+
78
+ const rating = rateMetric(value, bounds.good, bounds.poor);
79
+ scores[metric] = { value, rating };
80
+
81
+ if (rating === "poor") {
82
+ blocking.push({ metric, value, threshold: bounds.poor, rating });
83
+ } else if (rating === "needs-improvement") {
84
+ advisory.push({ metric, value, threshold: bounds.good, rating });
85
+ }
86
+ }
87
+
88
+ return {
89
+ pass: blocking.length === 0,
90
+ scores,
91
+ blocking,
92
+ advisory
93
+ };
94
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Chrome DevTools MCP detection and WebPerf skill auto-installation.
3
+ */
4
+
5
+ import { isOpenSkillsAvailable, installSkill, listSkills } from "../skills/openskills-client.js";
6
+
7
+ /** Skill names to auto-install when WebPerf is enabled. */
8
+ export const WEBPERF_SKILLS = [
9
+ "webperf",
10
+ "webperf-core-web-vitals",
11
+ "webperf-loading"
12
+ ];
13
+
14
+ /**
15
+ * Check whether Chrome DevTools MCP is configured and available.
16
+ * Since Karajan runs as an MCP server itself, it cannot directly call other MCPs.
17
+ * Instead, we check the config flag `webperf.devtools_mcp`.
18
+ * @param {object} config — merged Karajan config
19
+ * @returns {boolean}
20
+ */
21
+ export function isDevToolsMcpAvailable(config) {
22
+ return Boolean(config?.webperf?.devtools_mcp);
23
+ }
24
+
25
+ /**
26
+ * Ensure WebPerf skills are installed via OpenSkills.
27
+ * Installs any missing skills from WEBPERF_SKILLS.
28
+ * @param {string} projectDir — absolute path to the project root
29
+ * @param {object} [logger] — optional logger ({ info, warn })
30
+ * @returns {Promise<{ installed: string[], alreadyInstalled: string[], skipped: string[] }>}
31
+ */
32
+ export async function ensureWebPerfSkills(projectDir, logger) {
33
+ const result = { installed: [], alreadyInstalled: [], skipped: [] };
34
+
35
+ const available = await isOpenSkillsAvailable();
36
+ if (!available) {
37
+ logger?.warn?.("OpenSkills CLI not available — skipping WebPerf skill installation");
38
+ result.skipped = [...WEBPERF_SKILLS];
39
+ return result;
40
+ }
41
+
42
+ // Get currently installed skills
43
+ const listResult = await listSkills({ projectDir });
44
+ const installedNames = new Set(
45
+ (listResult.ok ? listResult.skills : []).map(s => s.name)
46
+ );
47
+
48
+ for (const skill of WEBPERF_SKILLS) {
49
+ if (installedNames.has(skill)) {
50
+ result.alreadyInstalled.push(skill);
51
+ continue;
52
+ }
53
+
54
+ const installResult = await installSkill(skill, { projectDir });
55
+ if (installResult.ok) {
56
+ result.installed.push(skill);
57
+ logger?.info?.(`Installed WebPerf skill: ${skill}`);
58
+ } else {
59
+ result.skipped.push(skill);
60
+ logger?.warn?.(`Failed to install WebPerf skill "${skill}": ${installResult.error}`);
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Integration helpers that format CWV gate results for the
3
+ * impeccable prompt and for pipeline progress events.
4
+ */
5
+
6
+ const RATING_ICONS = { good: "PASS", "needs-improvement": "WARN", poor: "FAIL" };
7
+
8
+ /**
9
+ * Build a human-readable section for the impeccable prompt.
10
+ *
11
+ * @param {{ pass: boolean, scores: object, blocking: Array, advisory: Array }} cwvResult
12
+ * @returns {string}
13
+ */
14
+ export function buildWebPerfSection(cwvResult) {
15
+ const lines = [];
16
+ const verdict = cwvResult.pass ? "PASSED" : "FAILED";
17
+ lines.push(`## WebPerf — Core Web Vitals (${verdict})`);
18
+ lines.push("");
19
+
20
+ for (const [metric, { value, rating }] of Object.entries(cwvResult.scores)) {
21
+ const icon = RATING_ICONS[rating] || rating;
22
+ const unit = metric === "cls" ? "" : "ms";
23
+ lines.push(`- **${metric.toUpperCase()}**: ${value}${unit} [${icon}]`);
24
+ }
25
+
26
+ if (cwvResult.blocking.length > 0) {
27
+ lines.push("");
28
+ lines.push("### Blocking issues");
29
+ for (const b of cwvResult.blocking) {
30
+ const unit = b.metric === "cls" ? "" : "ms";
31
+ lines.push(`- ${b.metric.toUpperCase()}: ${b.value}${unit} exceeds poor threshold (${b.threshold}${unit})`);
32
+ }
33
+ }
34
+
35
+ if (cwvResult.advisory.length > 0) {
36
+ lines.push("");
37
+ lines.push("### Advisory (needs improvement)");
38
+ for (const a of cwvResult.advisory) {
39
+ const unit = a.metric === "cls" ? "" : "ms";
40
+ lines.push(`- ${a.metric.toUpperCase()}: ${a.value}${unit} exceeds good threshold (${a.threshold}${unit})`);
41
+ }
42
+ }
43
+
44
+ return lines.join("\n");
45
+ }
46
+
47
+ /**
48
+ * Format CWV result for a pipeline progress event payload.
49
+ *
50
+ * @param {{ pass: boolean, scores: object, blocking: Array, advisory: Array }} cwvResult
51
+ * @returns {{ type: string, pass: boolean, metrics: object, blockingCount: number, advisoryCount: number }}
52
+ */
53
+ export function formatCwvForEvent(cwvResult) {
54
+ const metrics = {};
55
+ for (const [metric, { value, rating }] of Object.entries(cwvResult.scores)) {
56
+ metrics[metric] = { value, rating };
57
+ }
58
+
59
+ return {
60
+ type: "webperf-cwv",
61
+ pass: cwvResult.pass,
62
+ metrics,
63
+ blockingCount: cwvResult.blocking.length,
64
+ advisoryCount: cwvResult.advisory.length
65
+ };
66
+ }