karajan-code 2.17.0 → 2.18.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 +7 -1
- package/package.json +1 -1
- package/src/brain/agent-error-classifier.js +5 -1
- package/src/brain/standby-store.js +38 -1
- package/src/checks/binaries.js +56 -0
- package/src/checks/runner.js +4 -0
- package/src/cli/register-pipeline.js +21 -0
- package/src/commands/board.js +58 -2
- package/src/commands/doctor.js +17 -0
- package/src/commands/install-tools.js +211 -0
- package/src/commands/plan/generate.js +22 -0
- package/src/commands/run.js +5 -0
- package/src/config/loader.js +27 -3
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +21 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +56 -0
- package/src/orchestrator/flow-runner.js +5 -0
- package/src/orchestrator/stages/coder-stage.js +47 -4
- package/src/orchestrator/verification-gate.js +38 -9
- package/src/plan/plan-store.js +53 -9
- package/src/roles/agent-role.js +24 -1
- package/src/roles/triage-role.js +28 -9
- package/src/session/store.js +2 -1
- package/src/utils/atomic-write.js +43 -0
- package/src/utils/display/resume-hint.js +41 -0
- package/src/utils/install-hints.js +136 -0
- package/src/utils/process.js +18 -0
- package/src/utils/rate-limit-detector.js +22 -0
- package/src/utils/run-registry.js +2 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prints — as the LAST line of a stopped `kj run` / `kj plan` — the exact
|
|
3
|
+
* command to resume it. A halted run that never tells you how to restart
|
|
4
|
+
* it is the resilience gap this closes (resilience audit, Phase 1).
|
|
5
|
+
*
|
|
6
|
+
* Quota hibernation → `kj standby resume <id>`
|
|
7
|
+
* Solomon pause → `kj resume <id> --answer "..."`
|
|
8
|
+
* Stopped / failed → `kj resume <id>`
|
|
9
|
+
* Success → nothing.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} result - the flow / command result
|
|
12
|
+
* @param {object} [opts]
|
|
13
|
+
* @param {(line:string)=>void} [opts.print] - sink, default console.log
|
|
14
|
+
*/
|
|
15
|
+
export function printResumeHint(result, { print = console.log } = {}) {
|
|
16
|
+
if (!result || typeof result !== "object") return;
|
|
17
|
+
const id = result.sessionId || result.session?.id || null;
|
|
18
|
+
|
|
19
|
+
if (result.hibernated === true) {
|
|
20
|
+
print("");
|
|
21
|
+
print("⏸ Run hibernated — provider quota exhausted.");
|
|
22
|
+
print(` Resume when the quota resets: kj standby resume ${id || "<session-id>"}`);
|
|
23
|
+
if (!id) print(" (run `kj standby list` to find the id)");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (result.paused === true && id) {
|
|
28
|
+
print("");
|
|
29
|
+
print("⏸ Run paused — waiting for your input.");
|
|
30
|
+
print(` Resume: kj resume ${id} --answer "<your answer>"`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Stopped / failed / aborted but with a session on record → resumable.
|
|
35
|
+
// A successful run has approved === true and prints nothing.
|
|
36
|
+
if (id && result.approved !== true && result.paused !== true) {
|
|
37
|
+
print("");
|
|
38
|
+
print("✗ Run stopped before finishing.");
|
|
39
|
+
print(` Resume: kj resume ${id}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Platform-aware install hints for external tools that `kj audit` /
|
|
2
|
+
// `kj webperf` use (semgrep, osv-scanner, lighthouse).
|
|
3
|
+
//
|
|
4
|
+
// `kj doctor` calls these to render hint strings ("Install with:
|
|
5
|
+
// pipx install semgrep") that match what's actually available on the
|
|
6
|
+
// user's machine, rather than a generic "see docs" message. The same
|
|
7
|
+
// metadata drives `kj install-tools` (HU2) — it decides which package
|
|
8
|
+
// manager to invoke per tool.
|
|
9
|
+
//
|
|
10
|
+
// We probe each package manager once with `which`/`where`-style checks
|
|
11
|
+
// and cache the result for the lifetime of the process.
|
|
12
|
+
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {"pipx"|"brew"|"go"|"npm"|"pip"|"apt"|"dnf"|"choco"|"scoop"|"docker"} PackageManager
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @type {Record<PackageManager, boolean>|null} */
|
|
23
|
+
let cachedAvailability = null;
|
|
24
|
+
|
|
25
|
+
const PROBES = [
|
|
26
|
+
["pipx", ["--version"]],
|
|
27
|
+
["brew", ["--version"]],
|
|
28
|
+
["go", ["version"]],
|
|
29
|
+
["npm", ["--version"]],
|
|
30
|
+
["pip", ["--version"]],
|
|
31
|
+
["apt", ["--version"]],
|
|
32
|
+
["dnf", ["--version"]],
|
|
33
|
+
["choco", ["--version"]],
|
|
34
|
+
["scoop", ["--version"]],
|
|
35
|
+
["docker", ["--version"]],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
async function probeOne(bin, args) {
|
|
39
|
+
try {
|
|
40
|
+
await execFileAsync(bin, args, { timeout: 3000 });
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Detect which package managers are reachable on PATH. Cached on first
|
|
49
|
+
* call. Exported for tests; production callers go through getInstallHint.
|
|
50
|
+
*/
|
|
51
|
+
export async function detectPackageManagers() {
|
|
52
|
+
if (cachedAvailability) return cachedAvailability;
|
|
53
|
+
const entries = await Promise.all(
|
|
54
|
+
PROBES.map(async ([bin, args]) => [bin, await probeOne(bin, args)]),
|
|
55
|
+
);
|
|
56
|
+
cachedAvailability = Object.fromEntries(entries);
|
|
57
|
+
return cachedAvailability;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Test helper — reset the cache between unit cases. */
|
|
61
|
+
export function resetPackageManagerCache() {
|
|
62
|
+
cachedAvailability = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns the install command for a tool, preferring the highest-priority
|
|
67
|
+
* package manager available on the current system.
|
|
68
|
+
*
|
|
69
|
+
* The order encodes our recommendation when multiple managers can install
|
|
70
|
+
* the same tool. We prefer isolated installs (pipx, brew) over global
|
|
71
|
+
* `pip install` so we don't pollute the user's site-packages.
|
|
72
|
+
*
|
|
73
|
+
* @param {"semgrep"|"osv-scanner"|"lighthouse"|"docker"} tool
|
|
74
|
+
* @param {Record<PackageManager, boolean>} [available] — pre-computed availability; if omitted, detects.
|
|
75
|
+
* @returns {Promise<{ command: string|null, manager: PackageManager|null, manualUrl: string }>}
|
|
76
|
+
*/
|
|
77
|
+
export async function getInstallHint(tool, available = null) {
|
|
78
|
+
const avail = available || (await detectPackageManagers());
|
|
79
|
+
const candidates = INSTALL_CANDIDATES[tool] || [];
|
|
80
|
+
for (const { manager, command } of candidates) {
|
|
81
|
+
if (avail[manager]) return { command, manager, manualUrl: MANUAL_URLS[tool] };
|
|
82
|
+
}
|
|
83
|
+
// Nothing matched — fall back to the first candidate's command as a
|
|
84
|
+
// suggestion, so the user at least sees a working recipe.
|
|
85
|
+
const first = candidates[0];
|
|
86
|
+
return {
|
|
87
|
+
command: first ? first.command : null,
|
|
88
|
+
manager: null,
|
|
89
|
+
manualUrl: MANUAL_URLS[tool],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const INSTALL_CANDIDATES = {
|
|
94
|
+
semgrep: [
|
|
95
|
+
{ manager: "pipx", command: "pipx install semgrep" },
|
|
96
|
+
{ manager: "brew", command: "brew install semgrep" },
|
|
97
|
+
{ manager: "pip", command: "pip install semgrep" },
|
|
98
|
+
],
|
|
99
|
+
"osv-scanner": [
|
|
100
|
+
{ manager: "go", command: "go install github.com/google/osv-scanner@latest" },
|
|
101
|
+
{ manager: "brew", command: "brew install osv-scanner" },
|
|
102
|
+
],
|
|
103
|
+
lighthouse: [
|
|
104
|
+
{ manager: "npm", command: "npm install -g lighthouse" },
|
|
105
|
+
],
|
|
106
|
+
docker: [
|
|
107
|
+
// Docker Desktop / Engine cannot be installed by a package manager
|
|
108
|
+
// reliably across platforms — always point users to the official docs.
|
|
109
|
+
// Listing brew anyway as a hint for macOS users.
|
|
110
|
+
{ manager: "brew", command: "brew install --cask docker" },
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const MANUAL_URLS = {
|
|
115
|
+
semgrep: "https://semgrep.dev/docs/getting-started",
|
|
116
|
+
"osv-scanner": "https://google.github.io/osv-scanner/installation/",
|
|
117
|
+
lighthouse: "https://github.com/GoogleChrome/lighthouse#using-the-cli",
|
|
118
|
+
docker: "https://docs.docker.com/get-docker/",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Tools whose hint depends on the project stack. lighthouse is only
|
|
123
|
+
* relevant for frontend projects; suppressing the hint elsewhere keeps
|
|
124
|
+
* `kj doctor` output free of irrelevant noise.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} tool
|
|
127
|
+
* @param {Object} stack — shape returned by detectProjectStack()
|
|
128
|
+
*/
|
|
129
|
+
export function appliesToStack(tool, stack) {
|
|
130
|
+
if (tool === "lighthouse") {
|
|
131
|
+
if (!stack) return false;
|
|
132
|
+
return Boolean(stack.isFrontend || stack.isFullstack);
|
|
133
|
+
}
|
|
134
|
+
// semgrep, osv-scanner, docker, sonar — always relevant.
|
|
135
|
+
return true;
|
|
136
|
+
}
|
package/src/utils/process.js
CHANGED
|
@@ -152,6 +152,24 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
152
152
|
function enrichResult(result, stdoutAccum, _stderrAccum) {
|
|
153
153
|
if (result.timedOut) return result;
|
|
154
154
|
|
|
155
|
+
// execa with reject:false RESOLVES (does not throw) on a spawn
|
|
156
|
+
// failure — ENOENT (binary not in PATH), EACCES, etc. The result
|
|
157
|
+
// then carries exitCode null/undefined, an empty stderr, and the
|
|
158
|
+
// real cause only in `code` / `shortMessage`. Surface it: a missing
|
|
159
|
+
// agent CLI used to fail with a completely empty error message,
|
|
160
|
+
// leaving the user with "coder failed" and no idea what to install.
|
|
161
|
+
if (result.failed && result.exitCode == null && !result.signal) {
|
|
162
|
+
const reason = result.shortMessage || result.message
|
|
163
|
+
|| `spawn failed${result.code ? ` (${result.code})` : ""}`;
|
|
164
|
+
return {
|
|
165
|
+
...result,
|
|
166
|
+
exitCode: 1,
|
|
167
|
+
stdout: result.stdout || stdoutAccum,
|
|
168
|
+
stderr: result.stderr || reason,
|
|
169
|
+
spawnError: result.code || true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
155
173
|
const killed = result.killed || !!result.signal;
|
|
156
174
|
const signal = result.signal || null;
|
|
157
175
|
|
|
@@ -39,6 +39,26 @@ export function parseCooldown(message) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// 5. Claude Code session / weekly limit: "resets 10:10pm" / "resets 4am"
|
|
43
|
+
// (12-hour clock, no date). Resolve to the next wall-clock occurrence
|
|
44
|
+
// of that time in the machine's local timezone. Claude Code reports
|
|
45
|
+
// the reset in the user's local TZ and kj runs on the same machine,
|
|
46
|
+
// so the parenthesised "(Europe/Madrid)" hint is ignored on purpose.
|
|
47
|
+
const ampmMatch = /reset(?:s|ting)?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*([ap]m)\b/i.exec(
|
|
48
|
+
message
|
|
49
|
+
);
|
|
50
|
+
if (ampmMatch) {
|
|
51
|
+
let hour = Number.parseInt(ampmMatch[1], 10) % 12;
|
|
52
|
+
if (/pm/i.test(ampmMatch[3])) hour += 12;
|
|
53
|
+
const minute = ampmMatch[2] ? Number.parseInt(ampmMatch[2], 10) : 0;
|
|
54
|
+
if (hour < 24 && minute < 60) {
|
|
55
|
+
const target = new Date();
|
|
56
|
+
target.setHours(hour, minute, 0, 0);
|
|
57
|
+
if (target.getTime() <= Date.now()) target.setDate(target.getDate() + 1);
|
|
58
|
+
return { cooldownUntil: target.toISOString(), cooldownMs: target.getTime() - Date.now() };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
// 2. Relative seconds: "retry after 120 seconds" / "retry in 120s" / "Retry-After: 120"
|
|
43
63
|
const secMatch = /(?:retry[\s-]*after|retry\s+in|wait)\s*:?\s*(\d+)\s*(?:seconds?|secs?|s\b)/i.exec(
|
|
44
64
|
message
|
|
@@ -69,6 +89,8 @@ const RATE_LIMIT_PATTERNS = [
|
|
|
69
89
|
{ pattern: /usage limit/i, agent: "claude" },
|
|
70
90
|
{ pattern: /plan's usage limit/i, agent: "claude" },
|
|
71
91
|
{ pattern: /Claude Pro usage limit/i, agent: "claude" },
|
|
92
|
+
{ pattern: /session limit/i, agent: "claude" },
|
|
93
|
+
{ pattern: /weekly limit/i, agent: "claude" },
|
|
72
94
|
|
|
73
95
|
// OpenAI / Codex CLI
|
|
74
96
|
{ pattern: /exceeded your current quota/i, agent: "codex" },
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import os from "node:os";
|
|
23
|
+
import { writeJsonAtomicSync } from "./atomic-write.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Directorio raíz donde viven los registros de run.
|
|
@@ -54,7 +55,7 @@ export function registerRun(info = {}) {
|
|
|
54
55
|
source: info.source || "cli",
|
|
55
56
|
startedAt: new Date().toISOString(),
|
|
56
57
|
};
|
|
57
|
-
|
|
58
|
+
writeJsonAtomicSync(filePath, entry);
|
|
58
59
|
return runId;
|
|
59
60
|
} catch {
|
|
60
61
|
return null;
|