karajan-code 1.10.1 → 1.11.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/README.md +25 -9
- package/bin/kj-tail +70 -0
- package/docs/README.es.md +8 -5
- package/package.json +2 -1
- package/src/agents/claude-agent.js +12 -2
- package/src/cli.js +3 -2
- package/src/commands/agents.js +45 -21
- package/src/config.js +37 -16
- package/src/mcp/preflight.js +28 -0
- package/src/mcp/server-handlers.js +106 -7
- package/src/mcp/tools.js +19 -1
- package/src/orchestrator/iteration-stages.js +30 -43
- package/src/orchestrator/solomon-rules.js +138 -0
- package/src/orchestrator/standby.js +70 -0
- package/src/orchestrator.js +107 -0
- package/src/prompts/triage.js +61 -0
- package/src/roles/triage-role.js +2 -26
- package/src/utils/display.js +21 -0
- package/src/utils/rate-limit-detector.js +65 -4
- package/src/utils/run-log.js +75 -1
package/src/mcp/tools.js
CHANGED
|
@@ -149,6 +149,24 @@ export const tools = [
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
},
|
|
152
|
+
{
|
|
153
|
+
name: "kj_preflight",
|
|
154
|
+
description: "Confirm or adjust agent configuration before first kj_run/kj_code. REQUIRED before running any task via MCP. Show the config to the human, get their confirmation or adjustments, then call this tool with their response.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
required: ["humanResponse"],
|
|
158
|
+
properties: {
|
|
159
|
+
humanResponse: { type: "string", description: "The human's response: 'ok' to confirm defaults, or specific changes like 'use gemini as coder'" },
|
|
160
|
+
coder: { type: "string", description: "Override coder for this session" },
|
|
161
|
+
reviewer: { type: "string", description: "Override reviewer for this session" },
|
|
162
|
+
tester: { type: "string", description: "Override tester for this session" },
|
|
163
|
+
security: { type: "string", description: "Override security for this session" },
|
|
164
|
+
solomon: { type: "string", description: "Override solomon for this session" },
|
|
165
|
+
enableTester: { type: "boolean", description: "Enable/disable tester for this session" },
|
|
166
|
+
enableSecurity: { type: "boolean", description: "Enable/disable security for this session" }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
152
170
|
{
|
|
153
171
|
name: "kj_code",
|
|
154
172
|
description: "Run coder-only mode",
|
|
@@ -180,7 +198,7 @@ export const tools = [
|
|
|
180
198
|
},
|
|
181
199
|
{
|
|
182
200
|
name: "kj_status",
|
|
183
|
-
description: "Show real-time log of the current or last Karajan run. Use this to monitor progress while kj_run/kj_plan/kj_code is executing.
|
|
201
|
+
description: "Show real-time status and log of the current or last Karajan run. Returns a parsed status (current stage, agent, iteration, errors) plus recent log lines. Use this to monitor progress while kj_run/kj_plan/kj_code is executing.",
|
|
184
202
|
inputSchema: {
|
|
185
203
|
type: "object",
|
|
186
204
|
properties: {
|
|
@@ -93,21 +93,16 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// No fallback or fallback also failed —
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
message: question,
|
|
107
|
-
detail: { agent: coderRole.provider, rateLimitMessage: rateLimitCheck.message, sessionId: session.id }
|
|
108
|
-
})
|
|
109
|
-
);
|
|
110
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question, context: "rate_limit" } };
|
|
96
|
+
// No fallback or fallback also failed — enter standby
|
|
97
|
+
return {
|
|
98
|
+
action: "standby",
|
|
99
|
+
standbyInfo: {
|
|
100
|
+
agent: coderRole.provider,
|
|
101
|
+
cooldownMs: rateLimitCheck.cooldownMs,
|
|
102
|
+
cooldownUntil: rateLimitCheck.cooldownUntil,
|
|
103
|
+
message: rateLimitCheck.message
|
|
104
|
+
}
|
|
105
|
+
};
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
await markSessionStatus(session, "failed");
|
|
@@ -167,20 +162,16 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
|
|
|
167
162
|
});
|
|
168
163
|
|
|
169
164
|
if (rateLimitCheck.isRateLimit) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
detail: { agent: refactorerRole.provider, rateLimitMessage: rateLimitCheck.message, sessionId: session.id }
|
|
181
|
-
})
|
|
182
|
-
);
|
|
183
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question, context: "rate_limit" } };
|
|
165
|
+
// Enter standby instead of pausing
|
|
166
|
+
return {
|
|
167
|
+
action: "standby",
|
|
168
|
+
standbyInfo: {
|
|
169
|
+
agent: refactorerRole.provider,
|
|
170
|
+
cooldownMs: rateLimitCheck.cooldownMs,
|
|
171
|
+
cooldownUntil: rateLimitCheck.cooldownUntil,
|
|
172
|
+
message: rateLimitCheck.message
|
|
173
|
+
}
|
|
174
|
+
};
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
await markSessionStatus(session, "failed");
|
|
@@ -451,20 +442,16 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
451
442
|
});
|
|
452
443
|
|
|
453
444
|
if (rateLimitCheck.isRateLimit) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
detail: { agent: reviewerRole.provider, rateLimitMessage: rateLimitCheck.message, sessionId: session.id }
|
|
465
|
-
})
|
|
466
|
-
);
|
|
467
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question, context: "rate_limit" } };
|
|
445
|
+
// Enter standby instead of pausing
|
|
446
|
+
return {
|
|
447
|
+
action: "standby",
|
|
448
|
+
standbyInfo: {
|
|
449
|
+
agent: reviewerRole.provider,
|
|
450
|
+
cooldownMs: rateLimitCheck.cooldownMs,
|
|
451
|
+
cooldownUntil: rateLimitCheck.cooldownUntil,
|
|
452
|
+
message: rateLimitCheck.message
|
|
453
|
+
}
|
|
454
|
+
};
|
|
468
455
|
}
|
|
469
456
|
|
|
470
457
|
await markSessionStatus(session, "failed");
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solomon rules engine — detects anomalies during session execution.
|
|
3
|
+
* Each rule returns { triggered: boolean, severity: "warn"|"critical", message, detail }
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RULES = {
|
|
7
|
+
max_files_per_iteration: 10,
|
|
8
|
+
max_stale_iterations: 3,
|
|
9
|
+
no_new_dependencies_without_task: true,
|
|
10
|
+
scope_guard: true
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function evaluateRules(context, rulesConfig = {}) {
|
|
14
|
+
const rules = { ...DEFAULT_RULES, ...rulesConfig };
|
|
15
|
+
const alerts = [];
|
|
16
|
+
|
|
17
|
+
// Rule 1: Too many files modified
|
|
18
|
+
if (rules.max_files_per_iteration && context.filesChanged > rules.max_files_per_iteration) {
|
|
19
|
+
alerts.push({
|
|
20
|
+
rule: "max_files_per_iteration",
|
|
21
|
+
severity: "critical",
|
|
22
|
+
message: `Coder modified ${context.filesChanged} files (limit: ${rules.max_files_per_iteration}). Possible scope drift.`,
|
|
23
|
+
detail: { filesChanged: context.filesChanged, limit: rules.max_files_per_iteration }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Rule 2: Stale iterations (no progress)
|
|
28
|
+
if (rules.max_stale_iterations && context.staleIterations >= rules.max_stale_iterations) {
|
|
29
|
+
alerts.push({
|
|
30
|
+
rule: "max_stale_iterations",
|
|
31
|
+
severity: "critical",
|
|
32
|
+
message: `${context.staleIterations} iterations without progress. Same errors repeating.`,
|
|
33
|
+
detail: { staleIterations: context.staleIterations, limit: rules.max_stale_iterations }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Rule 3: New dependencies not in task
|
|
38
|
+
if (rules.no_new_dependencies_without_task && context.newDependencies?.length > 0) {
|
|
39
|
+
const depsNotInTask = context.newDependencies.filter(
|
|
40
|
+
dep => !context.task?.toLowerCase().includes(dep.toLowerCase())
|
|
41
|
+
);
|
|
42
|
+
if (depsNotInTask.length > 0) {
|
|
43
|
+
alerts.push({
|
|
44
|
+
rule: "no_new_dependencies_without_task",
|
|
45
|
+
severity: "warn",
|
|
46
|
+
message: `New dependencies added not mentioned in task: ${depsNotInTask.join(", ")}`,
|
|
47
|
+
detail: { dependencies: depsNotInTask }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Rule 4: Scope guard — files outside expected scope
|
|
53
|
+
if (rules.scope_guard && context.outOfScopeFiles?.length > 0) {
|
|
54
|
+
alerts.push({
|
|
55
|
+
rule: "scope_guard",
|
|
56
|
+
severity: "warn",
|
|
57
|
+
message: `Files modified outside expected scope: ${context.outOfScopeFiles.join(", ")}`,
|
|
58
|
+
detail: { files: context.outOfScopeFiles }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
alerts,
|
|
64
|
+
hasCritical: alerts.some(a => a.severity === "critical"),
|
|
65
|
+
hasWarnings: alerts.some(a => a.severity === "warn")
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build context for rules evaluation from git diff and session state.
|
|
71
|
+
*/
|
|
72
|
+
export async function buildRulesContext({ session, task, iteration }) {
|
|
73
|
+
const context = {
|
|
74
|
+
task,
|
|
75
|
+
iteration,
|
|
76
|
+
filesChanged: 0,
|
|
77
|
+
staleIterations: 0,
|
|
78
|
+
newDependencies: [],
|
|
79
|
+
outOfScopeFiles: []
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Count files changed via git
|
|
83
|
+
try {
|
|
84
|
+
const { execaCommand } = await import("execa");
|
|
85
|
+
const baseRef = session.session_start_sha || "HEAD~1";
|
|
86
|
+
|
|
87
|
+
// Files changed
|
|
88
|
+
const diffResult = await execaCommand(`git diff --name-only ${baseRef}`, { reject: false });
|
|
89
|
+
if (diffResult.stdout) {
|
|
90
|
+
const files = diffResult.stdout.split("\n").filter(Boolean);
|
|
91
|
+
context.filesChanged = files.length;
|
|
92
|
+
|
|
93
|
+
// Detect scope: config files, CI/CD, etc. that are often out of scope
|
|
94
|
+
const scopePatterns = [".github/", ".gitlab-ci", "docker-compose", ".env", "firebase.json", "firestore.rules"];
|
|
95
|
+
context.outOfScopeFiles = files.filter(f =>
|
|
96
|
+
scopePatterns.some(pattern => f.includes(pattern))
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Detect new dependencies
|
|
100
|
+
if (files.includes("package.json")) {
|
|
101
|
+
try {
|
|
102
|
+
const pkgDiff = await execaCommand(`git diff ${baseRef} -- package.json`, { reject: false });
|
|
103
|
+
const addedDeps = (pkgDiff.stdout || "").split("\n")
|
|
104
|
+
.filter(line => line.startsWith("+") && line.includes('"') && !line.startsWith("+++"))
|
|
105
|
+
.map(line => {
|
|
106
|
+
const match = line.match(/"([^"]+)":\s*"/);
|
|
107
|
+
return match ? match[1] : null;
|
|
108
|
+
})
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.filter(name => !["name", "version", "description", "main", "scripts", "type", "license", "author"].includes(name));
|
|
111
|
+
context.newDependencies = addedDeps;
|
|
112
|
+
} catch { /* ignore */ }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* git not available */ }
|
|
116
|
+
|
|
117
|
+
// Count stale iterations from session checkpoints
|
|
118
|
+
const checkpoints = session.checkpoints || [];
|
|
119
|
+
const recentCoderCheckpoints = checkpoints
|
|
120
|
+
.filter(cp => cp.stage === "coder" || cp.stage === "reviewer")
|
|
121
|
+
.slice(-6); // Last 3 iterations (coder+reviewer each)
|
|
122
|
+
|
|
123
|
+
// Simple heuristic: if last N reviewer checkpoints all have the same note/feedback, it's stale
|
|
124
|
+
if (recentCoderCheckpoints.length >= 4) {
|
|
125
|
+
const lastFeedbacks = checkpoints
|
|
126
|
+
.filter(cp => cp.stage === "reviewer")
|
|
127
|
+
.slice(-3)
|
|
128
|
+
.map(cp => cp.note || "");
|
|
129
|
+
const uniqueFeedbacks = new Set(lastFeedbacks);
|
|
130
|
+
if (uniqueFeedbacks.size === 1 && lastFeedbacks.length >= 2) {
|
|
131
|
+
context.staleIterations = lastFeedbacks.length;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return context;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { DEFAULT_RULES };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
4
|
+
const MAX_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
|
|
5
|
+
const MAX_STANDBY_RETRIES = 5;
|
|
6
|
+
const HEARTBEAT_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wait for a rate limit cooldown, emitting heartbeat events.
|
|
10
|
+
* Returns when the cooldown expires.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} options
|
|
13
|
+
* @param {number|null} options.cooldownMs - Milliseconds to wait (null = use default)
|
|
14
|
+
* @param {string|null} options.cooldownUntil - ISO timestamp when cooldown expires
|
|
15
|
+
* @param {string} options.agent - Agent that was rate-limited
|
|
16
|
+
* @param {number} options.retryCount - Current retry attempt (for backoff)
|
|
17
|
+
* @param {object} options.emitter - Event emitter
|
|
18
|
+
* @param {object} options.eventBase - Base event fields
|
|
19
|
+
* @param {object} options.logger
|
|
20
|
+
* @param {object} options.session
|
|
21
|
+
*/
|
|
22
|
+
export async function waitForCooldown({ cooldownMs, cooldownUntil, agent, retryCount, emitter, eventBase, logger, session }) {
|
|
23
|
+
// Calculate wait time with exponential backoff for retries without known cooldown
|
|
24
|
+
let waitMs = cooldownMs || DEFAULT_COOLDOWN_MS;
|
|
25
|
+
if (!cooldownMs && retryCount > 0) {
|
|
26
|
+
waitMs = Math.min(DEFAULT_COOLDOWN_MS * Math.pow(2, retryCount), MAX_COOLDOWN_MS);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resumeAt = cooldownUntil || new Date(Date.now() + waitMs).toISOString();
|
|
30
|
+
|
|
31
|
+
logger.info(`Standby: waiting ${Math.round(waitMs / 1000)}s for ${agent} rate limit (retry ${retryCount + 1}/${MAX_STANDBY_RETRIES})`);
|
|
32
|
+
|
|
33
|
+
// Emit standby start event
|
|
34
|
+
emitProgress(emitter, makeEvent("coder:standby", { ...eventBase, stage: "standby" }, {
|
|
35
|
+
message: `Rate limited — standby until ${resumeAt} (attempt ${retryCount + 1}/${MAX_STANDBY_RETRIES})`,
|
|
36
|
+
detail: { agent, cooldownUntil: resumeAt, cooldownMs: waitMs, retryCount: retryCount + 1, maxRetries: MAX_STANDBY_RETRIES }
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Update session status
|
|
40
|
+
session.status = "standby";
|
|
41
|
+
|
|
42
|
+
// Wait with periodic heartbeats
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
const endTime = startTime + waitMs;
|
|
45
|
+
|
|
46
|
+
while (Date.now() < endTime) {
|
|
47
|
+
const remaining = endTime - Date.now();
|
|
48
|
+
const sleepTime = Math.min(HEARTBEAT_INTERVAL_MS, remaining);
|
|
49
|
+
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, sleepTime));
|
|
51
|
+
|
|
52
|
+
if (Date.now() < endTime) {
|
|
53
|
+
const remainingSec = Math.round((endTime - Date.now()) / 1000);
|
|
54
|
+
emitProgress(emitter, makeEvent("coder:standby_heartbeat", { ...eventBase, stage: "standby" }, {
|
|
55
|
+
message: `Standby: ${remainingSec}s remaining`,
|
|
56
|
+
detail: { agent, remainingMs: endTime - Date.now(), retryCount: retryCount + 1 }
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Emit resume event
|
|
62
|
+
emitProgress(emitter, makeEvent("coder:standby_resume", { ...eventBase, stage: "standby" }, {
|
|
63
|
+
message: `Cooldown expired — resuming with ${agent}`,
|
|
64
|
+
detail: { agent, retryCount: retryCount + 1 }
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
session.status = "running";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { DEFAULT_COOLDOWN_MS, MAX_COOLDOWN_MS, MAX_STANDBY_RETRIES, HEARTBEAT_INTERVAL_MS };
|
package/src/orchestrator.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createSession,
|
|
4
4
|
loadSession,
|
|
5
5
|
markSessionStatus,
|
|
6
|
+
pauseSession,
|
|
6
7
|
resumeSessionWithAnswer,
|
|
7
8
|
saveSession,
|
|
8
9
|
addCheckpoint
|
|
@@ -25,6 +26,7 @@ import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
|
25
26
|
import { runTriageStage, runResearcherStage, runPlannerStage } from "./orchestrator/pre-loop-stages.js";
|
|
26
27
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
27
28
|
import { runTesterStage, runSecurityStage } from "./orchestrator/post-loop-stages.js";
|
|
29
|
+
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
|
|
@@ -146,6 +148,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
146
148
|
repeated_issue_count: 0,
|
|
147
149
|
sonar_retry_count: 0,
|
|
148
150
|
reviewer_retry_count: 0,
|
|
151
|
+
standby_retry_count: 0,
|
|
149
152
|
last_sonar_issue_signature: null,
|
|
150
153
|
sonar_repeat_count: 0,
|
|
151
154
|
last_reviewer_issue_signature: null,
|
|
@@ -406,6 +409,26 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
406
409
|
if (coderResult?.action === "pause") {
|
|
407
410
|
return coderResult.result;
|
|
408
411
|
}
|
|
412
|
+
if (coderResult?.action === "standby") {
|
|
413
|
+
const standbyRetries = session.standby_retry_count || 0;
|
|
414
|
+
if (standbyRetries >= MAX_STANDBY_RETRIES) {
|
|
415
|
+
await pauseSession(session, {
|
|
416
|
+
question: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${coderResult.standbyInfo.agent}`,
|
|
417
|
+
context: { iteration: i, stage: "coder", reason: "standby_exhausted" }
|
|
418
|
+
});
|
|
419
|
+
emitProgress(emitter, makeEvent("coder:rate_limit", { ...eventBase, stage: "coder" }, {
|
|
420
|
+
status: "paused",
|
|
421
|
+
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
422
|
+
detail: { agent: coderResult.standbyInfo.agent, sessionId: session.id }
|
|
423
|
+
}));
|
|
424
|
+
return { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" };
|
|
425
|
+
}
|
|
426
|
+
session.standby_retry_count = standbyRetries + 1;
|
|
427
|
+
await saveSession(session);
|
|
428
|
+
await waitForCooldown({ ...coderResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
429
|
+
i -= 1; // Retry the same iteration
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
409
432
|
|
|
410
433
|
// --- Refactorer ---
|
|
411
434
|
if (refactorerEnabled) {
|
|
@@ -413,6 +436,26 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
413
436
|
if (refResult?.action === "pause") {
|
|
414
437
|
return refResult.result;
|
|
415
438
|
}
|
|
439
|
+
if (refResult?.action === "standby") {
|
|
440
|
+
const standbyRetries = session.standby_retry_count || 0;
|
|
441
|
+
if (standbyRetries >= MAX_STANDBY_RETRIES) {
|
|
442
|
+
await pauseSession(session, {
|
|
443
|
+
question: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${refResult.standbyInfo.agent}`,
|
|
444
|
+
context: { iteration: i, stage: "refactorer", reason: "standby_exhausted" }
|
|
445
|
+
});
|
|
446
|
+
emitProgress(emitter, makeEvent("refactorer:rate_limit", { ...eventBase, stage: "refactorer" }, {
|
|
447
|
+
status: "paused",
|
|
448
|
+
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
449
|
+
detail: { agent: refResult.standbyInfo.agent, sessionId: session.id }
|
|
450
|
+
}));
|
|
451
|
+
return { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" };
|
|
452
|
+
}
|
|
453
|
+
session.standby_retry_count = standbyRetries + 1;
|
|
454
|
+
await saveSession(session);
|
|
455
|
+
await waitForCooldown({ ...refResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
456
|
+
i -= 1; // Retry the same iteration
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
416
459
|
}
|
|
417
460
|
|
|
418
461
|
// --- TDD Policy ---
|
|
@@ -458,6 +501,26 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
458
501
|
if (reviewerResult.action === "pause") {
|
|
459
502
|
return reviewerResult.result;
|
|
460
503
|
}
|
|
504
|
+
if (reviewerResult.action === "standby") {
|
|
505
|
+
const standbyRetries = session.standby_retry_count || 0;
|
|
506
|
+
if (standbyRetries >= MAX_STANDBY_RETRIES) {
|
|
507
|
+
await pauseSession(session, {
|
|
508
|
+
question: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${reviewerResult.standbyInfo.agent}`,
|
|
509
|
+
context: { iteration: i, stage: "reviewer", reason: "standby_exhausted" }
|
|
510
|
+
});
|
|
511
|
+
emitProgress(emitter, makeEvent("reviewer:rate_limit", { ...eventBase, stage: "reviewer" }, {
|
|
512
|
+
status: "paused",
|
|
513
|
+
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
514
|
+
detail: { agent: reviewerResult.standbyInfo.agent, sessionId: session.id }
|
|
515
|
+
}));
|
|
516
|
+
return { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" };
|
|
517
|
+
}
|
|
518
|
+
session.standby_retry_count = standbyRetries + 1;
|
|
519
|
+
await saveSession(session);
|
|
520
|
+
await waitForCooldown({ ...reviewerResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
521
|
+
i -= 1; // Retry the same iteration
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
461
524
|
review = reviewerResult.review;
|
|
462
525
|
if (reviewerResult.stalled) {
|
|
463
526
|
return reviewerResult.stalledResult;
|
|
@@ -474,6 +537,49 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
474
537
|
})
|
|
475
538
|
);
|
|
476
539
|
|
|
540
|
+
// Reset standby counter after successful iteration
|
|
541
|
+
session.standby_retry_count = 0;
|
|
542
|
+
|
|
543
|
+
// --- Solomon supervisor: anomaly detection after each iteration ---
|
|
544
|
+
if (config.pipeline?.solomon?.enabled !== false) {
|
|
545
|
+
try {
|
|
546
|
+
const { evaluateRules, buildRulesContext } = await import("./orchestrator/solomon-rules.js");
|
|
547
|
+
const rulesContext = await buildRulesContext({ session, task, iteration: i });
|
|
548
|
+
const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
|
|
549
|
+
|
|
550
|
+
if (rulesResult.alerts.length > 0) {
|
|
551
|
+
for (const alert of rulesResult.alerts) {
|
|
552
|
+
emitProgress(emitter, makeEvent("solomon:alert", { ...eventBase, stage: "solomon" }, {
|
|
553
|
+
status: alert.severity === "critical" ? "fail" : "warn",
|
|
554
|
+
message: alert.message,
|
|
555
|
+
detail: alert.detail
|
|
556
|
+
}));
|
|
557
|
+
logger.warn(`Solomon alert [${alert.rule}]: ${alert.message}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (rulesResult.hasCritical && askQuestion) {
|
|
561
|
+
const alertSummary = rulesResult.alerts
|
|
562
|
+
.filter(a => a.severity === "critical")
|
|
563
|
+
.map(a => a.message)
|
|
564
|
+
.join("\n");
|
|
565
|
+
const answer = await askQuestion(
|
|
566
|
+
`Solomon detected critical issues:\n${alertSummary}\n\nShould I continue, pause, or revert?`,
|
|
567
|
+
{ iteration: i, stage: "solomon" }
|
|
568
|
+
);
|
|
569
|
+
if (!answer || answer.toLowerCase().includes("pause") || answer.toLowerCase().includes("stop")) {
|
|
570
|
+
await pauseSession(session, {
|
|
571
|
+
question: `Solomon supervisor paused: ${alertSummary}`,
|
|
572
|
+
context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
|
|
573
|
+
});
|
|
574
|
+
return { paused: true, sessionId: session.id, reason: "solomon_alert" };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} catch (err) {
|
|
579
|
+
logger.warn(`Solomon rules evaluation failed: ${err.message}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
477
583
|
if (review.approved) {
|
|
478
584
|
session.reviewer_retry_count = 0;
|
|
479
585
|
|
|
@@ -650,6 +756,7 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
650
756
|
session.repeated_issue_count = 0;
|
|
651
757
|
session.sonar_retry_count = 0;
|
|
652
758
|
session.reviewer_retry_count = 0;
|
|
759
|
+
session.standby_retry_count = 0;
|
|
653
760
|
session.tester_retry_count = 0;
|
|
654
761
|
session.security_retry_count = 0;
|
|
655
762
|
session.last_sonar_issue_signature = null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const SUBAGENT_PREAMBLE = [
|
|
2
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
3
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
4
|
+
"Do NOT use any MCP tools. Focus only on task complexity triage."
|
|
5
|
+
].join(" ");
|
|
6
|
+
|
|
7
|
+
const ROLE_DESCRIPTIONS = [
|
|
8
|
+
{ role: "planner", description: "Generates an implementation plan before coding. Useful for complex multi-file tasks." },
|
|
9
|
+
{ role: "researcher", description: "Investigates the codebase for context before coding. Useful when understanding existing code is needed." },
|
|
10
|
+
{ role: "tester", description: "Runs dedicated testing pass after coding. Ensures tests exist and pass." },
|
|
11
|
+
{ role: "security", description: "Audits code for security vulnerabilities. Checks auth, input validation, injection risks." },
|
|
12
|
+
{ role: "refactorer", description: "Cleans up and refactors code after the main implementation." },
|
|
13
|
+
{ role: "reviewer", description: "Reviews the code diff for quality issues. Standard quality gate." }
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function buildTriagePrompt({ task, instructions, availableRoles }) {
|
|
17
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
18
|
+
|
|
19
|
+
if (instructions) {
|
|
20
|
+
sections.push(instructions);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const roles = availableRoles || ROLE_DESCRIPTIONS;
|
|
24
|
+
|
|
25
|
+
sections.push(
|
|
26
|
+
"You are a task triage agent for Karajan Code, a multi-agent coding orchestrator.",
|
|
27
|
+
"Analyze the following task and determine which pipeline roles should be activated."
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
sections.push(
|
|
31
|
+
"## Available Roles",
|
|
32
|
+
roles.map((r) => `- **${r.role}**: ${r.description}`).join("\n")
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
sections.push(
|
|
36
|
+
"## Decision Guidelines",
|
|
37
|
+
[
|
|
38
|
+
"- **planner**: Enable for complex tasks (multi-file, architectural changes, data model changes). Disable for simple fixes.",
|
|
39
|
+
"- **researcher**: Enable when the task needs codebase context, API understanding, or investigation. Disable for standalone new files.",
|
|
40
|
+
"- **tester**: Enable for any task with logic, APIs, components, services. Disable ONLY for pure documentation, comments, or CSS-only changes.",
|
|
41
|
+
"- **security**: Enable for authentication, APIs, user input handling, data access, external integrations. Disable for UI-only or doc changes.",
|
|
42
|
+
"- **refactorer**: Enable only when explicitly requested or when the task is a refactoring task.",
|
|
43
|
+
"- **reviewer**: Enable for most tasks as a quality gate. Disable only for trivial, single-line changes.",
|
|
44
|
+
"",
|
|
45
|
+
"Note: coder is ALWAYS active — you don't need to decide on it."
|
|
46
|
+
].join("\n")
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
sections.push(
|
|
50
|
+
"Classify the task complexity, recommend only the necessary pipeline roles, and assess whether the task should be decomposed into smaller subtasks.",
|
|
51
|
+
"Keep the reasoning short and practical.",
|
|
52
|
+
"Return a single valid JSON object and nothing else.",
|
|
53
|
+
'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security"],"reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
sections.push(`## Task\n${task}`);
|
|
57
|
+
|
|
58
|
+
return sections.join("\n\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { ROLE_DESCRIPTIONS };
|
package/src/roles/triage-role.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { BaseRole } from "./base-role.js";
|
|
2
2
|
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
-
|
|
4
|
-
const SUBAGENT_PREAMBLE = [
|
|
5
|
-
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
6
|
-
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
7
|
-
"Do NOT use any MCP tools. Focus only on task complexity triage."
|
|
8
|
-
].join(" ");
|
|
3
|
+
import { buildTriagePrompt } from "../prompts/triage.js";
|
|
9
4
|
|
|
10
5
|
const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
|
|
11
6
|
const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security"]);
|
|
@@ -18,25 +13,6 @@ function resolveProvider(config) {
|
|
|
18
13
|
);
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
function buildPrompt({ task, instructions }) {
|
|
22
|
-
const sections = [SUBAGENT_PREAMBLE];
|
|
23
|
-
|
|
24
|
-
if (instructions) {
|
|
25
|
-
sections.push(instructions);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
sections.push(
|
|
29
|
-
"Classify the task complexity, recommend only the necessary pipeline roles, and assess whether the task should be decomposed into smaller subtasks.",
|
|
30
|
-
"Keep the reasoning short and practical.",
|
|
31
|
-
"Return a single valid JSON object and nothing else.",
|
|
32
|
-
'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security"],"reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}'
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
sections.push(`## Task\n${task}`);
|
|
36
|
-
|
|
37
|
-
return sections.join("\n\n");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
16
|
function parseTriageOutput(raw) {
|
|
41
17
|
const text = raw?.trim() || "";
|
|
42
18
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
@@ -72,7 +48,7 @@ export class TriageRole extends BaseRole {
|
|
|
72
48
|
const provider = resolveProvider(this.config);
|
|
73
49
|
const agent = this._createAgent(provider, this.config, this.logger);
|
|
74
50
|
|
|
75
|
-
const prompt =
|
|
51
|
+
const prompt = buildTriagePrompt({ task, instructions: this.instructions });
|
|
76
52
|
const runArgs = { prompt, role: "triage" };
|
|
77
53
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
78
54
|
const result = await agent.runTask(runArgs);
|
package/src/utils/display.js
CHANGED
|
@@ -41,6 +41,9 @@ const ICONS = {
|
|
|
41
41
|
"solomon:start": "\u2696\ufe0f",
|
|
42
42
|
"solomon:end": "\u2696\ufe0f",
|
|
43
43
|
"solomon:escalate": "\u26a0\ufe0f",
|
|
44
|
+
"coder:standby": "\u23f3",
|
|
45
|
+
"coder:standby_heartbeat": "\u23f3",
|
|
46
|
+
"coder:standby_resume": "\u25b6\ufe0f",
|
|
44
47
|
"budget:update": "\ud83d\udcb8",
|
|
45
48
|
"iteration:end": "\u23f1\ufe0f",
|
|
46
49
|
"session:end": "\ud83c\udfc1",
|
|
@@ -245,6 +248,24 @@ export function printEvent(event) {
|
|
|
245
248
|
break;
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
case "coder:standby": {
|
|
252
|
+
const until = event.detail?.cooldownUntil || "?";
|
|
253
|
+
const attempt = event.detail?.retryCount || "?";
|
|
254
|
+
const maxRetries = event.detail?.maxRetries || "?";
|
|
255
|
+
console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Rate limited \u2014 standby until ${until} (attempt ${attempt}/${maxRetries})${ANSI.reset}`);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case "coder:standby_heartbeat": {
|
|
260
|
+
const remaining = event.detail?.remainingMs !== undefined ? Math.round(event.detail.remainingMs / 1000) : "?";
|
|
261
|
+
console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Standby: ${remaining}s remaining${ANSI.reset}`);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case "coder:standby_resume":
|
|
266
|
+
console.log(` \u251c\u2500 ${ANSI.green}${icon} Cooldown expired \u2014 resuming with ${event.detail?.coder || event.detail?.provider || "?"}${ANSI.reset}`);
|
|
267
|
+
break;
|
|
268
|
+
|
|
248
269
|
case "iteration:end":
|
|
249
270
|
console.log(` \u2514\u2500 ${icon} Duration: ${formatElapsed(event.detail?.duration)} ${elapsed}`);
|
|
250
271
|
break;
|