karajan-code 1.59.0 → 2.0.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/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +34 -6
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +3 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +10 -52
- package/src/domains/domain-loader.js +85 -4
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +2 -2
- package/src/mcp/tools.js +3 -2
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +435 -661
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +34 -91
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +52 -118
- package/src/roles/reviewer-role.js +69 -126
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +71 -15
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/banner.js +66 -0
- package/src/utils/display.js +35 -17
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/utils/welcome.js +3 -8
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -198
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
package/src/roles/triage-role.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
1
|
+
import { AgentRole } from "./agent-role.js";
|
|
3
2
|
import { buildTriagePrompt } from "../prompts/triage.js";
|
|
4
3
|
import { VALID_TASK_TYPES } from "../guards/policy-resolver.js";
|
|
5
4
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
@@ -7,18 +6,7 @@ import { extractFirstJson } from "../utils/json-extract.js";
|
|
|
7
6
|
const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
|
|
8
7
|
const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security", "impeccable"]);
|
|
9
8
|
const FALLBACK_TASK_TYPE = "sw";
|
|
10
|
-
|
|
11
|
-
function resolveProvider(config) {
|
|
12
|
-
return (
|
|
13
|
-
config?.roles?.triage?.provider ||
|
|
14
|
-
config?.roles?.coder?.provider ||
|
|
15
|
-
"claude"
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parseTriageOutput(raw) {
|
|
20
|
-
return extractFirstJson(raw);
|
|
21
|
-
}
|
|
9
|
+
const FALLBACK_RESULT = { level: "medium", roles: ["reviewer"], taskType: FALLBACK_TASK_TYPE, shouldDecompose: false };
|
|
22
10
|
|
|
23
11
|
function normalizeRoles(roles) {
|
|
24
12
|
if (!Array.isArray(roles)) return [];
|
|
@@ -27,115 +15,66 @@ function normalizeRoles(roles) {
|
|
|
27
15
|
|
|
28
16
|
function normalizeSubtasks(subtasks) {
|
|
29
17
|
if (!Array.isArray(subtasks)) return [];
|
|
30
|
-
return subtasks
|
|
31
|
-
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
32
|
-
.filter(Boolean)
|
|
33
|
-
.slice(0, 5);
|
|
18
|
+
return subtasks.map((s) => (typeof s === "string" ? s.trim() : "")).filter(Boolean).slice(0, 5);
|
|
34
19
|
}
|
|
35
20
|
|
|
36
21
|
function normalizeDomainHints(hints) {
|
|
37
22
|
if (!Array.isArray(hints)) return [];
|
|
38
|
-
return hints
|
|
39
|
-
.map((h) => (typeof h === "string" ? h.trim().toLowerCase() : ""))
|
|
40
|
-
.filter(Boolean);
|
|
23
|
+
return hints.map((h) => (typeof h === "string" ? h.trim().toLowerCase() : "")).filter(Boolean);
|
|
41
24
|
}
|
|
42
25
|
|
|
43
|
-
export class TriageRole extends
|
|
44
|
-
constructor(
|
|
45
|
-
super({ name: "triage"
|
|
46
|
-
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
26
|
+
export class TriageRole extends AgentRole {
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
super({ ...opts, name: "triage" });
|
|
47
29
|
}
|
|
48
30
|
|
|
49
|
-
async
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: input?.task || this.context?.task || "";
|
|
53
|
-
const onOutput = typeof input === "string" ? null : input?.onOutput || null;
|
|
54
|
-
|
|
55
|
-
const provider = resolveProvider(this.config);
|
|
56
|
-
const agent = this._createAgent(provider, this.config, this.logger);
|
|
57
|
-
|
|
58
|
-
const prompt = buildTriagePrompt({ task, instructions: this.instructions });
|
|
59
|
-
const runArgs = { prompt, role: "triage" };
|
|
60
|
-
if (onOutput) runArgs.onOutput = onOutput;
|
|
61
|
-
const result = await agent.runTask(runArgs);
|
|
31
|
+
async buildPrompt({ task }) {
|
|
32
|
+
return { prompt: buildTriagePrompt({ task, instructions: this.instructions }) };
|
|
33
|
+
}
|
|
62
34
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
35
|
+
parseOutput(raw) { return extractFirstJson(raw); }
|
|
36
|
+
|
|
37
|
+
buildSuccessResult(parsed, provider) {
|
|
38
|
+
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
39
|
+
const roles = normalizeRoles(parsed.roles);
|
|
40
|
+
const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
|
|
41
|
+
const shouldDecompose = Boolean(parsed.shouldDecompose);
|
|
42
|
+
const subtasks = normalizeSubtasks(parsed.subtasks);
|
|
43
|
+
const taskType = VALID_TASK_TYPES.has(parsed.taskType) ? parsed.taskType : FALLBACK_TASK_TYPE;
|
|
44
|
+
const domainHints = normalizeDomainHints(parsed.domainHints);
|
|
45
|
+
|
|
46
|
+
const result = { level, roles, reasoning, taskType, domainHints, provider };
|
|
47
|
+
if (shouldDecompose && subtasks.length > 0) {
|
|
48
|
+
result.shouldDecompose = true;
|
|
49
|
+
result.subtasks = subtasks;
|
|
50
|
+
} else {
|
|
51
|
+
result.shouldDecompose = false;
|
|
73
52
|
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
74
55
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
level: "medium",
|
|
82
|
-
roles: ["reviewer"],
|
|
83
|
-
reasoning: "Unstructured output, using safe defaults.",
|
|
84
|
-
taskType: FALLBACK_TASK_TYPE,
|
|
85
|
-
provider,
|
|
86
|
-
raw: result.output
|
|
87
|
-
},
|
|
88
|
-
summary: "Triage complete (fallback defaults)",
|
|
89
|
-
usage: result.usage
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
94
|
-
const roles = normalizeRoles(parsed.roles);
|
|
95
|
-
const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
|
|
96
|
-
const shouldDecompose = Boolean(parsed.shouldDecompose);
|
|
97
|
-
const subtasks = normalizeSubtasks(parsed.subtasks);
|
|
98
|
-
const taskType = VALID_TASK_TYPES.has(parsed.taskType) ? parsed.taskType : FALLBACK_TASK_TYPE;
|
|
99
|
-
|
|
100
|
-
const domainHints = normalizeDomainHints(parsed.domainHints);
|
|
101
|
-
|
|
102
|
-
const triageResult = {
|
|
103
|
-
level,
|
|
104
|
-
roles,
|
|
105
|
-
reasoning,
|
|
106
|
-
taskType,
|
|
107
|
-
domainHints,
|
|
108
|
-
provider
|
|
109
|
-
};
|
|
56
|
+
buildSummary(parsed) {
|
|
57
|
+
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
58
|
+
const roles = normalizeRoles(parsed.roles);
|
|
59
|
+
const decomposeNote = parsed.shouldDecompose ? " — decomposition recommended" : "";
|
|
60
|
+
return `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})${decomposeNote}`;
|
|
61
|
+
}
|
|
110
62
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
63
|
+
handleParseNull(agentResult, provider) {
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
result: { ...FALLBACK_RESULT, reasoning: "Unstructured output, using safe defaults.", provider, raw: agentResult.output },
|
|
67
|
+
summary: "Triage complete (fallback defaults)",
|
|
68
|
+
usage: agentResult.usage
|
|
69
|
+
};
|
|
70
|
+
}
|
|
117
71
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} catch { /* agent output is not structured JSON */
|
|
126
|
-
return {
|
|
127
|
-
ok: true,
|
|
128
|
-
result: {
|
|
129
|
-
level: "medium",
|
|
130
|
-
roles: ["reviewer"],
|
|
131
|
-
reasoning: "Failed to parse triage output, using safe defaults.",
|
|
132
|
-
taskType: FALLBACK_TASK_TYPE,
|
|
133
|
-
provider,
|
|
134
|
-
raw: result.output
|
|
135
|
-
},
|
|
136
|
-
summary: "Triage complete (fallback defaults)",
|
|
137
|
-
usage: result.usage
|
|
138
|
-
};
|
|
139
|
-
}
|
|
72
|
+
handleParseError(_err, agentResult, provider) {
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
result: { ...FALLBACK_RESULT, reasoning: "Failed to parse triage output, using safe defaults.", provider, raw: agentResult.output },
|
|
76
|
+
summary: "Triage complete (fallback defaults)",
|
|
77
|
+
usage: agentResult.usage
|
|
78
|
+
};
|
|
140
79
|
}
|
|
141
80
|
}
|
package/src/session-store.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getSessionRoot } from "./utils/paths.js";
|
|
|
5
5
|
|
|
6
6
|
const SESSION_ROOT = getSessionRoot();
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function newSessionId() {
|
|
9
9
|
const now = new Date();
|
|
10
10
|
const stamp = now.toISOString().replaceAll(/[:.]/g, "-");
|
|
11
11
|
return `s_${stamp}`;
|
|
@@ -63,6 +63,8 @@ const TASK_TEXT_PATTERNS = [
|
|
|
63
63
|
{ pattern: /\bgo(?:lang)?\b/i, skill: "go" },
|
|
64
64
|
{ pattern: /\brust\b/i, skill: "rust" },
|
|
65
65
|
{ pattern: /\bpython\b/i, skill: "python" },
|
|
66
|
+
{ pattern: /\bdjango\b/i, skill: "python" },
|
|
67
|
+
{ pattern: /\bflask\b/i, skill: "python" },
|
|
66
68
|
{ pattern: /\bruby\b/i, skill: "ruby" },
|
|
67
69
|
{ pattern: /\bflutter\b/i, skill: "flutter" },
|
|
68
70
|
{ pattern: /\bswift\b/i, skill: "swift" },
|
|
@@ -76,6 +78,15 @@ const TASK_TEXT_PATTERNS = [
|
|
|
76
78
|
{ pattern: /\breport\b/i, skill: "data-report" },
|
|
77
79
|
{ pattern: /\banalysis\b/i, skill: "data-report" },
|
|
78
80
|
{ pattern: /\bfindings\b/i, skill: "data-report" },
|
|
81
|
+
// Frontend/backend broad categories
|
|
82
|
+
{ pattern: /\bfrontend\b/i, skill: "frontend-design" },
|
|
83
|
+
{ pattern: /\bCSS\b/i, skill: "frontend-design" },
|
|
84
|
+
{ pattern: /\bHTML\b/i, skill: "frontend-design" },
|
|
85
|
+
{ pattern: /\bbackend\b/i, skill: "express" },
|
|
86
|
+
{ pattern: /\bnode\.?js\b/i, skill: "express" },
|
|
87
|
+
{ pattern: /\bAPI\b/, skill: "express" },
|
|
88
|
+
{ pattern: /\bwebsocket\b/i, skill: "express" },
|
|
89
|
+
{ pattern: /\breal[- ]time\b/i, skill: "express" },
|
|
79
90
|
];
|
|
80
91
|
|
|
81
92
|
/**
|
|
@@ -27,6 +27,8 @@ export function resolveSonarHost(rawHost) {
|
|
|
27
27
|
* 2. config.sonarqube.token
|
|
28
28
|
* 3. SONAR_TOKEN env var
|
|
29
29
|
*
|
|
30
|
+
* For async resolution (including credentials file), use resolveSonarTokenAsync.
|
|
31
|
+
*
|
|
30
32
|
* @param {object} [config={}] - Karajan config object
|
|
31
33
|
* @returns {string|null} Token string or null if none found
|
|
32
34
|
*/
|
|
@@ -39,6 +41,22 @@ export function resolveSonarToken(config = {}) {
|
|
|
39
41
|
return token || null;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Async version of resolveSonarToken that also checks the credentials file
|
|
46
|
+
* for previously generated tokens.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} [config={}] - Karajan config object
|
|
49
|
+
* @returns {Promise<string|null>} Token string or null if none found
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveSonarTokenAsync(config = {}) {
|
|
52
|
+
const syncToken = resolveSonarToken(config);
|
|
53
|
+
if (syncToken) return syncToken;
|
|
54
|
+
|
|
55
|
+
// Check credentials file for persisted token
|
|
56
|
+
const fileCreds = (await loadSonarCredentials()) || {};
|
|
57
|
+
return fileCreds.token || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
/**
|
|
43
61
|
* Resolve SonarQube admin credentials from (in priority order):
|
|
44
62
|
* 1. Env vars: KJ_SONAR_ADMIN_USER / KJ_SONAR_ADMIN_PASSWORD
|
|
@@ -57,12 +75,13 @@ export async function resolveSonarCredentials(config = {}) {
|
|
|
57
75
|
process.env.KJ_SONAR_ADMIN_USER ||
|
|
58
76
|
config.sonarqube?.admin_user ||
|
|
59
77
|
fileCreds.user ||
|
|
60
|
-
|
|
78
|
+
"admin"; // Default for fresh SonarQube installs
|
|
61
79
|
|
|
62
80
|
const candidates = [
|
|
63
81
|
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
64
82
|
config.sonarqube?.admin_password,
|
|
65
83
|
fileCreds.password,
|
|
84
|
+
"admin", // Default password for fresh SonarQube installs (zero-config)
|
|
66
85
|
].filter(Boolean);
|
|
67
86
|
|
|
68
87
|
return { user, passwords: [...new Set(candidates)] };
|
package/src/sonar/credentials.js
CHANGED
|
@@ -33,3 +33,20 @@ export async function loadSonarCredentials() {
|
|
|
33
33
|
export function credentialsPath() {
|
|
34
34
|
return path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Save a generated token to ~/.karajan/sonar-credentials.json so it persists across sessions.
|
|
39
|
+
* Merges with existing credentials (preserves user/password if present).
|
|
40
|
+
*/
|
|
41
|
+
export async function saveSonarToken(token) {
|
|
42
|
+
const filePath = path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
43
|
+
let existing = {};
|
|
44
|
+
try {
|
|
45
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
46
|
+
existing = JSON.parse(raw);
|
|
47
|
+
} catch { /* file may not exist */ }
|
|
48
|
+
existing.token = token;
|
|
49
|
+
const dir = getKarajanHome();
|
|
50
|
+
await fs.mkdir(dir, { recursive: true });
|
|
51
|
+
await fs.writeFile(filePath, JSON.stringify(existing, null, 2), "utf8");
|
|
52
|
+
}
|
package/src/sonar/project-key.js
CHANGED
|
@@ -9,7 +9,7 @@ function slug(value) {
|
|
|
9
9
|
.replaceAll(/(^-+)|(-+$)/g, "");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function normalizeProjectKey(value) {
|
|
13
13
|
const out = slug(value);
|
|
14
14
|
if (!out) return "kj-default";
|
|
15
15
|
return /[a-z]/.test(out) ? out : `kj-${out}`;
|
package/src/sonar/scanner.js
CHANGED
|
@@ -150,7 +150,7 @@ async function resolveSonarTokenWithFallback(config, apiHost) {
|
|
|
150
150
|
return null;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
async function ensureSonarProjectProperties(cwd = process.cwd()) {
|
|
154
154
|
const propsPath = path.join(cwd, "sonar-project.properties");
|
|
155
155
|
try {
|
|
156
156
|
await fsPromises.access(propsPath);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Karajan Code ASCII banner with brand colors.
|
|
3
|
+
*
|
|
4
|
+
* Brand palette:
|
|
5
|
+
* #AACCEE steel blue (primary text)
|
|
6
|
+
* #88CC88 green (accents, robot eyes)
|
|
7
|
+
* #464000 dark olive
|
|
8
|
+
* #444411 charcoal (secondary text)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const C = {
|
|
12
|
+
blue: "\x1b[38;2;170;204;238m", // #AACCEE
|
|
13
|
+
green: "\x1b[38;2;100;190;100m", // green accents
|
|
14
|
+
gray: "\x1b[38;2;140;140;140m", // borders/dim
|
|
15
|
+
white: "\x1b[38;2;220;220;220m", // bright text
|
|
16
|
+
charcoal: "\x1b[38;2;100;100;80m", // #444411 approx
|
|
17
|
+
bold: "\x1b[1m",
|
|
18
|
+
dim: "\x1b[2m",
|
|
19
|
+
reset: "\x1b[0m"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const R = C.reset;
|
|
23
|
+
|
|
24
|
+
const BANNER_LINES = [
|
|
25
|
+
``,
|
|
26
|
+
`${C.gray} ╭───────────╮`,
|
|
27
|
+
`${C.gray} │ ${C.green}~${C.gray} │`,
|
|
28
|
+
`${C.gray} │ ${C.green}|${C.gray} ${C.green}|${C.gray} │ ${C.charcoal}|`,
|
|
29
|
+
`${C.gray} | ${C.green}v${C.gray} │ ${C.charcoal}/`,
|
|
30
|
+
`${C.gray} ${C.charcoal}\\${C.gray} ╰───────────╯${C.charcoal}/`,
|
|
31
|
+
`${C.charcoal} \\${C.gray} ╭───${C.white}⋈${C.gray}───╮ ${C.charcoal}/`,
|
|
32
|
+
`${C.charcoal} \\${C.gray} ${C.charcoal}/`,
|
|
33
|
+
`${C.gray} ╰───────╯`,
|
|
34
|
+
``,
|
|
35
|
+
` ${C.bold}${C.blue}╦╔═ ╔═╗ ╦═╗ ╔═╗ ╦ ╔═╗ ╔╗╔${R}`,
|
|
36
|
+
` ${C.bold}${C.blue}╠╩╗ ╠═╣ ╠╦╝ ╠═╣ ║ ╠═╣ ║║║${R}`,
|
|
37
|
+
` ${C.bold}${C.blue}╩ ╩ ╩ ╩ ╩╚═ ╩ ╩ ╚╝ ╩ ╩ ╝╚╝${R}`,
|
|
38
|
+
` ${C.bold}${C.white} C O D E${R} %%VERSION%%`,
|
|
39
|
+
` ${C.charcoal}multiagent coding orchestrator${R}`,
|
|
40
|
+
``,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Print the Karajan Code ASCII banner.
|
|
45
|
+
* @param {string} version - Package version to display
|
|
46
|
+
* @param {object} [opts]
|
|
47
|
+
* @param {boolean} [opts.compact] - Skip mascot, show only text
|
|
48
|
+
* @param {boolean} [opts.force] - Print even without TTY
|
|
49
|
+
*/
|
|
50
|
+
export function printBanner(version, opts = {}) {
|
|
51
|
+
if (!opts.force && !process.stdout.isTTY) return;
|
|
52
|
+
|
|
53
|
+
const versionTag = `${C.dim}v${version}${R}`;
|
|
54
|
+
|
|
55
|
+
if (opts.compact) {
|
|
56
|
+
console.log();
|
|
57
|
+
for (const line of BANNER_LINES.slice(10)) {
|
|
58
|
+
console.log(line.replace("%%VERSION%%", versionTag));
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const line of BANNER_LINES) {
|
|
64
|
+
console.log(line.replace("%%VERSION%%", versionTag));
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/utils/display.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { printBanner } from "./banner.js";
|
|
4
5
|
|
|
5
6
|
// TODO: i18n display messages
|
|
6
7
|
const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
@@ -83,9 +84,7 @@ const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
|
|
|
83
84
|
|
|
84
85
|
export function printHeader({ task, config }) {
|
|
85
86
|
const version = DISPLAY_VERSION;
|
|
86
|
-
|
|
87
|
-
console.log(`${ANSI.bold}${ANSI.cyan}\u25b6 Karajan Code v${version}${ANSI.reset}`);
|
|
88
|
-
console.log(BAR);
|
|
87
|
+
printBanner(version);
|
|
89
88
|
console.log(`${ANSI.bold}Task:${ANSI.reset} ${task}`);
|
|
90
89
|
console.log(
|
|
91
90
|
`${ANSI.bold}Coder:${ANSI.reset} ${config.roles?.coder?.provider || config.coder} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Reviewer:${ANSI.reset} ${config.roles?.reviewer?.provider || config.reviewer}`
|
|
@@ -100,7 +99,10 @@ export function printHeader({ task, config }) {
|
|
|
100
99
|
if (pipeline.researcher?.enabled) activeRoles.push(`Researcher (${config.roles?.researcher?.provider || "?"})`);
|
|
101
100
|
if (pipeline.tester?.enabled) activeRoles.push("Tester");
|
|
102
101
|
if (pipeline.security?.enabled) activeRoles.push("Security");
|
|
103
|
-
if (pipeline.solomon?.enabled)
|
|
102
|
+
if (pipeline.solomon?.enabled) {
|
|
103
|
+
const solomonProvider = config.roles?.solomon?.provider;
|
|
104
|
+
activeRoles.push(solomonProvider ? `Solomon (${solomonProvider})` : "Solomon");
|
|
105
|
+
}
|
|
104
106
|
if (activeRoles.length > 0) {
|
|
105
107
|
const separator = ` ${ANSI.dim}|${ANSI.reset} `;
|
|
106
108
|
console.log(`${ANSI.bold}Pipeline:${ANSI.reset} ${activeRoles.join(separator)}`);
|
|
@@ -257,7 +259,14 @@ function printSessionRtkSavings(rtkSavings) {
|
|
|
257
259
|
const tokens = rtkSavings.estimatedTokensSaved ?? 0;
|
|
258
260
|
const ratio = rtkSavings.savedPct ?? 0;
|
|
259
261
|
const commands = rtkSavings.callCount ?? 0;
|
|
260
|
-
|
|
262
|
+
const original = rtkSavings.originalBytes ?? 0;
|
|
263
|
+
const compressed = rtkSavings.rtkBytes ?? 0;
|
|
264
|
+
|
|
265
|
+
if (tokens === 0 || ratio === 0) {
|
|
266
|
+
console.log(` ${ANSI.dim}⚡ RTK: ${commands} commands wrapped, 0% savings — output was already minimal (${original} bytes in, ${compressed} bytes out)${ANSI.reset}`);
|
|
267
|
+
} else {
|
|
268
|
+
console.log(` ${ANSI.dim}⚡ RTK: saved ~${tokens.toLocaleString()} tokens (${ratio}% compression, ${commands} commands, ${original.toLocaleString()} → ${compressed.toLocaleString()} bytes)${ANSI.reset}`);
|
|
269
|
+
}
|
|
261
270
|
}
|
|
262
271
|
|
|
263
272
|
function printSessionBudget(budget) {
|
|
@@ -268,11 +277,15 @@ function printSessionBudget(budget) {
|
|
|
268
277
|
}
|
|
269
278
|
const estPrefix = budget.includes_estimates ? "~" : "";
|
|
270
279
|
const estNote = budget.includes_estimates ? " (includes estimates)" : "";
|
|
271
|
-
|
|
280
|
+
const fmtTokens = (n) => Number(n || 0).toLocaleString("en-US");
|
|
281
|
+
console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${estPrefix}${fmtTokens(budget.total_tokens)}${estNote}${ANSI.reset}`);
|
|
272
282
|
console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: ${estPrefix}$${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
|
|
273
283
|
for (const [role, metrics] of Object.entries(budget.breakdown_by_role || {})) {
|
|
284
|
+
const tokens = Number(metrics.total_tokens || 0);
|
|
285
|
+
const cost = Number(metrics.total_cost_usd || 0);
|
|
286
|
+
if (tokens === 0 && cost === 0) continue; // skip roles with no usage
|
|
274
287
|
console.log(
|
|
275
|
-
` ${ANSI.dim} - ${role}: ${estPrefix}${
|
|
288
|
+
` ${ANSI.dim} - ${role}: ${estPrefix}${fmtTokens(tokens)} tokens, ${estPrefix}$${cost.toFixed(2)}${ANSI.reset}`
|
|
276
289
|
);
|
|
277
290
|
}
|
|
278
291
|
}
|
|
@@ -328,6 +341,10 @@ const EVENT_HANDLERS = {
|
|
|
328
341
|
|
|
329
342
|
"tdd:result": (event, icon) => {
|
|
330
343
|
const tdd = event.detail || {};
|
|
344
|
+
if (tdd.reason === "no_diff_available") {
|
|
345
|
+
console.log(` \u251c\u2500 ${icon} TDD policy: ${ANSI.yellow}SKIP${ANSI.reset} ${ANSI.dim}(no diff available)${ANSI.reset}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
331
348
|
const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
|
|
332
349
|
const files = tdd.sourceFiles === undefined ? "" : ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)`;
|
|
333
350
|
const executor = formatExecutor(event.detail);
|
|
@@ -435,14 +452,16 @@ const EVENT_HANDLERS = {
|
|
|
435
452
|
const max = Number(d.max_budget_usd);
|
|
436
453
|
const pct = Number(d.pct_used ?? 0);
|
|
437
454
|
const warn = Number(d.warn_threshold_pct ?? 80);
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
455
|
+
// Don't show N/A — just skip budget display when there's nothing to report
|
|
456
|
+
if (total === 0 && totalTokens === 0) return;
|
|
457
|
+
const fmtTokens = (n) => n.toLocaleString("en-US");
|
|
458
|
+
const tokenStr = totalTokens > 0 ? ` / ${fmtTokens(totalTokens)} tokens` : "";
|
|
459
|
+
const costStr = `$${total.toFixed(2)}`;
|
|
443
460
|
const color = budgetColor(max, pct, warn);
|
|
444
|
-
if (Number.isFinite(max) && max
|
|
445
|
-
console.log(` \u251c\u2500 ${icon} Budget: ${color}
|
|
461
|
+
if (Number.isFinite(max) && max > 0) {
|
|
462
|
+
console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
|
|
463
|
+
} else {
|
|
464
|
+
console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr}${ANSI.reset}`);
|
|
446
465
|
}
|
|
447
466
|
},
|
|
448
467
|
|
|
@@ -569,9 +588,9 @@ const EVENT_HANDLERS = {
|
|
|
569
588
|
}
|
|
570
589
|
},
|
|
571
590
|
|
|
572
|
-
"
|
|
591
|
+
"ci:pr-created": (event) => {
|
|
573
592
|
const url = event.detail?.prUrl || "";
|
|
574
|
-
console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80
|
|
593
|
+
console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 CI PR created: ${url}${ANSI.reset}`);
|
|
575
594
|
},
|
|
576
595
|
|
|
577
596
|
"solomon:alert": (event) => {
|
|
@@ -597,7 +616,6 @@ const QUIET_SUPPRESSED = new Set([
|
|
|
597
616
|
"skills:auto-install",
|
|
598
617
|
"context:loaded",
|
|
599
618
|
"rtk:detected",
|
|
600
|
-
"proxy:started",
|
|
601
619
|
"board:started",
|
|
602
620
|
"plan:loaded",
|
|
603
621
|
"tdd:auto-detect",
|
|
@@ -148,14 +148,29 @@ export function scanForInjection(text, opts = {}) {
|
|
|
148
148
|
export function scanDiff(diff, opts = {}) {
|
|
149
149
|
if (!diff) return { clean: true, findings: [], summary: "Empty diff" };
|
|
150
150
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
// Split diff into per-file chunks to avoid false positives from
|
|
152
|
+
// concatenated added-lines across unrelated files.
|
|
153
|
+
const files = diff.split(/^diff --git /m).filter(Boolean);
|
|
154
|
+
|
|
155
|
+
const allFindings = [];
|
|
156
|
+
for (const fileChunk of files) {
|
|
157
|
+
const addedLines = fileChunk
|
|
158
|
+
.split("\n")
|
|
159
|
+
.filter((l) => l.startsWith("+") && !l.startsWith("+++"))
|
|
160
|
+
.map((l) => l.slice(1))
|
|
161
|
+
.join("\n");
|
|
162
|
+
|
|
163
|
+
if (!addedLines) continue;
|
|
164
|
+
const result = scanForInjection(addedLines, opts);
|
|
165
|
+
if (!result.clean) allFindings.push(...result.findings);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const clean = allFindings.length === 0;
|
|
169
|
+
return {
|
|
170
|
+
clean,
|
|
171
|
+
findings: allFindings,
|
|
172
|
+
summary: clean ? "No injection patterns detected" : `${allFindings.length} injection pattern(s) detected across diff`
|
|
173
|
+
};
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
// --- Helpers ---
|
package/src/utils/logger.js
CHANGED
|
@@ -19,7 +19,12 @@ const LEVEL_COLORS = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
function timestamp() {
|
|
22
|
-
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
24
|
+
const m = String(now.getMinutes()).padStart(2, "0");
|
|
25
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
26
|
+
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
|
27
|
+
return `${h}:${m}:${s}.${ms}`;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
function formatContext(ctx) {
|
package/src/utils/process.js
CHANGED
|
@@ -76,10 +76,9 @@ async function awaitWithTimeout(subprocess, timeout, getStdout) {
|
|
|
76
76
|
|
|
77
77
|
export async function runCommand(command, args = [], options = {}) {
|
|
78
78
|
const { timeout, onOutput, silenceTimeoutMs, partialOutputFlushMs, ...rest } = options;
|
|
79
|
-
//
|
|
80
|
-
// Without this, a subprocess prompting for input
|
|
81
|
-
|
|
82
|
-
if (!rest.stdin) rest.stdin = "ignore";
|
|
79
|
+
// Detach stdin unless the caller provides input (e.g. Codex uses input: prompt).
|
|
80
|
+
// Without this, a subprocess prompting for input hangs forever since there is no TTY.
|
|
81
|
+
if (!rest.stdin && !rest.input) rest.stdin = "ignore";
|
|
83
82
|
const subprocess = execa(command, args, { reject: false, ...rest });
|
|
84
83
|
|
|
85
84
|
let stdoutAccum = "";
|