karajan-code 1.10.0 → 1.11.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/bin/kj-tail +70 -0
- package/package.json +2 -1
- package/src/cli.js +3 -2
- package/src/commands/agents.js +45 -21
- package/src/commands/run.js +3 -35
- 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 +157 -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
|
@@ -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,
|
|
@@ -157,6 +160,32 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
157
160
|
|
|
158
161
|
eventBase.sessionId = session.id;
|
|
159
162
|
|
|
163
|
+
// --- Planning Game: mark card as In Progress ---
|
|
164
|
+
let pgCard = null;
|
|
165
|
+
if (pgTaskId && pgProject && config.planning_game?.enabled !== false) {
|
|
166
|
+
try {
|
|
167
|
+
const { fetchCard, updateCard } = await import("./planning-game/client.js");
|
|
168
|
+
pgCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId });
|
|
169
|
+
if (pgCard && pgCard.status !== "In Progress") {
|
|
170
|
+
await updateCard({
|
|
171
|
+
projectId: pgProject,
|
|
172
|
+
cardId: pgTaskId,
|
|
173
|
+
firebaseId: pgCard.firebaseId,
|
|
174
|
+
updates: {
|
|
175
|
+
status: "In Progress",
|
|
176
|
+
startDate: new Date().toISOString(),
|
|
177
|
+
developer: "dev_016",
|
|
178
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
logger.info(`Planning Game: ${pgTaskId} → In Progress`);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
logger.warn(`Planning Game: could not update ${pgTaskId}: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
session.pg_card = pgCard || null;
|
|
188
|
+
|
|
160
189
|
emitProgress(
|
|
161
190
|
emitter,
|
|
162
191
|
makeEvent("session:start", eventBase, {
|
|
@@ -380,6 +409,26 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
380
409
|
if (coderResult?.action === "pause") {
|
|
381
410
|
return coderResult.result;
|
|
382
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
|
+
}
|
|
383
432
|
|
|
384
433
|
// --- Refactorer ---
|
|
385
434
|
if (refactorerEnabled) {
|
|
@@ -387,6 +436,26 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
387
436
|
if (refResult?.action === "pause") {
|
|
388
437
|
return refResult.result;
|
|
389
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
|
+
}
|
|
390
459
|
}
|
|
391
460
|
|
|
392
461
|
// --- TDD Policy ---
|
|
@@ -432,6 +501,26 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
432
501
|
if (reviewerResult.action === "pause") {
|
|
433
502
|
return reviewerResult.result;
|
|
434
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
|
+
}
|
|
435
524
|
review = reviewerResult.review;
|
|
436
525
|
if (reviewerResult.stalled) {
|
|
437
526
|
return reviewerResult.stalledResult;
|
|
@@ -448,6 +537,49 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
448
537
|
})
|
|
449
538
|
);
|
|
450
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
|
+
|
|
451
583
|
if (review.approved) {
|
|
452
584
|
session.reviewer_retry_count = 0;
|
|
453
585
|
|
|
@@ -493,6 +625,30 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
493
625
|
}
|
|
494
626
|
session.budget = budgetSummary();
|
|
495
627
|
await markSessionStatus(session, "approved");
|
|
628
|
+
|
|
629
|
+
// --- Planning Game: mark card as To Validate ---
|
|
630
|
+
if (pgCard && pgProject) {
|
|
631
|
+
try {
|
|
632
|
+
const { updateCard } = await import("./planning-game/client.js");
|
|
633
|
+
const { buildCompletionUpdates } = await import("./planning-game/adapter.js");
|
|
634
|
+
const pgUpdates = buildCompletionUpdates({
|
|
635
|
+
approved: true,
|
|
636
|
+
commits: gitResult?.commits || [],
|
|
637
|
+
startDate: session.pg_card?.startDate || session.created_at,
|
|
638
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
639
|
+
});
|
|
640
|
+
await updateCard({
|
|
641
|
+
projectId: pgProject,
|
|
642
|
+
cardId: session.pg_task_id,
|
|
643
|
+
firebaseId: pgCard.firebaseId,
|
|
644
|
+
updates: pgUpdates
|
|
645
|
+
});
|
|
646
|
+
logger.info(`Planning Game: ${session.pg_task_id} → To Validate`);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
logger.warn(`Planning Game: could not update ${session.pg_task_id} on completion: ${err.message}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
496
652
|
emitProgress(
|
|
497
653
|
emitter,
|
|
498
654
|
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
@@ -600,6 +756,7 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
600
756
|
session.repeated_issue_count = 0;
|
|
601
757
|
session.sonar_retry_count = 0;
|
|
602
758
|
session.reviewer_retry_count = 0;
|
|
759
|
+
session.standby_retry_count = 0;
|
|
603
760
|
session.tester_retry_count = 0;
|
|
604
761
|
session.security_retry_count = 0;
|
|
605
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;
|