karajan-code 1.31.0 → 1.31.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/package.json
CHANGED
|
@@ -134,36 +134,18 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
|
|
|
134
134
|
);
|
|
135
135
|
|
|
136
136
|
if (!testerOutput.ok) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
maxIterations: maxTesterRetries,
|
|
150
|
-
history: [{ agent: "tester", feedback: testerOutput.summary }]
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
if (solomonResult.action === "pause") {
|
|
155
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "tester_fail_fast" } };
|
|
156
|
-
}
|
|
157
|
-
if (solomonResult.action === "subtask") {
|
|
158
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "tester_subtask" } };
|
|
159
|
-
}
|
|
160
|
-
// Solomon approved — proceed to next stage
|
|
161
|
-
return { action: "ok" };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
session.last_reviewer_feedback = `Tester feedback: ${testerOutput.summary}`;
|
|
165
|
-
await saveSession(session);
|
|
166
|
-
return { action: "continue" };
|
|
137
|
+
// Tester findings are advisory when reviewer already approved.
|
|
138
|
+
// Auto-continue with a warning — no human escalation needed.
|
|
139
|
+
logger.warn(`Tester failed (advisory): ${testerOutput.summary}`);
|
|
140
|
+
emitProgress(
|
|
141
|
+
emitter,
|
|
142
|
+
makeEvent("tester:auto-continue", { ...eventBase, stage: "tester" }, {
|
|
143
|
+
status: "warn",
|
|
144
|
+
message: `Tester issues are advisory (reviewer approved), continuing: ${testerOutput.summary}`,
|
|
145
|
+
detail: { summary: testerOutput.summary, auto_continued: true }
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
return { action: "ok", stageResult: { ok: false, summary: testerOutput.summary || "Tester issues (advisory)", auto_continued: true } };
|
|
167
149
|
}
|
|
168
150
|
|
|
169
151
|
session.tester_retry_count = 0;
|
|
@@ -212,36 +194,46 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
|
|
|
212
194
|
);
|
|
213
195
|
|
|
214
196
|
if (!securityOutput.ok) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
197
|
+
// Check if the security finding is critical (SQL injection, RCE, auth bypass, etc.)
|
|
198
|
+
const summary = (securityOutput.summary || "").toLowerCase();
|
|
199
|
+
const criticalPatterns = ["injection", "rce", "remote code", "auth bypass", "authentication bypass", "privilege escalation", "credentials exposed", "secret", "critical vulnerability"];
|
|
200
|
+
const isCritical = criticalPatterns.some((p) => summary.includes(p));
|
|
201
|
+
|
|
202
|
+
if (isCritical) {
|
|
203
|
+
// Critical security issue — escalate to Solomon/human
|
|
204
|
+
logger.warn(`Critical security finding — escalating: ${securityOutput.summary}`);
|
|
220
205
|
const solomonResult = await invokeSolomon({
|
|
221
206
|
config, logger, emitter, eventBase, stage: "security", askQuestion, session, iteration,
|
|
222
207
|
conflict: {
|
|
223
208
|
stage: "security",
|
|
224
209
|
task,
|
|
225
210
|
diff,
|
|
226
|
-
iterationCount:
|
|
227
|
-
maxIterations:
|
|
211
|
+
iterationCount: 1,
|
|
212
|
+
maxIterations: 1,
|
|
228
213
|
history: [{ agent: "security", feedback: securityOutput.summary }]
|
|
229
214
|
}
|
|
230
215
|
});
|
|
231
216
|
|
|
232
217
|
if (solomonResult.action === "pause") {
|
|
233
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "
|
|
218
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "security_critical" } };
|
|
234
219
|
}
|
|
235
220
|
if (solomonResult.action === "subtask") {
|
|
236
221
|
return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "security_subtask" } };
|
|
237
222
|
}
|
|
238
|
-
// Solomon approved — proceed
|
|
239
223
|
return { action: "ok" };
|
|
240
224
|
}
|
|
241
225
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
226
|
+
// Non-critical security findings are advisory when reviewer already approved.
|
|
227
|
+
logger.warn(`Security failed (advisory): ${securityOutput.summary}`);
|
|
228
|
+
emitProgress(
|
|
229
|
+
emitter,
|
|
230
|
+
makeEvent("security:auto-continue", { ...eventBase, stage: "security" }, {
|
|
231
|
+
status: "warn",
|
|
232
|
+
message: `Security issues are advisory (reviewer approved), continuing: ${securityOutput.summary}`,
|
|
233
|
+
detail: { summary: securityOutput.summary, auto_continued: true }
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
return { action: "ok", stageResult: { ok: false, summary: securityOutput.summary || "Security issues (advisory)", auto_continued: true } };
|
|
245
237
|
}
|
|
246
238
|
|
|
247
239
|
session.security_retry_count = 0;
|
package/src/orchestrator.js
CHANGED
|
@@ -313,22 +313,60 @@ async function tryBecariaComment({ config, session, logger, agent, body }) {
|
|
|
313
313
|
} catch { /* non-blocking */ }
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
function detectCheckpointProgress(session, lastCheckpointSnapshot) {
|
|
317
|
+
if (!lastCheckpointSnapshot) return true; // First checkpoint — assume progress
|
|
318
|
+
const currentIteration = session.reviewer_retry_count ?? 0;
|
|
319
|
+
const currentStages = Object.keys(session.resolved_policies || {}).length;
|
|
320
|
+
const currentCheckpoints = (session.checkpoints || []).length;
|
|
321
|
+
|
|
322
|
+
const iterationAdvanced = currentIteration !== lastCheckpointSnapshot.iteration;
|
|
323
|
+
const stagesChanged = currentStages !== lastCheckpointSnapshot.stagesCount;
|
|
324
|
+
const checkpointsChanged = currentCheckpoints !== lastCheckpointSnapshot.checkpointsCount;
|
|
325
|
+
|
|
326
|
+
return iterationAdvanced || stagesChanged || checkpointsChanged;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function takeCheckpointSnapshot(session) {
|
|
330
|
+
return {
|
|
331
|
+
iteration: session.reviewer_retry_count ?? 0,
|
|
332
|
+
stagesCount: Object.keys(session.resolved_policies || {}).length,
|
|
333
|
+
checkpointsCount: (session.checkpoints || []).length
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary, lastCheckpointSnapshot }) {
|
|
317
338
|
if (checkpointDisabled || !askQuestion || (Date.now() - lastCheckpointAt) < checkpointIntervalMs) {
|
|
318
|
-
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt };
|
|
339
|
+
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt, lastCheckpointSnapshot };
|
|
319
340
|
}
|
|
320
341
|
|
|
321
342
|
const elapsedStr = elapsedMinutes.toFixed(1);
|
|
343
|
+
const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
|
|
344
|
+
|
|
345
|
+
// Auto-continue if progress detected since last checkpoint
|
|
346
|
+
const hasProgress = detectCheckpointProgress(session, lastCheckpointSnapshot);
|
|
347
|
+
const newSnapshot = takeCheckpointSnapshot(session);
|
|
348
|
+
|
|
349
|
+
if (hasProgress) {
|
|
350
|
+
emitProgress(
|
|
351
|
+
emitter,
|
|
352
|
+
makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
|
|
353
|
+
message: `Checkpoint: progress detected, continuing (${elapsedStr} min elapsed)`,
|
|
354
|
+
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true }
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now(), lastCheckpointSnapshot: newSnapshot };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// No progress — ask human
|
|
322
361
|
const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
|
|
323
362
|
const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
|
|
324
|
-
const
|
|
325
|
-
const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. What would you like to do?`;
|
|
363
|
+
const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. No progress since last checkpoint. What would you like to do?`;
|
|
326
364
|
|
|
327
365
|
emitProgress(
|
|
328
366
|
emitter,
|
|
329
367
|
makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
|
|
330
|
-
message: `Interactive checkpoint at ${elapsedStr} min`,
|
|
331
|
-
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted }
|
|
368
|
+
message: `Interactive checkpoint at ${elapsedStr} min (stalled)`,
|
|
369
|
+
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: false }
|
|
332
370
|
})
|
|
333
371
|
);
|
|
334
372
|
|
|
@@ -354,7 +392,9 @@ async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpoin
|
|
|
354
392
|
return { action: "stop", result: { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) } };
|
|
355
393
|
}
|
|
356
394
|
|
|
357
|
-
|
|
395
|
+
const parsed = parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config });
|
|
396
|
+
parsed.lastCheckpointSnapshot = newSnapshot;
|
|
397
|
+
return parsed;
|
|
358
398
|
}
|
|
359
399
|
|
|
360
400
|
function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
|
|
@@ -1159,6 +1199,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1159
1199
|
const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
|
|
1160
1200
|
let lastCheckpointAt = Date.now();
|
|
1161
1201
|
let checkpointDisabled = false;
|
|
1202
|
+
let lastCheckpointSnapshot = null;
|
|
1162
1203
|
|
|
1163
1204
|
let i = 0;
|
|
1164
1205
|
while (i < ctx.config.max_iterations) {
|
|
@@ -1167,11 +1208,12 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1167
1208
|
|
|
1168
1209
|
const cpResult = await handleCheckpoint({
|
|
1169
1210
|
checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
|
|
1170
|
-
i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary
|
|
1211
|
+
i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
|
|
1171
1212
|
});
|
|
1172
1213
|
if (cpResult.action === "stop") return cpResult.result;
|
|
1173
1214
|
checkpointDisabled = cpResult.checkpointDisabled;
|
|
1174
1215
|
lastCheckpointAt = cpResult.lastCheckpointAt;
|
|
1216
|
+
if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
|
|
1175
1217
|
|
|
1176
1218
|
await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
|
|
1177
1219
|
await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
|
|
@@ -4,7 +4,7 @@ import { resolveBin } from "../agents/resolve-bin.js";
|
|
|
4
4
|
const KNOWN_AGENTS = [
|
|
5
5
|
{ name: "claude", install: "npm install -g @anthropic-ai/claude-code" },
|
|
6
6
|
{ name: "codex", install: "npm install -g @openai/codex" },
|
|
7
|
-
{ name: "gemini", install: "npm install -g @
|
|
7
|
+
{ name: "gemini", install: "npm install -g @google/gemini-cli (or check https://geminicli.com/docs/get-started/installation/)" },
|
|
8
8
|
{ name: "aider", install: "pip install aider-chat" },
|
|
9
9
|
{ name: "opencode", install: "curl -fsSL https://opencode.ai/install | bash (or see https://opencode.ai)" }
|
|
10
10
|
];
|
package/src/utils/budget.js
CHANGED
|
@@ -121,6 +121,10 @@ export class BudgetTracker {
|
|
|
121
121
|
return this.total().cost_usd > n;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
hasUsageData() {
|
|
125
|
+
return this.entries.length > 0 && (this.total().tokens_in > 0 || this.total().tokens_out > 0 || this.total().cost_usd > 0);
|
|
126
|
+
}
|
|
127
|
+
|
|
124
128
|
summary() {
|
|
125
129
|
const totals = this.total();
|
|
126
130
|
const byRole = {};
|
|
@@ -133,7 +137,8 @@ export class BudgetTracker {
|
|
|
133
137
|
total_tokens: totals.tokens_in + totals.tokens_out,
|
|
134
138
|
total_cost_usd: totals.cost_usd,
|
|
135
139
|
breakdown_by_role: byRole,
|
|
136
|
-
entries: [...this.entries]
|
|
140
|
+
entries: [...this.entries],
|
|
141
|
+
usage_available: this.hasUsageData()
|
|
137
142
|
};
|
|
138
143
|
}
|
|
139
144
|
|
package/src/utils/display.js
CHANGED
|
@@ -221,6 +221,10 @@ function printSessionGit(git) {
|
|
|
221
221
|
|
|
222
222
|
function printSessionBudget(budget) {
|
|
223
223
|
if (!budget) return;
|
|
224
|
+
if (budget.usage_available === false || (budget.total_tokens === 0 && budget.total_cost_usd === 0 && Object.keys(budget.breakdown_by_role || {}).length > 0)) {
|
|
225
|
+
console.log(` ${ANSI.dim}\ud83d\udcb0 Budget: N/A (provider does not report usage)${ANSI.reset}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
224
228
|
console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${budget.total_tokens ?? 0}${ANSI.reset}`);
|
|
225
229
|
console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: $${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
|
|
226
230
|
for (const [role, metrics] of Object.entries(budget.breakdown_by_role || {})) {
|
|
@@ -376,9 +380,15 @@ const EVENT_HANDLERS = {
|
|
|
376
380
|
|
|
377
381
|
"budget:update": (event, icon) => {
|
|
378
382
|
const total = Number(event.detail?.total_cost_usd || 0);
|
|
383
|
+
const totalTokens = Number(event.detail?.total_tokens || 0);
|
|
379
384
|
const max = Number(event.detail?.max_budget_usd);
|
|
380
385
|
const pct = Number(event.detail?.pct_used ?? 0);
|
|
381
386
|
const warn = Number(event.detail?.warn_threshold_pct ?? 80);
|
|
387
|
+
const hasEntries = (event.detail?.entries?.length ?? 0) > 0 || Object.keys(event.detail?.breakdown_by_role || {}).length > 0;
|
|
388
|
+
if (hasEntries && totalTokens === 0 && total === 0) {
|
|
389
|
+
console.log(` \u251c\u2500 ${icon} Budget: ${ANSI.dim}N/A (provider does not report usage)${ANSI.reset}`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
382
392
|
const color = budgetColor(max, pct, warn);
|
|
383
393
|
if (Number.isFinite(max) && max >= 0) {
|
|
384
394
|
console.log(` \u251c\u2500 ${icon} Budget: ${color}$${total.toFixed(2)} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
|
package/src/utils/wizard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
|
|
3
3
|
export function createWizard(input = process.stdin, output = process.stdout) {
|
|
4
|
-
const rl = readline.createInterface({ input, output });
|
|
4
|
+
const rl = readline.createInterface({ input, output, terminal: false });
|
|
5
5
|
|
|
6
6
|
function ask(question) {
|
|
7
7
|
return new Promise((resolve) => {
|