karajan-code 1.59.1 → 2.0.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.
Files changed (112) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +1 -4
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +2 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/git/automation.js +9 -9
  21. package/src/guards/output-guard.js +0 -17
  22. package/src/mcp/handlers/direct-handlers.js +1 -1
  23. package/src/mcp/handlers/run-handler.js +1 -2
  24. package/src/mcp/sovereignty-guard.js +1 -1
  25. package/src/mcp/tools.js +1 -1
  26. package/src/orchestrator/brain-coordinator.js +165 -0
  27. package/src/orchestrator/ci-integration.js +139 -0
  28. package/src/orchestrator/config-init.js +435 -0
  29. package/src/orchestrator/direct-actions.js +186 -0
  30. package/src/orchestrator/feedback-enrichment.js +140 -0
  31. package/src/orchestrator/feedback-queue.js +152 -0
  32. package/src/orchestrator/flow-control.js +159 -0
  33. package/src/orchestrator/post-loop-stages.js +20 -15
  34. package/src/orchestrator/preflight-checks.js +7 -3
  35. package/src/orchestrator/role-output-compressor.js +190 -0
  36. package/src/orchestrator/session-journal.js +344 -0
  37. package/src/orchestrator/solomon-escalation.js +38 -5
  38. package/src/orchestrator/solomon-rules.js +32 -33
  39. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  40. package/src/orchestrator/stages/sonar-stage.js +24 -1
  41. package/src/orchestrator/verification-gate.js +146 -0
  42. package/src/orchestrator.js +428 -664
  43. package/src/planning-game/client.js +2 -2
  44. package/src/planning-game/pipeline-adapter.js +1 -1
  45. package/src/prompts/coder.js +17 -3
  46. package/src/prompts/reviewer.js +2 -2
  47. package/src/prompts/rtk-snippet.js +3 -5
  48. package/src/review/diff-generator.js +66 -17
  49. package/src/review/snapshot-diff.js +122 -0
  50. package/src/review/tdd-policy.js +4 -2
  51. package/src/roles/agent-role.js +135 -0
  52. package/src/roles/architect-role.js +61 -108
  53. package/src/roles/audit-role.js +25 -71
  54. package/src/roles/coder-role.js +32 -57
  55. package/src/roles/discover-role.js +30 -71
  56. package/src/roles/hu-reviewer-role.js +51 -99
  57. package/src/roles/impeccable-role.js +33 -88
  58. package/src/roles/index.js +2 -0
  59. package/src/roles/karajan-brain-role.js +134 -0
  60. package/src/roles/planner-role.js +40 -75
  61. package/src/roles/refactorer-role.js +22 -64
  62. package/src/roles/researcher-role.js +51 -115
  63. package/src/roles/reviewer-role.js +68 -125
  64. package/src/roles/security-role.js +38 -102
  65. package/src/roles/solomon-role.js +69 -11
  66. package/src/roles/tester-role.js +121 -95
  67. package/src/roles/triage-role.js +50 -111
  68. package/src/session-store.js +1 -1
  69. package/src/skills/skill-detector.js +11 -0
  70. package/src/sonar/config-resolver.js +20 -1
  71. package/src/sonar/credentials.js +17 -0
  72. package/src/sonar/project-key.js +1 -1
  73. package/src/sonar/scanner.js +1 -1
  74. package/src/utils/display.js +33 -14
  75. package/src/utils/injection-guard.js +23 -8
  76. package/src/utils/logger.js +6 -1
  77. package/src/utils/process.js +3 -4
  78. package/src/utils/role-assigner.js +131 -0
  79. package/src/utils/update-check.js +5 -1
  80. package/src/webperf/devtools-detect.js +70 -23
  81. package/templates/roles/coder.md +7 -0
  82. package/templates/roles/karajan-brain.md +95 -0
  83. package/templates/roles/solomon.md +67 -106
  84. package/templates/workflows/automerge.yml +4 -4
  85. package/templates/workflows/houston-override.yml +4 -4
  86. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  87. package/src/proxy/adapters/anthropic.js +0 -131
  88. package/src/proxy/adapters/gemini.js +0 -106
  89. package/src/proxy/adapters/index.js +0 -51
  90. package/src/proxy/adapters/openai.js +0 -71
  91. package/src/proxy/compression/ai-compressor.js +0 -209
  92. package/src/proxy/compression/dedup-cache.js +0 -100
  93. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  94. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  95. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  96. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  97. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  98. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  99. package/src/proxy/compression/deterministic/glob.js +0 -26
  100. package/src/proxy/compression/deterministic/grep.js +0 -53
  101. package/src/proxy/compression/deterministic/index.js +0 -58
  102. package/src/proxy/compression/deterministic/read.js +0 -22
  103. package/src/proxy/compression/deterministic/utils.js +0 -33
  104. package/src/proxy/compression/pipeline.js +0 -270
  105. package/src/proxy/middleware/prompt-injector.js +0 -165
  106. package/src/proxy/middleware/response-interceptor.js +0 -223
  107. package/src/proxy/progress-monitor.js +0 -98
  108. package/src/proxy/proxy-lifecycle.js +0 -201
  109. package/src/proxy/proxy-server.js +0 -239
  110. package/src/proxy/security.js +0 -45
  111. /package/src/{becaria → ci}/index.js +0 -0
  112. /package/src/{becaria → ci}/repo.js +0 -0
