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
|
@@ -1,9 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Detects rate limit / usage cap messages from CLI agent output.
|
|
3
|
-
* Returns { isRateLimit, agent, message
|
|
4
|
-
* of which CLI triggered it (or "unknown").
|
|
3
|
+
* Returns { isRateLimit, agent, message, cooldownUntil, cooldownMs }
|
|
4
|
+
* where agent is the best guess of which CLI triggered it (or "unknown").
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Extracts cooldown timing from a rate limit message string.
|
|
9
|
+
* Returns { cooldownUntil, cooldownMs } where cooldownUntil is an ISO string
|
|
10
|
+
* and cooldownMs is milliseconds to wait, or both null if not found.
|
|
11
|
+
*/
|
|
12
|
+
export function parseCooldown(message) {
|
|
13
|
+
if (!message || typeof message !== "string") {
|
|
14
|
+
return { cooldownUntil: null, cooldownMs: null };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 1. ISO timestamp: "try again after 2026-03-07T15:30:00Z"
|
|
18
|
+
// Also: "resets at 2026-03-07T15:30:00Z"
|
|
19
|
+
const isoMatch = message.match(
|
|
20
|
+
/(?:after|at)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/i
|
|
21
|
+
);
|
|
22
|
+
if (isoMatch) {
|
|
23
|
+
const target = new Date(isoMatch[1]);
|
|
24
|
+
if (!isNaN(target.getTime())) {
|
|
25
|
+
const ms = Math.max(0, target.getTime() - Date.now());
|
|
26
|
+
return { cooldownUntil: target.toISOString(), cooldownMs: ms };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 4. Claude specific: "resets at 2026-03-07 15:30 UTC" (space-separated date/time)
|
|
31
|
+
const resetMatch = message.match(
|
|
32
|
+
/resets?\s+at\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s*UTC/i
|
|
33
|
+
);
|
|
34
|
+
if (resetMatch) {
|
|
35
|
+
const target = new Date(`${resetMatch[1]}T${resetMatch[2]}:00Z`);
|
|
36
|
+
if (!isNaN(target.getTime())) {
|
|
37
|
+
const ms = Math.max(0, target.getTime() - Date.now());
|
|
38
|
+
return { cooldownUntil: target.toISOString(), cooldownMs: ms };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Relative seconds: "retry after 120 seconds" / "retry in 120s" / "Retry-After: 120"
|
|
43
|
+
const secMatch = message.match(
|
|
44
|
+
/(?:retry[\s-]*after|retry\s+in|wait)\s*:?\s*(\d+)\s*(?:seconds?|secs?|s\b)/i
|
|
45
|
+
) || message.match(/Retry-After:\s*(\d+)/i);
|
|
46
|
+
if (secMatch) {
|
|
47
|
+
const seconds = parseInt(secMatch[1], 10);
|
|
48
|
+
const ms = seconds * 1000;
|
|
49
|
+
const target = new Date(Date.now() + ms);
|
|
50
|
+
return { cooldownUntil: target.toISOString(), cooldownMs: ms };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Relative minutes: "retry in 5 minutes" / "wait 5 min"
|
|
54
|
+
const minMatch = message.match(
|
|
55
|
+
/(?:retry\s+in|wait|after)\s+(\d+)\s*(?:minutes?|mins?)/i
|
|
56
|
+
);
|
|
57
|
+
if (minMatch) {
|
|
58
|
+
const minutes = parseInt(minMatch[1], 10);
|
|
59
|
+
const ms = minutes * 60 * 1000;
|
|
60
|
+
const target = new Date(Date.now() + ms);
|
|
61
|
+
return { cooldownUntil: target.toISOString(), cooldownMs: ms };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { cooldownUntil: null, cooldownMs: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
7
67
|
const RATE_LIMIT_PATTERNS = [
|
|
8
68
|
// Claude CLI
|
|
9
69
|
{ pattern: /usage limit/i, agent: "claude" },
|
|
@@ -34,10 +94,11 @@ export function detectRateLimit({ stderr = "", stdout = "" }) {
|
|
|
34
94
|
return {
|
|
35
95
|
isRateLimit: true,
|
|
36
96
|
agent,
|
|
37
|
-
message: matchedLine.trim()
|
|
97
|
+
message: matchedLine.trim(),
|
|
98
|
+
...parseCooldown(matchedLine)
|
|
38
99
|
};
|
|
39
100
|
}
|
|
40
101
|
}
|
|
41
102
|
|
|
42
|
-
return { isRateLimit: false, agent: "", message: "" };
|
|
103
|
+
return { isRateLimit: false, agent: "", message: "", cooldownUntil: null, cooldownMs: null };
|
|
43
104
|
}
|
package/src/utils/run-log.js
CHANGED
|
@@ -40,6 +40,9 @@ function formatLine(event) {
|
|
|
40
40
|
if (event.detail?.silenceMs !== undefined) extra.push(`silence=${Math.round(event.detail.silenceMs / 1000)}s`);
|
|
41
41
|
if (event.detail?.severity) extra.push(`severity=${event.detail.severity}`);
|
|
42
42
|
if (event.detail?.stream) extra.push(`stream=${event.detail.stream}`);
|
|
43
|
+
if (event.detail?.cooldownUntil) extra.push(`until=${event.detail.cooldownUntil}`);
|
|
44
|
+
if (event.detail?.retryCount !== undefined) extra.push(`retry=${event.detail.retryCount}/${event.detail.maxRetries || "?"}`);
|
|
45
|
+
if (event.detail?.remainingMs !== undefined) extra.push(`remaining=${Math.round(event.detail.remainingMs / 1000)}s`);
|
|
43
46
|
|
|
44
47
|
const extraStr = extra.length ? ` (${extra.join(", ")})` : "";
|
|
45
48
|
return `${ts} [${type}] ${stage ? `[${stage}] ` : ""}${msg}${extraStr}`;
|
|
@@ -96,9 +99,78 @@ export function createRunLog(projectDir) {
|
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Parse the run log to extract current status information.
|
|
104
|
+
*/
|
|
105
|
+
function parseRunStatus(lines) {
|
|
106
|
+
const status = {
|
|
107
|
+
currentStage: null,
|
|
108
|
+
currentAgent: null,
|
|
109
|
+
startedAt: null,
|
|
110
|
+
isRunning: false,
|
|
111
|
+
lastEvent: null,
|
|
112
|
+
iteration: null,
|
|
113
|
+
errors: []
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
// Detect run start
|
|
118
|
+
if (line.includes("[kj_run] started") || line.includes("[kj_code] started") || line.includes("[kj_plan] started")) {
|
|
119
|
+
status.isRunning = true;
|
|
120
|
+
const tool = line.includes("kj_run") ? "kj_run" : line.includes("kj_code") ? "kj_code" : "kj_plan";
|
|
121
|
+
status.currentStage = tool;
|
|
122
|
+
const tsMatch = line.match(/^(\d{2}:\d{2}:\d{2}\.\d{3})/);
|
|
123
|
+
if (tsMatch) status.startedAt = tsMatch[1];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Detect run finish
|
|
127
|
+
if (line.includes("[kj_run] finished") || line.includes("[kj_code] finished") || line.includes("[kj_plan] finished") || line.includes("finished")) {
|
|
128
|
+
if (line.includes("[kj_run]") || line.includes("[kj_code]") || line.includes("[kj_plan]")) {
|
|
129
|
+
status.isRunning = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Detect stage transitions
|
|
134
|
+
const stageStart = line.match(/\[(\w+):start\]/);
|
|
135
|
+
if (stageStart) {
|
|
136
|
+
status.currentStage = stageStart[1];
|
|
137
|
+
}
|
|
138
|
+
const stageDone = line.match(/\[(\w+):done\]|\[(\w+)\] finished/);
|
|
139
|
+
if (stageDone) {
|
|
140
|
+
const doneName = stageDone[1] || stageDone[2];
|
|
141
|
+
if (doneName === status.currentStage) status.currentStage = "idle";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Detect agent
|
|
145
|
+
const agentMatch = line.match(/agent=(\w+)/);
|
|
146
|
+
if (agentMatch) status.currentAgent = agentMatch[1];
|
|
147
|
+
|
|
148
|
+
// Detect iteration
|
|
149
|
+
const iterMatch = line.match(/[Ii]teration\s+(\d+)/);
|
|
150
|
+
if (iterMatch) status.iteration = parseInt(iterMatch[1], 10);
|
|
151
|
+
|
|
152
|
+
// Detect errors
|
|
153
|
+
if (line.match(/\[.*:fail\]|\[.*error\]/i) || line.includes("ERROR")) {
|
|
154
|
+
status.errors.push(line.trim());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Detect standby
|
|
158
|
+
if (line.includes("[standby]") || line.includes("standby")) {
|
|
159
|
+
status.currentStage = "standby";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
status.lastEvent = line.trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Keep only last 3 errors
|
|
166
|
+
if (status.errors.length > 3) status.errors = status.errors.slice(-3);
|
|
167
|
+
|
|
168
|
+
return status;
|
|
169
|
+
}
|
|
170
|
+
|
|
99
171
|
/**
|
|
100
172
|
* Read the current run log contents.
|
|
101
|
-
* Returns the last N lines (default 50).
|
|
173
|
+
* Returns the last N lines (default 50) plus a parsed status summary.
|
|
102
174
|
*/
|
|
103
175
|
export function readRunLog(maxLines = 50, projectDir) {
|
|
104
176
|
const logPath = resolveLogPath(projectDir);
|
|
@@ -107,10 +179,12 @@ export function readRunLog(maxLines = 50, projectDir) {
|
|
|
107
179
|
const lines = content.split("\n").filter(Boolean);
|
|
108
180
|
const total = lines.length;
|
|
109
181
|
const shown = lines.slice(-maxLines);
|
|
182
|
+
const status = parseRunStatus(lines);
|
|
110
183
|
return {
|
|
111
184
|
ok: true,
|
|
112
185
|
path: logPath,
|
|
113
186
|
totalLines: total,
|
|
187
|
+
status,
|
|
114
188
|
lines: shown,
|
|
115
189
|
summary: shown.join("\n")
|
|
116
190
|
};
|