karajan-code 1.25.3 → 1.27.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 +1 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +31 -0
- package/src/commands/init.js +16 -0
- package/src/orchestrator/post-loop-stages.js +112 -26
- package/src/orchestrator/preflight-checks.js +266 -0
- package/src/orchestrator.js +18 -1
package/README.md
CHANGED
|
@@ -459,6 +459,7 @@ Karajan Code works great on its own, but combining it with these MCP servers giv
|
|
|
459
459
|
| [**GitHub MCP**](https://github.com/modelcontextprotocol/servers/tree/main/src/github) | Create PRs, manage issues, read repos directly from the agent | Combine with `--auto-push` for end-to-end: code → review → push → PR |
|
|
460
460
|
| [**Serena**](https://github.com/oramasearch/serena) | Symbol-level code navigation (find references, go-to-definition) for JS/TS projects | Enable with `--enable-serena` to inject symbol context into coder/reviewer prompts |
|
|
461
461
|
| [**Chrome DevTools MCP**](https://github.com/anthropics/anthropic-quickstarts/tree/main/chrome-devtools-mcp) | Browser automation, screenshots, console/network inspection | Verify UI changes visually after `kj` modifies frontend code |
|
|
462
|
+
| [**RTK**](https://github.com/rtk-ai/rtk) | Reduces LLM token consumption by 60-90% on Bash command outputs (git, test, build) | Install globally with `brew install rtk && rtk init --global` — all KJ agent commands automatically compressed |
|
|
462
463
|
|
|
463
464
|
## Role Templates
|
|
464
465
|
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -204,6 +204,36 @@ async function checkBecariaInfra(config) {
|
|
|
204
204
|
return checks;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
async function checkRtk() {
|
|
208
|
+
try {
|
|
209
|
+
const res = await runCommand("rtk", ["--version"]);
|
|
210
|
+
if (res.exitCode === 0) {
|
|
211
|
+
return {
|
|
212
|
+
name: "rtk",
|
|
213
|
+
label: "RTK (Rust Token Killer)",
|
|
214
|
+
ok: true,
|
|
215
|
+
detail: `${res.stdout.trim()} — token savings active`,
|
|
216
|
+
fix: null
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
name: "rtk",
|
|
221
|
+
label: "RTK (Rust Token Killer)",
|
|
222
|
+
ok: true,
|
|
223
|
+
detail: "Not found — install for 60-90% token savings: brew install rtk",
|
|
224
|
+
fix: null
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return {
|
|
228
|
+
name: "rtk",
|
|
229
|
+
label: "RTK (Rust Token Killer)",
|
|
230
|
+
ok: true,
|
|
231
|
+
detail: "Not found — install for 60-90% token savings: brew install rtk",
|
|
232
|
+
fix: null
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
207
237
|
async function checkRuleFiles(config) {
|
|
208
238
|
const projectDir = config.projectDir || process.cwd();
|
|
209
239
|
const reviewRules = await loadFirstExisting(resolveRoleMdPath("reviewer", projectDir));
|
|
@@ -248,6 +278,7 @@ export async function runChecks({ config }) {
|
|
|
248
278
|
}
|
|
249
279
|
|
|
250
280
|
checks.push(...await checkRuleFiles(config));
|
|
281
|
+
checks.push(await checkRtk());
|
|
251
282
|
|
|
252
283
|
return checks;
|
|
253
284
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { exists, ensureDir } from "../utils/fs.js";
|
|
|
7
7
|
import { getKarajanHome } from "../utils/paths.js";
|
|
8
8
|
import { detectAvailableAgents } from "../utils/agent-detect.js";
|
|
9
9
|
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
10
|
+
import { runCommand } from "../utils/process.js";
|
|
10
11
|
|
|
11
12
|
async function runWizard(config, logger) {
|
|
12
13
|
const agents = await detectAvailableAgents();
|
|
@@ -270,6 +271,21 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
270
271
|
await ensureReviewRules(reviewRulesPath, logger);
|
|
271
272
|
await ensureCoderRules(coderRulesPath, logger);
|
|
272
273
|
await installSkills(logger, interactive);
|
|
274
|
+
|
|
275
|
+
// Check RTK availability and inform user
|
|
276
|
+
let hasRtk = false;
|
|
277
|
+
try {
|
|
278
|
+
const rtkRes = await runCommand("rtk", ["--version"]);
|
|
279
|
+
hasRtk = rtkRes.exitCode === 0;
|
|
280
|
+
} catch {
|
|
281
|
+
hasRtk = false;
|
|
282
|
+
}
|
|
283
|
+
if (!hasRtk) {
|
|
284
|
+
logger.info("");
|
|
285
|
+
logger.info("RTK (Rust Token Killer) can reduce token usage by 60-90%.");
|
|
286
|
+
logger.info(" Install: brew install rtk && rtk init --global");
|
|
287
|
+
}
|
|
288
|
+
|
|
273
289
|
await setupSonarQube(config, logger);
|
|
274
290
|
await scaffoldBecariaGateway(config, flags, logger);
|
|
275
291
|
}
|
|
@@ -5,6 +5,93 @@ import { addCheckpoint, saveSession } from "../session-store.js";
|
|
|
5
5
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
6
6
|
import { invokeSolomon } from "./solomon-escalation.js";
|
|
7
7
|
|
|
8
|
+
const KNOWN_AGENTS = ["claude", "codex", "gemini"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build an ordered fallback chain for a role.
|
|
12
|
+
* Primary provider first, then remaining known agents (no duplicates).
|
|
13
|
+
*/
|
|
14
|
+
function buildFallbackChain(config, roleName) {
|
|
15
|
+
const primary =
|
|
16
|
+
config?.roles?.[roleName]?.provider ||
|
|
17
|
+
config?.roles?.coder?.provider ||
|
|
18
|
+
config?.coder ||
|
|
19
|
+
"claude";
|
|
20
|
+
return [primary, ...KNOWN_AGENTS.filter((a) => a !== primary)];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect if a role output is an agent/spawn failure (vs a genuine evaluation failure).
|
|
25
|
+
* Agent failures have `result.error` but no `result.verdict`.
|
|
26
|
+
*/
|
|
27
|
+
function isAgentFailure(output) {
|
|
28
|
+
if (!output || output.ok) return false;
|
|
29
|
+
return Boolean(output.result?.error) && !output.result?.verdict;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run a role (TesterRole or SecurityRole) with agent fallback chain.
|
|
34
|
+
* If the primary agent fails to start (spawn/auth failure), tries the next agent.
|
|
35
|
+
* Genuine evaluation failures (agent ran but verdict=fail) are NOT retried.
|
|
36
|
+
*
|
|
37
|
+
* @returns {{ output, provider, attempts }}
|
|
38
|
+
*/
|
|
39
|
+
async function runRoleWithFallback(RoleClass, { roleName, config, logger, emitter, eventBase, task, iteration, diff }) {
|
|
40
|
+
const chain = buildFallbackChain(config, roleName);
|
|
41
|
+
const attempts = [];
|
|
42
|
+
|
|
43
|
+
for (const provider of chain) {
|
|
44
|
+
const overrideConfig = {
|
|
45
|
+
...config,
|
|
46
|
+
roles: { ...config.roles, [roleName]: { ...config.roles?.[roleName], provider } }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const role = new RoleClass({ config: overrideConfig, logger, emitter });
|
|
50
|
+
await role.init({ task, iteration });
|
|
51
|
+
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
let output;
|
|
54
|
+
try {
|
|
55
|
+
output = await role.run({ task, diff });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
output = {
|
|
58
|
+
ok: false,
|
|
59
|
+
result: { error: err.message, provider },
|
|
60
|
+
summary: `${roleName} threw: ${err.message}`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const duration = Date.now() - start;
|
|
64
|
+
|
|
65
|
+
attempts.push({ provider, ok: output.ok, duration, summary: output.summary });
|
|
66
|
+
|
|
67
|
+
if (output.ok || !isAgentFailure(output)) {
|
|
68
|
+
return { output, provider, attempts };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.warn(`${roleName} agent "${provider}" failed (${duration}ms): ${output.summary} — trying next agent`);
|
|
72
|
+
emitProgress(emitter, makeEvent(`${roleName}:fallback`, { ...eventBase, stage: roleName }, {
|
|
73
|
+
status: "warn",
|
|
74
|
+
message: `Agent "${provider}" failed, falling back`,
|
|
75
|
+
detail: { provider, duration, summary: output.summary, remaining: chain.length - attempts.length }
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// All agents failed
|
|
80
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
81
|
+
const allProviders = attempts.map((a) => a.provider).join(", ");
|
|
82
|
+
logger.error(`${roleName}: all agents failed (${allProviders})`);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
output: {
|
|
86
|
+
ok: false,
|
|
87
|
+
result: { error: `All agents failed: ${allProviders}`, attempts },
|
|
88
|
+
summary: `All ${roleName} agents failed (${allProviders}) — check agent installation and configuration`
|
|
89
|
+
},
|
|
90
|
+
provider: lastAttempt?.provider,
|
|
91
|
+
attempts
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
8
95
|
export async function runTesterStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff, askQuestion }) {
|
|
9
96
|
logger.setContext({ iteration, stage: "tester" });
|
|
10
97
|
emitProgress(
|
|
@@ -14,30 +101,28 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
|
|
|
14
101
|
})
|
|
15
102
|
);
|
|
16
103
|
|
|
17
|
-
const tester = new TesterRole({ config, logger, emitter });
|
|
18
|
-
await tester.init({ task, iteration });
|
|
19
104
|
const testerStart = Date.now();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
105
|
+
const { output: testerOutput, provider, attempts } = await runRoleWithFallback(
|
|
106
|
+
TesterRole,
|
|
107
|
+
{ roleName: "tester", config, logger, emitter, eventBase, task, iteration, diff }
|
|
108
|
+
);
|
|
109
|
+
const totalDuration = Date.now() - testerStart;
|
|
110
|
+
|
|
27
111
|
trackBudget({
|
|
28
112
|
role: "tester",
|
|
29
|
-
provider:
|
|
113
|
+
provider: provider || coderRole.provider,
|
|
30
114
|
model: config?.roles?.tester?.model || coderRole.model,
|
|
31
115
|
result: testerOutput,
|
|
32
|
-
duration_ms:
|
|
116
|
+
duration_ms: totalDuration
|
|
33
117
|
});
|
|
34
118
|
|
|
35
119
|
await addCheckpoint(session, {
|
|
36
120
|
stage: "tester",
|
|
37
121
|
iteration,
|
|
38
122
|
ok: testerOutput.ok,
|
|
39
|
-
provider:
|
|
40
|
-
model: config?.roles?.tester?.model || coderRole.model || null
|
|
123
|
+
provider: provider || coderRole.provider,
|
|
124
|
+
model: config?.roles?.tester?.model || coderRole.model || null,
|
|
125
|
+
attempts: attempts.length > 1 ? attempts : undefined
|
|
41
126
|
});
|
|
42
127
|
|
|
43
128
|
emitProgress(
|
|
@@ -94,30 +179,28 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
|
|
|
94
179
|
})
|
|
95
180
|
);
|
|
96
181
|
|
|
97
|
-
const security = new SecurityRole({ config, logger, emitter });
|
|
98
|
-
await security.init({ task, iteration });
|
|
99
182
|
const securityStart = Date.now();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
183
|
+
const { output: securityOutput, provider, attempts } = await runRoleWithFallback(
|
|
184
|
+
SecurityRole,
|
|
185
|
+
{ roleName: "security", config, logger, emitter, eventBase, task, iteration, diff }
|
|
186
|
+
);
|
|
187
|
+
const totalDuration = Date.now() - securityStart;
|
|
188
|
+
|
|
107
189
|
trackBudget({
|
|
108
190
|
role: "security",
|
|
109
|
-
provider:
|
|
191
|
+
provider: provider || coderRole.provider,
|
|
110
192
|
model: config?.roles?.security?.model || coderRole.model,
|
|
111
193
|
result: securityOutput,
|
|
112
|
-
duration_ms:
|
|
194
|
+
duration_ms: totalDuration
|
|
113
195
|
});
|
|
114
196
|
|
|
115
197
|
await addCheckpoint(session, {
|
|
116
198
|
stage: "security",
|
|
117
199
|
iteration,
|
|
118
200
|
ok: securityOutput.ok,
|
|
119
|
-
provider:
|
|
120
|
-
model: config?.roles?.security?.model || coderRole.model || null
|
|
201
|
+
provider: provider || coderRole.provider,
|
|
202
|
+
model: config?.roles?.security?.model || coderRole.model || null,
|
|
203
|
+
attempts: attempts.length > 1 ? attempts : undefined
|
|
121
204
|
});
|
|
122
205
|
|
|
123
206
|
emitProgress(
|
|
@@ -214,3 +297,6 @@ export async function runImpeccableStage({ config, logger, emitter, eventBase, s
|
|
|
214
297
|
// Impeccable is advisory — failures do not block the pipeline
|
|
215
298
|
return { action: "ok", stageResult: { ok: impeccableOutput.ok, verdict, summary: impeccableOutput.summary || "No frontend design issues found" } };
|
|
216
299
|
}
|
|
300
|
+
|
|
301
|
+
// Exported for testing
|
|
302
|
+
export { buildFallbackChain, isAgentFailure, runRoleWithFallback };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight environment checks for kj_run.
|
|
3
|
+
*
|
|
4
|
+
* Runs AFTER policy resolution (so we know which stages are active)
|
|
5
|
+
* and BEFORE session iteration loop (so we fail fast or degrade gracefully).
|
|
6
|
+
*
|
|
7
|
+
* Design: always returns ok:true (graceful degradation, never hard-fail).
|
|
8
|
+
* Disabled stages are auto-disabled via configOverrides instead of blocking.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { checkBinary } from "../utils/agent-detect.js";
|
|
12
|
+
import { isSonarReachable, sonarUp } from "../sonar/manager.js";
|
|
13
|
+
import { runCommand } from "../utils/process.js";
|
|
14
|
+
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
15
|
+
|
|
16
|
+
function normalizeApiHost(rawHost) {
|
|
17
|
+
return String(rawHost || "http://localhost:9000").replace(/host\.docker\.internal/g, "localhost");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseJsonSafe(text) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function checkDocker() {
|
|
29
|
+
const result = await checkBinary("docker");
|
|
30
|
+
return {
|
|
31
|
+
name: "docker",
|
|
32
|
+
ok: result.ok,
|
|
33
|
+
detail: result.ok ? `Docker ${result.version}` : "Docker not found",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function checkSonarReachable(host) {
|
|
38
|
+
const reachable = await isSonarReachable(host);
|
|
39
|
+
if (reachable) {
|
|
40
|
+
return { name: "sonar-reachable", ok: true, detail: `SonarQube reachable at ${host}`, remediated: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Auto-remediation: try to start SonarQube
|
|
44
|
+
try {
|
|
45
|
+
const upResult = await sonarUp(host);
|
|
46
|
+
if (upResult.exitCode === 0) {
|
|
47
|
+
// Verify it's actually reachable now
|
|
48
|
+
const reachableAfter = await isSonarReachable(host);
|
|
49
|
+
if (reachableAfter) {
|
|
50
|
+
return { name: "sonar-reachable", ok: true, detail: `SonarQube started and reachable at ${host}`, remediated: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// sonarUp failed, fall through
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { name: "sonar-reachable", ok: false, detail: `SonarQube not reachable at ${host} (auto-start failed)` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function checkSonarAuth(config) {
|
|
61
|
+
const host = normalizeApiHost(config.sonarqube?.host);
|
|
62
|
+
|
|
63
|
+
// Check explicit token first
|
|
64
|
+
const explicitToken = process.env.KJ_SONAR_TOKEN || process.env.SONAR_TOKEN || config.sonarqube?.token;
|
|
65
|
+
if (explicitToken) {
|
|
66
|
+
// Validate the token works
|
|
67
|
+
const res = await runCommand("curl", [
|
|
68
|
+
"-sS", "-o", "/dev/null", "-w", "%{http_code}",
|
|
69
|
+
"-H", `Authorization: Bearer ${explicitToken}`,
|
|
70
|
+
"--max-time", "5",
|
|
71
|
+
`${host}/api/authentication/validate`
|
|
72
|
+
]);
|
|
73
|
+
if (res.exitCode === 0 && res.stdout.trim().startsWith("2")) {
|
|
74
|
+
return { name: "sonar-auth", ok: true, detail: "Sonar token valid", token: explicitToken };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try admin credentials to generate a token
|
|
79
|
+
const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube?.admin_user || "admin";
|
|
80
|
+
const candidates = [
|
|
81
|
+
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
82
|
+
config.sonarqube?.admin_password,
|
|
83
|
+
"admin"
|
|
84
|
+
].filter(Boolean);
|
|
85
|
+
|
|
86
|
+
for (const password of [...new Set(candidates)]) {
|
|
87
|
+
const validateRes = await runCommand("curl", [
|
|
88
|
+
"-sS", "-u", `${adminUser}:${password}`,
|
|
89
|
+
`${host}/api/authentication/validate`
|
|
90
|
+
]);
|
|
91
|
+
if (validateRes.exitCode !== 0) continue;
|
|
92
|
+
const parsed = parseJsonSafe(validateRes.stdout);
|
|
93
|
+
if (!parsed?.valid) continue;
|
|
94
|
+
|
|
95
|
+
// Generate a user token
|
|
96
|
+
const tokenName = `karajan-preflight-${Date.now()}`;
|
|
97
|
+
const tokenRes = await runCommand("curl", [
|
|
98
|
+
"-sS", "-u", `${adminUser}:${password}`,
|
|
99
|
+
"-X", "POST",
|
|
100
|
+
"--data-urlencode", `name=${tokenName}`,
|
|
101
|
+
`${host}/api/user_tokens/generate`
|
|
102
|
+
]);
|
|
103
|
+
if (tokenRes.exitCode !== 0) continue;
|
|
104
|
+
const tokenParsed = parseJsonSafe(tokenRes.stdout);
|
|
105
|
+
if (tokenParsed?.token) {
|
|
106
|
+
return { name: "sonar-auth", ok: true, detail: "Sonar token generated", token: tokenParsed.token };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { name: "sonar-auth", ok: false, detail: "Could not validate or generate Sonar token" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function checkSecurityAgent(config) {
|
|
114
|
+
const provider = config.roles?.security?.provider
|
|
115
|
+
|| config.roles?.coder?.provider
|
|
116
|
+
|| config.coder
|
|
117
|
+
|| "claude";
|
|
118
|
+
|
|
119
|
+
const result = await checkBinary(provider);
|
|
120
|
+
return {
|
|
121
|
+
name: "security-agent",
|
|
122
|
+
ok: result.ok,
|
|
123
|
+
detail: result.ok ? `Security agent "${provider}" available (${result.version})` : `Security agent "${provider}" not found`,
|
|
124
|
+
provider,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run preflight environment checks.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} opts
|
|
132
|
+
* @param {object} opts.config - Karajan config
|
|
133
|
+
* @param {object} opts.logger - Logger instance
|
|
134
|
+
* @param {object|null} opts.emitter - Event emitter
|
|
135
|
+
* @param {object} opts.eventBase - Base event data
|
|
136
|
+
* @param {object} opts.resolvedPolicies - Output from applyPolicies()
|
|
137
|
+
* @param {boolean} opts.securityEnabled - Whether security stage is enabled
|
|
138
|
+
* @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[] }}
|
|
139
|
+
*/
|
|
140
|
+
export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
|
|
141
|
+
const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
|
|
142
|
+
const isExternalSonar = Boolean(config.sonarqube?.external);
|
|
143
|
+
const sonarHost = normalizeApiHost(config.sonarqube?.host);
|
|
144
|
+
|
|
145
|
+
const result = {
|
|
146
|
+
ok: true,
|
|
147
|
+
checks: [],
|
|
148
|
+
remediations: [],
|
|
149
|
+
configOverrides: {},
|
|
150
|
+
warnings: [],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Short-circuit: nothing to check
|
|
154
|
+
if (!sonarEnabled && !securityEnabled) {
|
|
155
|
+
logger.info("Preflight: skipped (no sonar, no security)");
|
|
156
|
+
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
157
|
+
message: "Preflight skipped (no checks needed)",
|
|
158
|
+
detail: result
|
|
159
|
+
}));
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
emitProgress(emitter, makeEvent("preflight:start", { ...eventBase, stage: "preflight" }, {
|
|
164
|
+
message: "Running preflight environment checks",
|
|
165
|
+
detail: { sonarEnabled, securityEnabled }
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
// --- 1. Docker (only if sonar enabled and not external) ---
|
|
169
|
+
if (sonarEnabled && !isExternalSonar) {
|
|
170
|
+
const dockerCheck = await checkDocker();
|
|
171
|
+
result.checks.push(dockerCheck);
|
|
172
|
+
|
|
173
|
+
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
174
|
+
status: dockerCheck.ok ? "ok" : "warn",
|
|
175
|
+
message: `Docker: ${dockerCheck.detail}`,
|
|
176
|
+
detail: dockerCheck
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
if (!dockerCheck.ok) {
|
|
180
|
+
result.configOverrides.sonarDisabled = true;
|
|
181
|
+
result.warnings.push("Docker not available — SonarQube auto-disabled");
|
|
182
|
+
logger.warn("Preflight: Docker not found, disabling SonarQube");
|
|
183
|
+
|
|
184
|
+
// Skip remaining sonar checks, continue to security
|
|
185
|
+
if (!securityEnabled) {
|
|
186
|
+
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
187
|
+
status: "warn", message: "Preflight completed with warnings", detail: result
|
|
188
|
+
}));
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- 2. SonarQube reachable ---
|
|
195
|
+
if (sonarEnabled && !result.configOverrides.sonarDisabled) {
|
|
196
|
+
const reachableCheck = await checkSonarReachable(sonarHost);
|
|
197
|
+
result.checks.push(reachableCheck);
|
|
198
|
+
|
|
199
|
+
if (reachableCheck.remediated) {
|
|
200
|
+
result.remediations.push("SonarQube auto-started via docker compose");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
204
|
+
status: reachableCheck.ok ? "ok" : "warn",
|
|
205
|
+
message: `SonarQube reachability: ${reachableCheck.detail}`,
|
|
206
|
+
detail: reachableCheck
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
if (!reachableCheck.ok) {
|
|
210
|
+
result.configOverrides.sonarDisabled = true;
|
|
211
|
+
result.warnings.push("SonarQube not reachable — auto-disabled");
|
|
212
|
+
logger.warn("Preflight: SonarQube not reachable after remediation, disabling");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- 3. SonarQube auth/token ---
|
|
217
|
+
if (sonarEnabled && !result.configOverrides.sonarDisabled) {
|
|
218
|
+
const authCheck = await checkSonarAuth(config);
|
|
219
|
+
result.checks.push(authCheck);
|
|
220
|
+
|
|
221
|
+
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
222
|
+
status: authCheck.ok ? "ok" : "warn",
|
|
223
|
+
message: `SonarQube auth: ${authCheck.detail}`,
|
|
224
|
+
detail: { name: authCheck.name, ok: authCheck.ok, detail: authCheck.detail }
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
if (authCheck.ok && authCheck.token) {
|
|
228
|
+
process.env.KJ_SONAR_TOKEN = authCheck.token;
|
|
229
|
+
result.remediations.push("Sonar token resolved and cached in KJ_SONAR_TOKEN");
|
|
230
|
+
logger.info("Preflight: Sonar token resolved and cached");
|
|
231
|
+
} else if (!authCheck.ok) {
|
|
232
|
+
result.configOverrides.sonarDisabled = true;
|
|
233
|
+
result.warnings.push("SonarQube auth failed — auto-disabled");
|
|
234
|
+
logger.warn("Preflight: Sonar auth failed, disabling SonarQube");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- 4. Security agent ---
|
|
239
|
+
if (securityEnabled) {
|
|
240
|
+
const secCheck = await checkSecurityAgent(config);
|
|
241
|
+
result.checks.push(secCheck);
|
|
242
|
+
|
|
243
|
+
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
244
|
+
status: secCheck.ok ? "ok" : "warn",
|
|
245
|
+
message: `Security agent: ${secCheck.detail}`,
|
|
246
|
+
detail: secCheck
|
|
247
|
+
}));
|
|
248
|
+
|
|
249
|
+
if (!secCheck.ok) {
|
|
250
|
+
result.configOverrides.securityDisabled = true;
|
|
251
|
+
result.warnings.push(`Security agent "${secCheck.provider}" not found — security stage auto-disabled`);
|
|
252
|
+
logger.warn(`Preflight: Security agent "${secCheck.provider}" not found, disabling security stage`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const hasWarnings = result.warnings.length > 0;
|
|
257
|
+
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
258
|
+
status: hasWarnings ? "warn" : "ok",
|
|
259
|
+
message: hasWarnings
|
|
260
|
+
? `Preflight completed with ${result.warnings.length} warning(s)`
|
|
261
|
+
: "Preflight passed — all checks OK",
|
|
262
|
+
detail: result
|
|
263
|
+
}));
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
}
|
package/src/orchestrator.js
CHANGED
|
@@ -34,6 +34,7 @@ import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, run
|
|
|
34
34
|
import { runTesterStage, runSecurityStage, runImpeccableStage } from "./orchestrator/post-loop-stages.js";
|
|
35
35
|
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
|
|
36
36
|
import { detectTestFramework } from "./utils/project-detect.js";
|
|
37
|
+
import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
// --- Extracted helper functions (pure refactoring, zero behavior change) ---
|
|
@@ -857,7 +858,23 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
857
858
|
}));
|
|
858
859
|
}
|
|
859
860
|
|
|
860
|
-
|
|
861
|
+
let updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
|
|
862
|
+
|
|
863
|
+
// --- Preflight environment checks ---
|
|
864
|
+
const preflightResult = await runPreflightChecks({
|
|
865
|
+
config: updatedConfig, logger, emitter, eventBase,
|
|
866
|
+
resolvedPolicies: session.resolved_policies,
|
|
867
|
+
securityEnabled: pipelineFlags.securityEnabled
|
|
868
|
+
});
|
|
869
|
+
session.preflight = preflightResult;
|
|
870
|
+
await saveSession(session);
|
|
871
|
+
|
|
872
|
+
if (preflightResult.configOverrides.sonarDisabled) {
|
|
873
|
+
updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
|
|
874
|
+
}
|
|
875
|
+
if (preflightResult.configOverrides.securityDisabled) {
|
|
876
|
+
pipelineFlags.securityEnabled = false;
|
|
877
|
+
}
|
|
861
878
|
|
|
862
879
|
// --- Researcher → Planner ---
|
|
863
880
|
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
|