@@ -0,0 +1,140 @@
1
+ // Feedback enrichment: transform vague feedback into actionable instructions.
2
+ // Extracts file hints, line hints, and suggests fixes from feedback text.
3
+
4
+ /**
5
+ * Extract file path hints from feedback description.
6
+ * Matches patterns like: packages/server/auth.js, src/routes/*, auth/middleware.ts
7
+ */
8
+ export function extractFileHints(description) {
9
+ if (!description) return [];
10
+ const hints = new Set();
11
+
12
+ // Explicit paths with extensions (supports bare filenames too)
13
+ const pathRe = /\b([\w./_-]+\.(?:js|ts|jsx|tsx|py|go|rs|java|rb|php|cs|swift|dart|html|css|vue|svelte|astro|json|yml|yaml|md|sql))\b/gi;
14
+ const paths = description.match(pathRe) || [];
15
+ for (const p of paths) hints.add(p);
16
+
17
+ // Directory references
18
+ const dirRe = /\b(packages\/[\w-]+|src\/[\w/-]+|tests?\/[\w/-]+|lib\/[\w/-]+)/gi;
19
+ const dirs = description.match(dirRe) || [];
20
+ for (const d of dirs) hints.add(d);
21
+
22
+ return [...hints];
23
+ }
24
+
25
+ /**
26
+ * Detect the category of feedback from description keywords.
27
+ */
28
+ export function detectCategory(description) {
29
+ const text = (description || "").toLowerCase();
30
+ if (/\b(injection|xss|csrf|httponly|sql\s*injection|exposed\s*secret|auth(?:entication)?\s*bypass|authoriza(?:tion)?\s*bypass|cryptograph|token\s*expos)/i.test(text)) return "security";
31
+ if (/\btest|coverage|assert|spec|mock\b/.test(text)) return "tests";
32
+ if (/\bperf(ormance)?|slow|optimize|bottleneck|latency\b/.test(text)) return "performance";
33
+ if (/\b(style|format(?:ting)?|naming|rename|indent|whitespace|comment|jsdoc|cosmetic)\b/.test(text)) return "style";
34
+ if (/\b(bug|incorrect|wrong|fails?|error|crash|broken)\b/.test(text)) return "correctness";
35
+ return "other";
36
+ }
37
+
38
+ /**
39
+ * Detect severity from description keywords and existing severity field.
40
+ */
41
+ export function detectSeverity(description, existingSeverity) {
42
+ if (existingSeverity) return existingSeverity;
43
+ const text = (description || "").toLowerCase();
44
+ if (/\bcritical|blocker|severe|production\s*risk\b/.test(text)) return "critical";
45
+ if (/\b(high|security|vulnerab|breaking|regression|injection|xss|csrf)\b/.test(text)) return "high";
46
+ if (/\blow|minor|cosmetic|nitpick\b/.test(text)) return "low";
47
+ return "medium";
48
+ }
49
+
50
+ /**
51
+ * Generate actionable instructions from a feedback entry.
52
+ * Adds concrete steps the coder can follow.
53
+ */
54
+ export function generateActionPlan(entry) {
55
+ const steps = [];
56
+ const cat = entry.category || detectCategory(entry.description);
57
+
58
+ // Always start with locating the problem
59
+ if (entry.file) {
60
+ steps.push(`Open ${entry.file}${entry.line ? ` at line ${entry.line}` : ""}`);
61
+ } else {
62
+ const hints = extractFileHints(entry.description);
63
+ if (hints.length > 0) {
64
+ steps.push(`Look in: ${hints.join(", ")}`);
65
+ } else {
66
+ steps.push("Identify the relevant file(s) using grep/search");
67
+ }
68
+ }
69
+
70
+ // Category-specific actions
71
+ if (cat === "security") {
72
+ steps.push("Apply the security fix (validate input, escape output, use parameterized queries, etc.)");
73
+ } else if (cat === "tests") {
74
+ steps.push("Write the missing test cases covering the scenario described");
75
+ steps.push("Run tests to verify they pass: npm test or equivalent");
76
+ } else if (cat === "correctness") {
77
+ steps.push("Fix the bug described");
78
+ steps.push("Add a regression test");
79
+ } else if (cat === "performance") {
80
+ steps.push("Optimize the identified bottleneck");
81
+ steps.push("Verify improvement with measurements");
82
+ } else if (cat === "style") {
83
+ steps.push("Apply the style fix (run linter if available)");
84
+ }
85
+
86
+ // Include suggested fix if provided
87
+ if (entry.suggestedFix) {
88
+ steps.push(`Suggested approach: ${entry.suggestedFix}`);
89
+ }
90
+
91
+ return steps;
92
+ }
93
+
94
+ /**
95
+ * Enrich a single feedback entry with file hints, category, severity, and action plan.
96
+ */
97
+ export function enrichEntry(entry) {
98
+ const description = entry.description || "";
99
+ // Re-detect category when missing or set to default "other"
100
+ const category = (!entry.category || entry.category === "other")
101
+ ? detectCategory(description)
102
+ : entry.category;
103
+ const severity = detectSeverity(description, entry.severity);
104
+ const fileHints = entry.file ? [entry.file] : extractFileHints(description);
105
+
106
+ return {
107
+ ...entry,
108
+ category,
109
+ severity,
110
+ fileHints,
111
+ actionPlan: generateActionPlan({ ...entry, category })
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Enrich all entries in a queue in-place.
117
+ */
118
+ export function enrichQueue(queue) {
119
+ queue.entries = queue.entries.map(enrichEntry);
120
+ return queue;
121
+ }
122
+
123
+ /**
124
+ * Format enriched entries as actionable coder prompt sections.
125
+ */
126
+ export function formatEnrichedForCoder(enrichedEntries) {
127
+ if (!enrichedEntries?.length) return "";
128
+
129
+ const sections = [];
130
+ enrichedEntries.forEach((e, i) => {
131
+ const header = `### Issue ${i + 1}: [${e.severity}] ${e.category} — ${e.description}`;
132
+ const location = e.fileHints?.length
133
+ ? `**Location hints:** ${e.fileHints.join(", ")}`
134
+ : "**Location:** search the project for relevant files";
135
+ const plan = `**Action plan:**\n${e.actionPlan.map(s => `- ${s}`).join("\n")}`;
136
+ sections.push([header, location, plan].join("\n"));
137
+ });
138
+
139
+ return sections.join("\n\n");
140
+ }
@@ -0,0 +1,152 @@
1
+ // Structured feedback queue for inter-role communication.
2
+ // Replaces the flat session.last_reviewer_feedback string with typed messages.
3
+
4
+ /**
5
+ * @typedef {Object} FeedbackEntry
6
+ * @property {string} source - "reviewer" | "tester" | "security" | "sonar" | "audit" | "brain"
7
+ * @property {string} severity - "critical" | "high" | "medium" | "low"
8
+ * @property {string} category - "security" | "correctness" | "tests" | "style" | "performance" | "other"
9
+ * @property {string} description
10
+ * @property {string} [file]
11
+ * @property {number} [line]
12
+ * @property {string} [suggestedFix]
13
+ * @property {string} [id]
14
+ * @property {number} [iteration] - when this feedback was added
15
+ */
16
+
17
+ const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
18
+ const CATEGORY_ORDER = { security: 0, correctness: 1, tests: 2, performance: 3, style: 4, other: 5 };
19
+
20
+ /**
21
+ * Create a new empty feedback queue.
22
+ */
23
+ export function createQueue() {
24
+ return { entries: [] };
25
+ }
26
+
27
+ /**
28
+ * Add a feedback entry to the queue.
29
+ */
30
+ export function addEntry(queue, entry) {
31
+ if (!entry || !entry.description) return queue;
32
+ const normalized = {
33
+ source: entry.source || "unknown",
34
+ severity: entry.severity || "medium",
35
+ category: entry.category || "other",
36
+ description: entry.description,
37
+ file: entry.file || null,
38
+ line: entry.line || null,
39
+ suggestedFix: entry.suggestedFix || null,
40
+ id: entry.id || null,
41
+ iteration: entry.iteration || 0
42
+ };
43
+ queue.entries.push(normalized);
44
+ return queue;
45
+ }
46
+
47
+ /**
48
+ * Add multiple entries at once.
49
+ */
50
+ export function addEntries(queue, entries) {
51
+ for (const e of entries || []) addEntry(queue, e);
52
+ return queue;
53
+ }
54
+
55
+ /**
56
+ * Deduplicate entries with the same description + source (keep the most severe).
57
+ */
58
+ export function deduplicate(queue) {
59
+ const seen = new Map();
60
+ for (const e of queue.entries) {
61
+ const key = `${e.source}::${e.description}`;
62
+ const existing = seen.get(key);
63
+ if (!existing || SEVERITY_ORDER[e.severity] < SEVERITY_ORDER[existing.severity]) {
64
+ seen.set(key, e);
65
+ }
66
+ }
67
+ queue.entries = [...seen.values()];
68
+ return queue;
69
+ }
70
+
71
+ /**
72
+ * Sort entries by priority: category first (security > correctness > ...), then severity.
73
+ */
74
+ export function prioritize(queue) {
75
+ queue.entries.sort((a, b) => {
76
+ const catDiff = (CATEGORY_ORDER[a.category] ?? 5) - (CATEGORY_ORDER[b.category] ?? 5);
77
+ if (catDiff !== 0) return catDiff;
78
+ return (SEVERITY_ORDER[a.severity] ?? 2) - (SEVERITY_ORDER[b.severity] ?? 2);
79
+ });
80
+ return queue;
81
+ }
82
+
83
+ /**
84
+ * Format queue as a numbered list for the coder prompt.
85
+ */
86
+ export function formatForCoder(queue) {
87
+ if (!queue.entries.length) return "";
88
+
89
+ const lines = [];
90
+ queue.entries.forEach((e, i) => {
91
+ const loc = e.file ? ` (${e.file}${e.line ? `:${e.line}` : ""})` : "";
92
+ const tag = `[${e.source}:${e.severity}:${e.category}]`;
93
+ lines.push(`${i + 1}. ${tag}${loc} ${e.description}`);
94
+ if (e.suggestedFix) lines.push(` Fix: ${e.suggestedFix}`);
95
+ });
96
+ return lines.join("\n");
97
+ }
98
+
99
+ /**
100
+ * Filter entries by category.
101
+ */
102
+ export function filterByCategory(queue, category) {
103
+ return { entries: queue.entries.filter(e => e.category === category) };
104
+ }
105
+
106
+ /**
107
+ * Check if queue has any critical/high severity entries.
108
+ */
109
+ export function hasBlockingIssues(queue) {
110
+ return queue.entries.some(e => e.severity === "critical" || e.severity === "high");
111
+ }
112
+
113
+ /**
114
+ * Get count of entries by category.
115
+ */
116
+ export function countByCategory(queue) {
117
+ const counts = {};
118
+ for (const e of queue.entries) {
119
+ counts[e.category] = (counts[e.category] || 0) + 1;
120
+ }
121
+ return counts;
122
+ }
123
+
124
+ /**
125
+ * Clear the queue (after coder addresses the feedback).
126
+ */
127
+ export function clear(queue) {
128
+ queue.entries = [];
129
+ return queue;
130
+ }
131
+
132
+ /**
133
+ * Serialize queue for persistence in session.
134
+ */
135
+ export function serialize(queue) {
136
+ return JSON.stringify(queue);
137
+ }
138
+
139
+ /**
140
+ * Deserialize queue from session.
141
+ */
142
+ export function deserialize(str) {
143
+ if (!str) return createQueue();
144
+ try {
145
+ const parsed = JSON.parse(str);
146
+ return { entries: Array.isArray(parsed.entries) ? parsed.entries : [] };
147
+ } catch {
148
+ return createQueue();
149
+ }
150
+ }
151
+
152
+ export { SEVERITY_ORDER, CATEGORY_ORDER };
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Session flow control — checkpoints, timeouts, budget checks.
3
+ * Extracted from orchestrator.js — session management, not orchestration.
4
+ */
5
+ import { markSessionStatus, addCheckpoint } from "../session-store.js";
6
+ import { emitProgress, makeEvent } from "../utils/events.js";
7
+ import { msg, getLang } from "../utils/messages.js";
8
+
9
+ function detectCheckpointProgress(session, lastCheckpointSnapshot) {
10
+ if (!lastCheckpointSnapshot) return true; // First checkpoint — assume progress
11
+ const currentIteration = session.reviewer_retry_count ?? 0;
12
+ const currentStages = Object.keys(session.resolved_policies || {}).length;
13
+ const currentCheckpoints = (session.checkpoints || []).length;
14
+
15
+ const iterationAdvanced = currentIteration !== lastCheckpointSnapshot.iteration;
16
+ const stagesChanged = currentStages !== lastCheckpointSnapshot.stagesCount;
17
+ const checkpointsChanged = currentCheckpoints !== lastCheckpointSnapshot.checkpointsCount;
18
+
19
+ return iterationAdvanced || stagesChanged || checkpointsChanged;
20
+ }
21
+
22
+ export function takeCheckpointSnapshot(session) {
23
+ return {
24
+ iteration: session.reviewer_retry_count ?? 0,
25
+ stagesCount: Object.keys(session.resolved_policies || {}).length,
26
+ checkpointsCount: (session.checkpoints || []).length
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Determine if checkpoint should auto-continue without asking the user.
32
+ * Exported for testing.
33
+ */
34
+ export function shouldAutoContinueCheckpoint(session, hasProgress) {
35
+ if (hasProgress) {
36
+ session._checkpoint_stall_count = 0;
37
+ return { autoContinue: true, reason: "progress_detected" };
38
+ }
39
+ const wasRateLimited = (session.standby_retry_count || 0) > 0;
40
+ const consecutiveStalls = (session._checkpoint_stall_count || 0) + 1;
41
+ session._checkpoint_stall_count = consecutiveStalls;
42
+ if (wasRateLimited && consecutiveStalls < 3) {
43
+ return { autoContinue: true, reason: "recoverable_stall" };
44
+ }
45
+ return { autoContinue: false, reason: consecutiveStalls >= 3 ? "max_stalls_reached" : "no_progress" };
46
+ }
47
+
48
+ export async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary, lastCheckpointSnapshot }) {
49
+ if (checkpointDisabled || !askQuestion || (Date.now() - lastCheckpointAt) < checkpointIntervalMs) {
50
+ return { action: "continue_loop", checkpointDisabled, lastCheckpointAt, lastCheckpointSnapshot };
51
+ }
52
+
53
+ const elapsedStr = elapsedMinutes.toFixed(1);
54
+ const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
55
+
56
+ const hasProgress = detectCheckpointProgress(session, lastCheckpointSnapshot);
57
+ const newSnapshot = takeCheckpointSnapshot(session);
58
+ const decision = shouldAutoContinueCheckpoint(session, hasProgress);
59
+
60
+ if (decision.autoContinue) {
61
+ emitProgress(
62
+ emitter,
63
+ makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
64
+ message: `Checkpoint: ${decision.reason === "progress_detected" ? "progress detected" : "stall caused by rate limit/cooldown"}, auto-continuing (${elapsedStr} min elapsed)`,
65
+ detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true, reason: decision.reason }
66
+ })
67
+ );
68
+ return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now(), lastCheckpointSnapshot: newSnapshot };
69
+ }
70
+
71
+ const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
72
+ const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
73
+ const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. No progress since last checkpoint. What would you like to do?`;
74
+
75
+ emitProgress(
76
+ emitter,
77
+ makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
78
+ message: `Interactive checkpoint at ${elapsedStr} min (stalled)`,
79
+ detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: false }
80
+ })
81
+ );
82
+
83
+ const lang = getLang(config);
84
+ const answer = await askQuestion(
85
+ `${checkpointMsg}\n\n${msg("checkpoint_options", lang)}`
86
+ );
87
+
88
+ await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
89
+
90
+ const trimmedAnswer = (answer || "").trim();
91
+ const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
92
+
93
+ if (isExplicitStop) {
94
+ await markSessionStatus(session, "stopped");
95
+ emitProgress(
96
+ emitter,
97
+ makeEvent("session:end", { ...eventBase, iteration: i, stage: "user-stop" }, {
98
+ status: "stopped",
99
+ message: "Session stopped by user at checkpoint",
100
+ detail: { approved: false, reason: "user_stopped", elapsed_minutes: Number(elapsedStr), budget: budgetSummary() }
101
+ })
102
+ );
103
+ return { action: "stop", result: { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) } };
104
+ }
105
+
106
+ const parsed = parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config });
107
+ parsed.lastCheckpointSnapshot = newSnapshot;
108
+ return parsed;
109
+ }
110
+
111
+ export function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
112
+ if (!trimmedAnswer) {
113
+ return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
114
+ }
115
+ if (trimmedAnswer === "2" || trimmedAnswer.toLowerCase().startsWith("continue until")) {
116
+ return { action: "continue_loop", checkpointDisabled: true, lastCheckpointAt: Date.now() };
117
+ }
118
+ if (trimmedAnswer === "1" || trimmedAnswer.toLowerCase().includes("5 m")) {
119
+ return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
120
+ }
121
+ const customMinutes = Number.parseInt(trimmedAnswer.replaceAll(/\D/g, ""), 10);
122
+ if (customMinutes > 0) {
123
+ config.session.checkpoint_interval_minutes = customMinutes;
124
+ return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
125
+ }
126
+ return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
127
+ }
128
+
129
+ export async function checkSessionTimeout({ askQuestion, elapsedMinutes, config, session, emitter, eventBase, i, budgetSummary }) {
130
+ if (askQuestion || elapsedMinutes <= config.session.max_total_minutes) return;
131
+
132
+ await markSessionStatus(session, "failed");
133
+ emitProgress(
134
+ emitter,
135
+ makeEvent("session:end", { ...eventBase, iteration: i, stage: "timeout" }, {
136
+ status: "fail",
137
+ message: "Session timed out",
138
+ detail: { approved: false, reason: "timeout", budget: budgetSummary() }
139
+ })
140
+ );
141
+ throw new Error("Session timed out");
142
+ }
143
+
144
+ export async function checkBudgetExceeded({ budgetTracker, config, session, emitter, eventBase, i, budgetLimit, budgetSummary }) {
145
+ if (!budgetTracker.isOverBudget(config?.max_budget_usd)) return;
146
+
147
+ await markSessionStatus(session, "failed");
148
+ const totalCost = budgetTracker.total().cost_usd;
149
+ const message = `Budget exceeded: $${totalCost.toFixed(2)} > $${budgetLimit.toFixed(2)}`;
150
+ emitProgress(
151
+ emitter,
152
+ makeEvent("session:end", { ...eventBase, iteration: i, stage: "budget" }, {
153
+ status: "fail",
154
+ message,
155
+ detail: { approved: false, reason: "budget_exceeded", budget: budgetSummary(), max_budget_usd: budgetLimit }
156
+ })
157
+ );
158
+ throw new Error(message);
159
+ }
@@ -138,18 +138,20 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
138
138
  );
139
139
 
140
140
  if (!testerOutput.ok) {
141
- // Tester findings are advisory when reviewer already approved.
142
- // Auto-continue with a warning — no human escalation needed.
143
- logger.warn(`Tester failed (advisory): ${testerOutput.summary}`);
141
+ // Tester failed — blocking, return to coder for correction
142
+ logger.warn(`Tester failed (blocking): ${testerOutput.summary}`);
144
143
  emitProgress(
145
144
  emitter,
146
- makeEvent("tester:auto-continue", { ...eventBase, stage: "tester" }, {
147
- status: "warn",
148
- message: `Tester issues are advisory (reviewer approved), continuing: ${testerOutput.summary}`,
149
- detail: { summary: testerOutput.summary, auto_continued: true, provider: testerProvider, executorType: "agent" }
145
+ makeEvent("tester:blocking", { ...eventBase, stage: "tester" }, {
146
+ status: "fail",
147
+ message: `Tester failed — sending back to coder for correction: ${testerOutput.summary}`,
148
+ detail: { summary: testerOutput.summary, blocking: true, provider: testerProvider, executorType: "agent" }
150
149
  })
151
150
  );
152
- return { action: "ok", stageResult: { ok: false, summary: testerOutput.summary || "Tester issues (advisory)", auto_continued: true } };
151
+ return {
152
+ action: "continue",
153
+ stageResult: { ...testerOutput.result, summary: testerOutput.summary, provider: testerProvider }
154
+ };
153
155
  }
154
156
 
155
157
  session.tester_retry_count = 0;
@@ -230,17 +232,20 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
230
232
  return { action: "ok" };
231
233
  }
232
234
 
233
- // Non-critical security findings are advisory when reviewer already approved.
234
- logger.warn(`Security failed (advisory): ${securityOutput.summary}`);
235
+ // Non-critical security findings — blocking, return to coder for correction
236
+ logger.warn(`Security failed (blocking): ${securityOutput.summary}`);
235
237
  emitProgress(
236
238
  emitter,
237
- makeEvent("security:auto-continue", { ...eventBase, stage: "security" }, {
238
- status: "warn",
239
- message: `Security issues are advisory (reviewer approved), continuing: ${securityOutput.summary}`,
240
- detail: { summary: securityOutput.summary, auto_continued: true }
239
+ makeEvent("security:blocking", { ...eventBase, stage: "security" }, {
240
+ status: "fail",
241
+ message: `Security failed — sending back to coder for correction: ${securityOutput.summary}`,
242
+ detail: { summary: securityOutput.summary, blocking: true, provider: securityProvider, executorType: "agent" }
241
243
  })
242
244
  );
243
- return { action: "ok", stageResult: { ok: false, summary: securityOutput.summary || "Security issues (advisory)", auto_continued: true } };
245
+ return {
246
+ action: "continue",
247
+ stageResult: { ...securityOutput.result, summary: securityOutput.summary, provider: securityProvider }
248
+ };
244
249
  }
245
250
 
246
251
  session.security_retry_count = 0;
@@ -17,8 +17,10 @@ import { msg, getLang } from "../utils/messages.js";
17
17
  import {
18
18
  resolveSonarHost,
19
19
  resolveSonarToken,
20
+ resolveSonarTokenAsync,
20
21
  resolveSonarCredentials,
21
22
  } from "../sonar/config-resolver.js";
23
+ import { saveSonarToken } from "../sonar/credentials.js";
22
24
  import { withDocLink } from "../utils/doc-links.js";
23
25
 
24
26
  function parseJsonSafe(text) {
@@ -75,8 +77,8 @@ async function checkSonarReachable(host) {
75
77
  async function checkSonarAuth(config) {
76
78
  const host = resolveSonarHost(config.sonarqube?.host);
77
79
 
78
- // Check explicit token first
79
- const explicitToken = resolveSonarToken(config);
80
+ // Check explicit token first (including persisted tokens from credentials file)
81
+ const explicitToken = await resolveSonarTokenAsync(config);
80
82
  if (explicitToken) {
81
83
  // Validate the token works
82
84
  const res = await runCommand("curl", [
@@ -117,7 +119,9 @@ async function checkSonarAuth(config) {
117
119
  if (tokenRes.exitCode !== 0) continue;
118
120
  const tokenParsed = parseJsonSafe(tokenRes.stdout);
119
121
  if (tokenParsed?.token) {
120
- return { name: "sonar-auth", ok: true, detail: "Sonar token generated", token: tokenParsed.token };
122
+ // Persist the generated token for future sessions
123
+ try { await saveSonarToken(tokenParsed.token); } catch { /* non-blocking */ }
124
+ return { name: "sonar-auth", ok: true, detail: "Sonar token generated and saved", token: tokenParsed.token };
121
125
  }
122
126
  }
123
127