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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +1 -4
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +2 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +10 -52
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +1 -1
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +428 -664
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +33 -88
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +51 -115
- package/src/roles/reviewer-role.js +68 -125
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +69 -11
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/display.js +33 -14
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -201
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /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
|
|
142
|
-
|
|
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:
|
|
147
|
-
status: "
|
|
148
|
-
message: `Tester
|
|
149
|
-
detail: { summary: testerOutput.summary,
|
|
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 {
|
|
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
|
|
234
|
-
logger.warn(`Security failed (
|
|
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:
|
|
238
|
-
status: "
|
|
239
|
-
message: `Security
|
|
240
|
-
detail: { summary: securityOutput.summary,
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
|