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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.31.0",
3
+ "version": "1.31.1",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -10,6 +10,7 @@ const SEARCH_DIRS = [
10
10
  path.join(os.homedir(), ".npm-global", "bin"),
11
11
  "/usr/local/bin",
12
12
  path.join(os.homedir(), ".local", "bin"),
13
+ path.join(os.homedir(), ".opencode", "bin"),
13
14
  ];
14
15
 
15
16
  function getNvmDirs() {
@@ -134,36 +134,18 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
134
134
  );
135
135
 
136
136
  if (!testerOutput.ok) {
137
- const maxTesterRetries = config.session?.max_tester_retries ?? 1;
138
- session.tester_retry_count = (session.tester_retry_count || 0) + 1;
139
- await saveSession(session);
140
-
141
- if (session.tester_retry_count >= maxTesterRetries) {
142
- const solomonResult = await invokeSolomon({
143
- config, logger, emitter, eventBase, stage: "tester", askQuestion, session, iteration,
144
- conflict: {
145
- stage: "tester",
146
- task,
147
- diff,
148
- iterationCount: session.tester_retry_count,
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
- const maxSecurityRetries = config.session?.max_security_retries ?? 1;
216
- session.security_retry_count = (session.security_retry_count || 0) + 1;
217
- await saveSession(session);
218
-
219
- if (session.security_retry_count >= maxSecurityRetries) {
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: session.security_retry_count,
227
- maxIterations: maxSecurityRetries,
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: "security_fail_fast" } };
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
- session.last_reviewer_feedback = `Security feedback: ${securityOutput.summary}`;
243
- await saveSession(session);
244
- return { action: "continue" };
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;
@@ -313,22 +313,60 @@ async function tryBecariaComment({ config, session, logger, agent, body }) {
313
313
  } catch { /* non-blocking */ }
314
314
  }
315
315
 
316
- async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary }) {
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 stagesCompleted = Object.keys(stageResults).join(", ") || "none";
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
- return parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config });
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 @anthropic-ai/gemini-code (or check Gemini CLI docs)" },
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
  ];
@@ -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
 
@@ -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}`);
@@ -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) => {