karajan-code 1.31.0 → 1.32.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 +4 -1
- package/package.json +1 -1
- package/src/agents/resolve-bin.js +1 -0
- package/src/orchestrator/post-loop-stages.js +143 -42
- package/src/orchestrator.js +67 -9
- package/src/utils/agent-detect.js +1 -1
- package/src/utils/budget.js +6 -1
- package/src/utils/display.js +10 -0
- package/src/utils/wizard.js +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/karajan-code-logo
|
|
2
|
+
<img src="docs/karajan-code-logo.svg" alt="Karajan Code" width="180">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Karajan Code</h1>
|
|
@@ -46,6 +46,7 @@ Use Karajan when you want:
|
|
|
46
46
|
- **Zero-config operation** — auto-detects test frameworks, starts SonarQube, simplifies pipeline for trivial tasks
|
|
47
47
|
- **Composable role architecture** — define agent behaviors as plain markdown files that travel with your project
|
|
48
48
|
- **Local-first** — your code, your keys, your machine, no data leaves unless you say so
|
|
49
|
+
- **Zero API costs** — Karajan uses AI agent CLIs (Claude Code, Codex, Gemini CLI), not APIs. You pay your existing subscription (Claude Pro, ChatGPT Plus), not per-token API fees. No surprise bills.
|
|
49
50
|
|
|
50
51
|
If Claude Code is a smart pair programmer, Karajan is the CI/CD pipeline for AI-assisted development. They work great together — Karajan is designed to be used as an MCP server inside Claude Code.
|
|
51
52
|
|
|
@@ -64,6 +65,8 @@ That's it. No Docker required (SonarQube uses Docker, but Karajan auto-manages i
|
|
|
64
65
|
kj run "Create a utility function that validates Spanish DNI numbers, with tests"
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
[**▶ Watch the full pipeline demo**](https://karajancode.com#demo) — HU certification, triage, architecture, TDD, SonarQube, code review, Solomon arbitration, security audit.
|
|
69
|
+
|
|
67
70
|
Karajan will:
|
|
68
71
|
1. Triage the task complexity and activate the right roles
|
|
69
72
|
2. Write tests first (TDD)
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TesterRole } from "../roles/tester-role.js";
|
|
2
2
|
import { SecurityRole } from "../roles/security-role.js";
|
|
3
3
|
import { ImpeccableRole } from "../roles/impeccable-role.js";
|
|
4
|
+
import { AuditRole } from "../roles/audit-role.js";
|
|
4
5
|
import { addCheckpoint, saveSession } from "../session-store.js";
|
|
5
6
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
6
7
|
import { invokeSolomon } from "./solomon-escalation.js";
|
|
@@ -134,36 +135,18 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
|
|
|
134
135
|
);
|
|
135
136
|
|
|
136
137
|
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" };
|
|
138
|
+
// Tester findings are advisory when reviewer already approved.
|
|
139
|
+
// Auto-continue with a warning — no human escalation needed.
|
|
140
|
+
logger.warn(`Tester failed (advisory): ${testerOutput.summary}`);
|
|
141
|
+
emitProgress(
|
|
142
|
+
emitter,
|
|
143
|
+
makeEvent("tester:auto-continue", { ...eventBase, stage: "tester" }, {
|
|
144
|
+
status: "warn",
|
|
145
|
+
message: `Tester issues are advisory (reviewer approved), continuing: ${testerOutput.summary}`,
|
|
146
|
+
detail: { summary: testerOutput.summary, auto_continued: true }
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
return { action: "ok", stageResult: { ok: false, summary: testerOutput.summary || "Tester issues (advisory)", auto_continued: true } };
|
|
167
150
|
}
|
|
168
151
|
|
|
169
152
|
session.tester_retry_count = 0;
|
|
@@ -212,36 +195,46 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
|
|
|
212
195
|
);
|
|
213
196
|
|
|
214
197
|
if (!securityOutput.ok) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
198
|
+
// Check if the security finding is critical (SQL injection, RCE, auth bypass, etc.)
|
|
199
|
+
const summary = (securityOutput.summary || "").toLowerCase();
|
|
200
|
+
const criticalPatterns = ["injection", "rce", "remote code", "auth bypass", "authentication bypass", "privilege escalation", "credentials exposed", "secret", "critical vulnerability"];
|
|
201
|
+
const isCritical = criticalPatterns.some((p) => summary.includes(p));
|
|
202
|
+
|
|
203
|
+
if (isCritical) {
|
|
204
|
+
// Critical security issue — escalate to Solomon/human
|
|
205
|
+
logger.warn(`Critical security finding — escalating: ${securityOutput.summary}`);
|
|
220
206
|
const solomonResult = await invokeSolomon({
|
|
221
207
|
config, logger, emitter, eventBase, stage: "security", askQuestion, session, iteration,
|
|
222
208
|
conflict: {
|
|
223
209
|
stage: "security",
|
|
224
210
|
task,
|
|
225
211
|
diff,
|
|
226
|
-
iterationCount:
|
|
227
|
-
maxIterations:
|
|
212
|
+
iterationCount: 1,
|
|
213
|
+
maxIterations: 1,
|
|
228
214
|
history: [{ agent: "security", feedback: securityOutput.summary }]
|
|
229
215
|
}
|
|
230
216
|
});
|
|
231
217
|
|
|
232
218
|
if (solomonResult.action === "pause") {
|
|
233
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "
|
|
219
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "security_critical" } };
|
|
234
220
|
}
|
|
235
221
|
if (solomonResult.action === "subtask") {
|
|
236
222
|
return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "security_subtask" } };
|
|
237
223
|
}
|
|
238
|
-
// Solomon approved — proceed
|
|
239
224
|
return { action: "ok" };
|
|
240
225
|
}
|
|
241
226
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
227
|
+
// Non-critical security findings are advisory when reviewer already approved.
|
|
228
|
+
logger.warn(`Security failed (advisory): ${securityOutput.summary}`);
|
|
229
|
+
emitProgress(
|
|
230
|
+
emitter,
|
|
231
|
+
makeEvent("security:auto-continue", { ...eventBase, stage: "security" }, {
|
|
232
|
+
status: "warn",
|
|
233
|
+
message: `Security issues are advisory (reviewer approved), continuing: ${securityOutput.summary}`,
|
|
234
|
+
detail: { summary: securityOutput.summary, auto_continued: true }
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
return { action: "ok", stageResult: { ok: false, summary: securityOutput.summary || "Security issues (advisory)", auto_continued: true } };
|
|
245
238
|
}
|
|
246
239
|
|
|
247
240
|
session.security_retry_count = 0;
|
|
@@ -298,5 +291,113 @@ export async function runImpeccableStage({ config, logger, emitter, eventBase, s
|
|
|
298
291
|
return { action: "ok", stageResult: { ok: impeccableOutput.ok, verdict, summary: impeccableOutput.summary || "No frontend design issues found" } };
|
|
299
292
|
}
|
|
300
293
|
|
|
294
|
+
export async function runFinalAuditStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff }) {
|
|
295
|
+
logger.setContext({ iteration, stage: "audit" });
|
|
296
|
+
emitProgress(
|
|
297
|
+
emitter,
|
|
298
|
+
makeEvent("audit:start", { ...eventBase, stage: "audit" }, {
|
|
299
|
+
message: "Final audit — verifying code quality"
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const auditStart = Date.now();
|
|
304
|
+
const { output: auditOutput, provider, attempts } = await runRoleWithFallback(
|
|
305
|
+
AuditRole,
|
|
306
|
+
{ roleName: "audit", config, logger, emitter, eventBase, task, iteration, diff }
|
|
307
|
+
);
|
|
308
|
+
const totalDuration = Date.now() - auditStart;
|
|
309
|
+
|
|
310
|
+
trackBudget({
|
|
311
|
+
role: "audit",
|
|
312
|
+
provider: provider || coderRole.provider,
|
|
313
|
+
model: config?.roles?.audit?.model || coderRole.model,
|
|
314
|
+
result: auditOutput,
|
|
315
|
+
duration_ms: totalDuration
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await addCheckpoint(session, {
|
|
319
|
+
stage: "audit",
|
|
320
|
+
iteration,
|
|
321
|
+
ok: auditOutput.ok,
|
|
322
|
+
provider: provider || coderRole.provider,
|
|
323
|
+
model: config?.roles?.audit?.model || coderRole.model || null,
|
|
324
|
+
attempts: attempts.length > 1 ? attempts : undefined
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (!auditOutput.ok) {
|
|
328
|
+
// Audit agent failed to run — treat as advisory, don't block pipeline
|
|
329
|
+
logger.warn(`Audit agent error (advisory): ${auditOutput.summary}`);
|
|
330
|
+
emitProgress(
|
|
331
|
+
emitter,
|
|
332
|
+
makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
|
|
333
|
+
status: "warn",
|
|
334
|
+
message: `Audit: agent error (advisory), continuing — ${auditOutput.summary}`
|
|
335
|
+
})
|
|
336
|
+
);
|
|
337
|
+
return { action: "ok", stageResult: { ok: false, summary: auditOutput.summary || "Audit agent error (advisory)", auto_continued: true } };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Parse findings from audit result
|
|
341
|
+
const result = auditOutput.result || {};
|
|
342
|
+
const summary = result.summary || {};
|
|
343
|
+
const overallHealth = summary.overallHealth || "fair";
|
|
344
|
+
const criticalCount = summary.critical || 0;
|
|
345
|
+
const highCount = summary.high || 0;
|
|
346
|
+
|
|
347
|
+
// Collect critical and high findings for feedback
|
|
348
|
+
const actionableFindings = [];
|
|
349
|
+
if (result.dimensions) {
|
|
350
|
+
for (const [dimName, dim] of Object.entries(result.dimensions)) {
|
|
351
|
+
for (const finding of (dim.findings || [])) {
|
|
352
|
+
if (finding.severity === "critical" || finding.severity === "high") {
|
|
353
|
+
actionableFindings.push({
|
|
354
|
+
dimension: dimName,
|
|
355
|
+
...finding
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const hasActionableIssues = (overallHealth === "poor" || overallHealth === "critical") && (criticalCount > 0 || highCount > 0);
|
|
363
|
+
|
|
364
|
+
if (hasActionableIssues) {
|
|
365
|
+
// Build feedback string for the coder
|
|
366
|
+
const feedbackLines = actionableFindings.map(f => {
|
|
367
|
+
const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "";
|
|
368
|
+
return `[${f.severity.toUpperCase()}] ${loc} ${f.description}${f.recommendation ? ` — Fix: ${f.recommendation}` : ""}`;
|
|
369
|
+
});
|
|
370
|
+
const feedback = `Audit found ${criticalCount + highCount} critical/high issue(s) that must be fixed:\n${feedbackLines.join("\n")}`;
|
|
371
|
+
|
|
372
|
+
logger.warn(`Audit: ${criticalCount + highCount} actionable issues found, sending back to coder`);
|
|
373
|
+
emitProgress(
|
|
374
|
+
emitter,
|
|
375
|
+
makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
|
|
376
|
+
status: "fail",
|
|
377
|
+
message: `Audit: ${criticalCount + highCount} issue(s) found, sending back to coder`
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return { action: "retry", feedback, stageResult: { ok: false, summary: auditOutput.summary || `${criticalCount + highCount} actionable issues` } };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Audit passed (good/fair or no critical/high findings)
|
|
385
|
+
const hasAdvisory = (summary.medium || 0) + (summary.low || 0) > 0;
|
|
386
|
+
const certifiedMsg = hasAdvisory
|
|
387
|
+
? `Audit: CERTIFIED (with ${(summary.medium || 0) + (summary.low || 0)} advisory warning(s))`
|
|
388
|
+
: "Audit: CERTIFIED";
|
|
389
|
+
|
|
390
|
+
logger.info(certifiedMsg);
|
|
391
|
+
emitProgress(
|
|
392
|
+
emitter,
|
|
393
|
+
makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
|
|
394
|
+
status: "ok",
|
|
395
|
+
message: certifiedMsg
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
return { action: "ok", stageResult: { ok: true, summary: certifiedMsg } };
|
|
400
|
+
}
|
|
401
|
+
|
|
301
402
|
// Exported for testing
|
|
302
403
|
export { buildFallbackChain, isAgentFailure, runRoleWithFallback };
|
package/src/orchestrator.js
CHANGED
|
@@ -32,7 +32,7 @@ import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
|
32
32
|
import { PipelineContext } from "./orchestrator/pipeline-context.js";
|
|
33
33
|
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
|
|
34
34
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
35
|
-
import { runTesterStage, runSecurityStage, runImpeccableStage } from "./orchestrator/post-loop-stages.js";
|
|
35
|
+
import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
|
|
36
36
|
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
|
|
37
37
|
import { detectTestFramework } from "./utils/project-detect.js";
|
|
38
38
|
import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
|
|
@@ -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 }) {
|
|
@@ -629,6 +669,22 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
629
669
|
}
|
|
630
670
|
}
|
|
631
671
|
|
|
672
|
+
// Final audit — last quality gate before declaring success
|
|
673
|
+
const auditResult = await runFinalAuditStage({
|
|
674
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
675
|
+
iteration: i, task, diff: postLoopDiff
|
|
676
|
+
});
|
|
677
|
+
if (auditResult.stageResult) {
|
|
678
|
+
stageResults.audit = auditResult.stageResult;
|
|
679
|
+
await tryBecariaComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
|
|
680
|
+
}
|
|
681
|
+
if (auditResult.action === "retry") {
|
|
682
|
+
// Audit found actionable issues — loop back to coder
|
|
683
|
+
session.last_reviewer_feedback = auditResult.feedback;
|
|
684
|
+
await saveSession(session);
|
|
685
|
+
return { action: "continue" };
|
|
686
|
+
}
|
|
687
|
+
|
|
632
688
|
return { action: "proceed" };
|
|
633
689
|
}
|
|
634
690
|
|
|
@@ -1159,6 +1215,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1159
1215
|
const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
|
|
1160
1216
|
let lastCheckpointAt = Date.now();
|
|
1161
1217
|
let checkpointDisabled = false;
|
|
1218
|
+
let lastCheckpointSnapshot = takeCheckpointSnapshot(ctx.session);
|
|
1162
1219
|
|
|
1163
1220
|
let i = 0;
|
|
1164
1221
|
while (i < ctx.config.max_iterations) {
|
|
@@ -1167,11 +1224,12 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1167
1224
|
|
|
1168
1225
|
const cpResult = await handleCheckpoint({
|
|
1169
1226
|
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
|
|
1227
|
+
i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
|
|
1171
1228
|
});
|
|
1172
1229
|
if (cpResult.action === "stop") return cpResult.result;
|
|
1173
1230
|
checkpointDisabled = cpResult.checkpointDisabled;
|
|
1174
1231
|
lastCheckpointAt = cpResult.lastCheckpointAt;
|
|
1232
|
+
if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
|
|
1175
1233
|
|
|
1176
1234
|
await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
|
|
1177
1235
|
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) => {
|