karajan-code 1.58.2 → 1.59.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 +1 -1
- package/src/agents/aider-agent.js +7 -0
- package/src/agents/claude-agent.js +36 -2
- package/src/agents/codex-agent.js +4 -0
- package/src/agents/gemini-agent.js +4 -0
- package/src/agents/opencode-agent.js +4 -0
- package/src/bootstrap.js +20 -9
- package/src/cli.js +1 -0
- package/src/commands/doctor.js +19 -0
- package/src/commands/report.js +13 -0
- package/src/commands/status.js +7 -0
- package/src/config.js +35 -0
- package/src/domains/domain-loader.js +85 -4
- package/src/git/automation.js +32 -4
- package/src/guards/policy-guard.js +90 -0
- package/src/mcp/handlers/run-handler.js +20 -3
- package/src/mcp/shared-helpers.js +4 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/orchestrator.js +77 -20
- package/src/prompts/coder.js +5 -4
- package/src/prompts/reviewer.js +5 -4
- package/src/prompts/rtk-snippet.js +12 -0
- package/src/proxy/adapters/anthropic.js +131 -0
- package/src/proxy/adapters/gemini.js +106 -0
- package/src/proxy/adapters/index.js +51 -0
- package/src/proxy/adapters/openai.js +71 -0
- package/src/proxy/compression/ai-compressor.js +209 -0
- package/src/proxy/compression/dedup-cache.js +100 -0
- package/src/proxy/compression/deterministic/bash-build.js +60 -0
- package/src/proxy/compression/deterministic/bash-git.js +101 -0
- package/src/proxy/compression/deterministic/bash-infra.js +64 -0
- package/src/proxy/compression/deterministic/bash-misc.js +53 -0
- package/src/proxy/compression/deterministic/bash-pkg.js +49 -0
- package/src/proxy/compression/deterministic/bash-test.js +61 -0
- package/src/proxy/compression/deterministic/glob.js +26 -0
- package/src/proxy/compression/deterministic/grep.js +53 -0
- package/src/proxy/compression/deterministic/index.js +58 -0
- package/src/proxy/compression/deterministic/read.js +22 -0
- package/src/proxy/compression/deterministic/utils.js +33 -0
- package/src/proxy/compression/pipeline.js +270 -0
- package/src/proxy/middleware/prompt-injector.js +165 -0
- package/src/proxy/middleware/response-interceptor.js +223 -0
- package/src/proxy/progress-monitor.js +98 -0
- package/src/proxy/proxy-lifecycle.js +201 -0
- package/src/proxy/proxy-server.js +239 -0
- package/src/proxy/security.js +45 -0
- package/src/review/diff-generator.js +10 -8
- package/src/roles/coder-role.js +1 -0
- package/src/roles/impeccable-role.js +2 -4
- package/src/roles/researcher-role.js +2 -4
- package/src/roles/reviewer-role.js +10 -8
- package/src/roles/solomon-role.js +2 -4
- package/src/session-cleanup.js +39 -15
- package/src/sonar/cloud-scanner.js +6 -3
- package/src/sonar/scanner.js +12 -7
- package/src/utils/banner.js +66 -0
- package/src/utils/display.js +46 -7
- package/src/utils/git.js +5 -0
- package/src/utils/welcome.js +3 -8
|
@@ -8,6 +8,7 @@ import { runFlow, resumeFlow } from "../../orchestrator.js";
|
|
|
8
8
|
import { loadConfig, applyRunOverrides, validateConfig, resolveRole } from "../../config.js";
|
|
9
9
|
import { createLogger } from "../../utils/logger.js";
|
|
10
10
|
import { assertAgentsAvailable } from "../../agents/availability.js";
|
|
11
|
+
import { validatePolicyCompliance } from "../../guards/policy-guard.js";
|
|
11
12
|
import { createRunLog } from "../../utils/run-log.js";
|
|
12
13
|
import { buildProgressHandler, buildProgressNotifier, buildPipelineTracker } from "../progress.js";
|
|
13
14
|
import { isPreflightAcked, ackPreflight, getSessionOverrides } from "../preflight.js";
|
|
@@ -113,7 +114,7 @@ function collectRequiredProviders(config) {
|
|
|
113
114
|
|
|
114
115
|
export async function handleRunDirect(a, server, extra) {
|
|
115
116
|
const config = await buildConfig(a);
|
|
116
|
-
await assertNotOnBaseBranch(config);
|
|
117
|
+
await assertNotOnBaseBranch(config, { allowForRun: true });
|
|
117
118
|
const logger = createLogger(config.output.log_level, "mcp");
|
|
118
119
|
|
|
119
120
|
// Best-effort session cleanup before starting
|
|
@@ -203,7 +204,7 @@ const RESUME_INJECTION_PATTERNS = [
|
|
|
203
204
|
/force\s+(approve|merge|push|commit)\b/i,
|
|
204
205
|
];
|
|
205
206
|
|
|
206
|
-
export function validateResumeAnswer(answer) {
|
|
207
|
+
export function validateResumeAnswer(answer, pauseContext) {
|
|
207
208
|
if (answer == null || answer === "") return { valid: true, sanitized: answer ?? null };
|
|
208
209
|
if (typeof answer !== "string") return { valid: true, sanitized: String(answer) };
|
|
209
210
|
if (answer.length > RESUME_MAX_ANSWER_LENGTH) {
|
|
@@ -214,6 +215,14 @@ export function validateResumeAnswer(answer) {
|
|
|
214
215
|
return { valid: false, reason: "Answer rejected: matches guardrail bypass pattern" };
|
|
215
216
|
}
|
|
216
217
|
}
|
|
218
|
+
// Policy compliance: reject attempts to bypass TDD, reviewer, security, etc.
|
|
219
|
+
const policy = validatePolicyCompliance(answer, pauseContext);
|
|
220
|
+
if (!policy.valid) {
|
|
221
|
+
const suggestionsText = policy.suggestions.length > 0
|
|
222
|
+
? `\nValid alternatives:\n${policy.suggestions.map(s => ` - ${s}`).join("\n")}`
|
|
223
|
+
: "";
|
|
224
|
+
return { valid: false, reason: `${policy.reason}${suggestionsText}` };
|
|
225
|
+
}
|
|
217
226
|
return { valid: true, sanitized: answer.trim() };
|
|
218
227
|
}
|
|
219
228
|
|
|
@@ -240,7 +249,15 @@ export async function handleResume(a, server, extra) {
|
|
|
240
249
|
}
|
|
241
250
|
await runBootstrapGate(server, a);
|
|
242
251
|
if (a.answer) {
|
|
243
|
-
|
|
252
|
+
// Load session to get pause context for policy-aware validation
|
|
253
|
+
let pauseContext;
|
|
254
|
+
try {
|
|
255
|
+
const { loadSession } = await import("../../session-store.js");
|
|
256
|
+
const session = await loadSession(a.sessionId);
|
|
257
|
+
const ctx = session?.paused_state?.context;
|
|
258
|
+
pauseContext = typeof ctx === "string" ? ctx : ctx?.reason;
|
|
259
|
+
} catch { /* session read error — validate without context */ }
|
|
260
|
+
const validation = validateResumeAnswer(a.answer, pauseContext);
|
|
244
261
|
if (!validation.valid) {
|
|
245
262
|
return failPayload(`Resume answer rejected: ${validation.reason}`);
|
|
246
263
|
}
|
|
@@ -67,15 +67,17 @@ export function failPayload(message, details = {}) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export async function assertNotOnBaseBranch(config) {
|
|
70
|
+
export async function assertNotOnBaseBranch(config, { allowForRun = false } = {}) {
|
|
71
71
|
const baseBranch = config?.base_branch || "main";
|
|
72
72
|
let branch;
|
|
73
73
|
try {
|
|
74
74
|
branch = await currentBranch();
|
|
75
75
|
} catch {
|
|
76
|
-
return;
|
|
76
|
+
return; // No git repo or no commits — bootstrap/orchestrator will handle it
|
|
77
77
|
}
|
|
78
78
|
if (branch === baseBranch) {
|
|
79
|
+
// kj_run creates its own branch via prepareGitAutomation — allow it
|
|
80
|
+
if (allowForRun) return;
|
|
79
81
|
throw new Error(
|
|
80
82
|
`You are on the base branch '${baseBranch}'. Karajan needs a feature branch to compute the diff for review. ` +
|
|
81
83
|
`Create a new branch first (e.g. 'git checkout -b feat/<task-description>') and then run this command again. ` +
|
|
@@ -24,7 +24,7 @@ const ALLOWED_PARAMS = new Set([
|
|
|
24
24
|
"autoCommit", "autoPush", "autoPr", "autoRebase", "branchPrefix",
|
|
25
25
|
"autoSimplify", "smartModels", "checkpointInterval",
|
|
26
26
|
"taskType", "quiet", "noSonar", "enableSonarcloud",
|
|
27
|
-
"kjHome", "sonarToken", "timeoutMs",
|
|
27
|
+
"kjHome", "sonarToken", "timeoutMs", "domain",
|
|
28
28
|
// Sovereign flags — validated below but still "known"
|
|
29
29
|
"enableTriage", "enableHuReviewer",
|
|
30
30
|
]);
|
package/src/mcp/tools.js
CHANGED
|
@@ -104,7 +104,8 @@ export const tools = [
|
|
|
104
104
|
enableSonarcloud: { type: "boolean", description: "Enable SonarCloud scan (complementary to SonarQube)" },
|
|
105
105
|
kjHome: { type: "string" },
|
|
106
106
|
sonarToken: { type: "string" },
|
|
107
|
-
timeoutMs: { type: "number" }
|
|
107
|
+
timeoutMs: { type: "number" },
|
|
108
|
+
domain: { type: "string", description: "Domain knowledge: inline text describing the project domain, or absolute path to a .md file. Auto-saved to .karajan/domains/ for the curator." }
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
},
|
package/src/orchestrator.js
CHANGED
|
@@ -35,6 +35,7 @@ import { msg, getLang } from "./utils/messages.js";
|
|
|
35
35
|
import { PipelineContext } from "./orchestrator/pipeline-context.js";
|
|
36
36
|
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
|
|
37
37
|
import { runDomainCuratorStage } from "./orchestrator/stages/domain-curator-stage.js";
|
|
38
|
+
import { persistInlineDomain } from "./domains/domain-loader.js";
|
|
38
39
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
39
40
|
import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
|
|
40
41
|
import { needsSubPipeline, runHuSubPipeline } from "./orchestrator/hu-sub-pipeline.js";
|
|
@@ -47,6 +48,7 @@ import { setRunner as setDiffRunner } from "./review/diff-generator.js";
|
|
|
47
48
|
import { setRunner as setGitRunner } from "./utils/git.js";
|
|
48
49
|
import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "./skills/skill-detector.js";
|
|
49
50
|
import { isOpenSkillsAvailable } from "./skills/openskills-client.js";
|
|
51
|
+
import { startProxy, stopProxy } from "./proxy/proxy-lifecycle.js";
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
// --- Product Context loader ---
|
|
@@ -104,8 +106,8 @@ async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
|
|
|
104
106
|
const projectDir = config.projectDir || process.cwd();
|
|
105
107
|
const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
|
|
106
108
|
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
|
107
|
-
const coderPrompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), productContext: config.productContext || null });
|
|
108
|
-
const reviewerPrompt = await buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), productContext: config.productContext || null });
|
|
109
|
+
const coderPrompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), proxyEnabled: Boolean(config.proxy?.enabled), productContext: config.productContext || null });
|
|
110
|
+
const reviewerPrompt = await buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), proxyEnabled: Boolean(config.proxy?.enabled), productContext: config.productContext || null });
|
|
109
111
|
|
|
110
112
|
const summary = {
|
|
111
113
|
dry_run: true,
|
|
@@ -380,6 +382,24 @@ function takeCheckpointSnapshot(session) {
|
|
|
380
382
|
};
|
|
381
383
|
}
|
|
382
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Determine if checkpoint should auto-continue without asking the user.
|
|
387
|
+
* Exported for testing.
|
|
388
|
+
*/
|
|
389
|
+
export function shouldAutoContinueCheckpoint(session, hasProgress) {
|
|
390
|
+
if (hasProgress) {
|
|
391
|
+
session._checkpoint_stall_count = 0;
|
|
392
|
+
return { autoContinue: true, reason: "progress_detected" };
|
|
393
|
+
}
|
|
394
|
+
const wasRateLimited = (session.standby_retry_count || 0) > 0;
|
|
395
|
+
const consecutiveStalls = (session._checkpoint_stall_count || 0) + 1;
|
|
396
|
+
session._checkpoint_stall_count = consecutiveStalls;
|
|
397
|
+
if (wasRateLimited && consecutiveStalls < 3) {
|
|
398
|
+
return { autoContinue: true, reason: "recoverable_stall" };
|
|
399
|
+
}
|
|
400
|
+
return { autoContinue: false, reason: consecutiveStalls >= 3 ? "max_stalls_reached" : "no_progress" };
|
|
401
|
+
}
|
|
402
|
+
|
|
383
403
|
async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary, lastCheckpointSnapshot }) {
|
|
384
404
|
if (checkpointDisabled || !askQuestion || (Date.now() - lastCheckpointAt) < checkpointIntervalMs) {
|
|
385
405
|
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt, lastCheckpointSnapshot };
|
|
@@ -388,22 +408,23 @@ async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpoin
|
|
|
388
408
|
const elapsedStr = elapsedMinutes.toFixed(1);
|
|
389
409
|
const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
|
|
390
410
|
|
|
391
|
-
//
|
|
411
|
+
// Decide whether to auto-continue or ask the user
|
|
392
412
|
const hasProgress = detectCheckpointProgress(session, lastCheckpointSnapshot);
|
|
393
413
|
const newSnapshot = takeCheckpointSnapshot(session);
|
|
414
|
+
const decision = shouldAutoContinueCheckpoint(session, hasProgress);
|
|
394
415
|
|
|
395
|
-
if (
|
|
416
|
+
if (decision.autoContinue) {
|
|
396
417
|
emitProgress(
|
|
397
418
|
emitter,
|
|
398
419
|
makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
|
|
399
|
-
message: `Checkpoint: progress detected, continuing (${elapsedStr} min elapsed)`,
|
|
400
|
-
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true,
|
|
420
|
+
message: `Checkpoint: ${decision.reason === "progress_detected" ? "progress detected" : "stall caused by rate limit/cooldown"}, auto-continuing (${elapsedStr} min elapsed)`,
|
|
421
|
+
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true, reason: decision.reason }
|
|
401
422
|
})
|
|
402
423
|
);
|
|
403
424
|
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now(), lastCheckpointSnapshot: newSnapshot };
|
|
404
425
|
}
|
|
405
426
|
|
|
406
|
-
// No progress — ask human
|
|
427
|
+
// No progress and not recoverable — ask human
|
|
407
428
|
const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
|
|
408
429
|
const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
|
|
409
430
|
const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. No progress since last checkpoint. What would you like to do?`;
|
|
@@ -920,6 +941,15 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
920
941
|
logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
|
|
921
942
|
}
|
|
922
943
|
|
|
944
|
+
// --- Persist inline domain if provided via --domain flag ---
|
|
945
|
+
if (flags?.domain) {
|
|
946
|
+
try {
|
|
947
|
+
await persistInlineDomain(flags.domain, config.projectDir || process.cwd());
|
|
948
|
+
} catch (err) {
|
|
949
|
+
logger.warn(`Failed to persist inline domain: ${err.message}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
923
953
|
// --- Domain Curator (after triage + skill auto-install, before planning phases) ---
|
|
924
954
|
const domainHints = triageResult.stageResult?.domainHints || [];
|
|
925
955
|
if (domainHints.length > 0 || config.projectDir) {
|
|
@@ -1310,19 +1340,23 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1310
1340
|
ctx.trackBudget = trackBudget;
|
|
1311
1341
|
|
|
1312
1342
|
// --- RTK detection ---
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1343
|
+
if (config.proxy?.enabled) {
|
|
1344
|
+
logger.info("Proxy compression active, RTK not needed");
|
|
1345
|
+
} else {
|
|
1346
|
+
const rtkResult = await detectRtk();
|
|
1347
|
+
if (rtkResult.available) {
|
|
1348
|
+
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
1349
|
+
const rtkTracker = new RtkSavingsTracker();
|
|
1350
|
+
const rtkRunner = createRtkRunner(true, rtkTracker);
|
|
1351
|
+
setDiffRunner(rtkRunner);
|
|
1352
|
+
setGitRunner(rtkRunner);
|
|
1353
|
+
ctx.rtkTracker = rtkTracker;
|
|
1354
|
+
logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
|
|
1355
|
+
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
1356
|
+
message: "RTK detected — internal commands wrapped for token optimization",
|
|
1357
|
+
detail: { version: rtkResult.version, executorType: "local" }
|
|
1358
|
+
}));
|
|
1359
|
+
}
|
|
1326
1360
|
}
|
|
1327
1361
|
|
|
1328
1362
|
// --- HU Board auto-start ---
|
|
@@ -1348,6 +1382,21 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1348
1382
|
ctx.pgCard = pgAdapterResult.pgCard;
|
|
1349
1383
|
ctx.session.pg_card = ctx.pgCard || null;
|
|
1350
1384
|
|
|
1385
|
+
// --- Proxy startup ---
|
|
1386
|
+
if (config.proxy?.enabled !== false) {
|
|
1387
|
+
try {
|
|
1388
|
+
const proxyResult = await startProxy({ config: config.proxy || {}, sessionId: ctx.session.id });
|
|
1389
|
+
ctx.proxyPort = proxyResult.port;
|
|
1390
|
+
logger.info(`Proxy started on 127.0.0.1:${proxyResult.port}`);
|
|
1391
|
+
emitProgress(emitter, makeEvent("proxy:started", ctx.eventBase, {
|
|
1392
|
+
message: `Proxy started on port ${proxyResult.port}`,
|
|
1393
|
+
detail: { port: proxyResult.port, baseUrls: proxyResult.baseUrls }
|
|
1394
|
+
}));
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
logger.warn(`Proxy startup failed (non-blocking): ${err.message}`);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1351
1400
|
emitProgress(
|
|
1352
1401
|
emitter,
|
|
1353
1402
|
makeEvent("session:start", ctx.eventBase, {
|
|
@@ -1618,6 +1667,14 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1618
1667
|
await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
|
|
1619
1668
|
return result;
|
|
1620
1669
|
} finally {
|
|
1670
|
+
// --- Proxy shutdown ---
|
|
1671
|
+
try {
|
|
1672
|
+
await stopProxy();
|
|
1673
|
+
if (ctx.proxyPort) {
|
|
1674
|
+
logger.info("Proxy stopped");
|
|
1675
|
+
}
|
|
1676
|
+
} catch { /* non-blocking */ }
|
|
1677
|
+
|
|
1621
1678
|
// --- Cleanup auto-installed skills ---
|
|
1622
1679
|
const autoSkills = ctx.session?.autoInstalledSkills;
|
|
1623
1680
|
if (autoSkills && autoSkills.length > 0) {
|
package/src/prompts/coder.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildRtkInstructions } from "./rtk-snippet.js";
|
|
2
2
|
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
3
3
|
import { getLanguageInstruction } from "../utils/locale.js";
|
|
4
4
|
|
|
@@ -33,7 +33,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
33
33
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
34
34
|
].join("\n");
|
|
35
35
|
|
|
36
|
-
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en" }) {
|
|
36
|
+
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, proxyEnabled = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en" }) {
|
|
37
37
|
const langInstruction = getLanguageInstruction(language);
|
|
38
38
|
const sections = [
|
|
39
39
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
@@ -48,8 +48,9 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
48
48
|
sections.push(SERENA_INSTRUCTIONS);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const rtkSnippet = buildRtkInstructions({ rtkAvailable, proxyEnabled });
|
|
52
|
+
if (rtkSnippet) {
|
|
53
|
+
sections.push(rtkSnippet);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
if (productContext) {
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildRtkInstructions } from "./rtk-snippet.js";
|
|
2
2
|
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
3
3
|
import { getLanguageInstruction } from "../utils/locale.js";
|
|
4
4
|
|
|
@@ -24,7 +24,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
24
24
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
25
25
|
].join("\n");
|
|
26
26
|
|
|
27
|
-
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en" }) {
|
|
27
|
+
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, proxyEnabled = false, productContext = null, domainContext = null, projectDir = null, language = "en" }) {
|
|
28
28
|
const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
|
|
29
29
|
|
|
30
30
|
const langInstruction = getLanguageInstruction(language);
|
|
@@ -43,8 +43,9 @@ export async function buildReviewerPrompt({ task, diff, reviewRules, mode, seren
|
|
|
43
43
|
sections.push(SERENA_INSTRUCTIONS);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const rtkSnippet = buildRtkInstructions({ rtkAvailable, proxyEnabled });
|
|
47
|
+
if (rtkSnippet) {
|
|
48
|
+
sections.push(rtkSnippet);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
if (productContext) {
|
|
@@ -13,3 +13,15 @@ export const RTK_INSTRUCTIONS = [
|
|
|
13
13
|
"- Use `rtk cat` instead of `cat`",
|
|
14
14
|
"This does NOT apply to non-Bash tools (Read, Write, Edit, Glob, Grep)."
|
|
15
15
|
].join("\n");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build RTK instructions, returning empty string when proxy compression
|
|
19
|
+
* handles token optimization at the HTTP layer.
|
|
20
|
+
* @param {{ rtkAvailable?: boolean, proxyEnabled?: boolean }} options
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function buildRtkInstructions({ rtkAvailable = false, proxyEnabled = false } = {}) {
|
|
24
|
+
if (proxyEnabled) return "";
|
|
25
|
+
if (!rtkAvailable) return "";
|
|
26
|
+
return RTK_INSTRUCTIONS;
|
|
27
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic message format adapter.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic messages API uses an array of messages where each message has a
|
|
5
|
+
* `content` array. Tool results appear as content blocks with
|
|
6
|
+
* `type: "tool_result"`. Each tool_result block contains an `id` (matching
|
|
7
|
+
* the prior tool_use), optional `tool_use_id`, and nested `content` that
|
|
8
|
+
* can be a string or an array of content blocks (text, image, etc.).
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract tool results from Anthropic-format messages.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array<object>} messages - Anthropic messages array
|
|
17
|
+
* @returns {Array<{id: string, toolName: string, text: string, turnIndex: number}>}
|
|
18
|
+
*/
|
|
19
|
+
export function extractToolResults(messages) {
|
|
20
|
+
const results = [];
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < messages.length; i++) {
|
|
23
|
+
const msg = messages[i];
|
|
24
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
25
|
+
|
|
26
|
+
// Collect tool_use names from assistant messages so we can map ids to names
|
|
27
|
+
const toolNameMap = buildToolNameMap(messages, i);
|
|
28
|
+
|
|
29
|
+
for (const block of msg.content) {
|
|
30
|
+
if (block.type !== "tool_result") continue;
|
|
31
|
+
|
|
32
|
+
const id = block.tool_use_id || block.id || "";
|
|
33
|
+
const text = extractText(block);
|
|
34
|
+
const toolName = toolNameMap.get(id) || "unknown";
|
|
35
|
+
|
|
36
|
+
results.push({ id, toolName, text, turnIndex: i });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Rebuild messages replacing tool_result text with compressed versions.
|
|
45
|
+
*
|
|
46
|
+
* @param {Array<object>} messages - Original Anthropic messages array
|
|
47
|
+
* @param {Map<string, string>|Record<string, string>} compressedMap - id → compressed text
|
|
48
|
+
* @returns {Array<object>} Deep-cloned messages with replacements applied
|
|
49
|
+
*/
|
|
50
|
+
export function rebuildMessages(messages, compressedMap) {
|
|
51
|
+
const map = compressedMap instanceof Map ? compressedMap : new Map(Object.entries(compressedMap));
|
|
52
|
+
return messages.map((msg) => {
|
|
53
|
+
if (!msg || !Array.isArray(msg.content)) return msg;
|
|
54
|
+
|
|
55
|
+
const newContent = msg.content.map((block) => {
|
|
56
|
+
if (block.type !== "tool_result") return block;
|
|
57
|
+
|
|
58
|
+
const id = block.tool_use_id || block.id || "";
|
|
59
|
+
if (!map.has(id)) return block;
|
|
60
|
+
|
|
61
|
+
const compressed = map.get(id);
|
|
62
|
+
return replaceText(block, compressed);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { ...msg, content: newContent };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── helpers ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a map of tool_use id → tool name by scanning assistant messages
|
|
73
|
+
* that precede or are at the given turn index.
|
|
74
|
+
*/
|
|
75
|
+
function buildToolNameMap(messages, upToIndex) {
|
|
76
|
+
const map = new Map();
|
|
77
|
+
for (let i = 0; i <= upToIndex; i++) {
|
|
78
|
+
const msg = messages[i];
|
|
79
|
+
if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
80
|
+
for (const block of msg.content) {
|
|
81
|
+
if (block.type === "tool_use" && block.id) {
|
|
82
|
+
map.set(block.id, block.name || "unknown");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return map;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract plain text from a tool_result block.
|
|
91
|
+
* Content can be a string, an array of {type:"text", text} blocks, or absent.
|
|
92
|
+
*/
|
|
93
|
+
function extractText(block) {
|
|
94
|
+
if (typeof block.content === "string") return block.content;
|
|
95
|
+
if (Array.isArray(block.content)) {
|
|
96
|
+
return block.content
|
|
97
|
+
.filter((c) => c.type === "text")
|
|
98
|
+
.map((c) => c.text)
|
|
99
|
+
.join("\n");
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Return a new tool_result block with text replaced.
|
|
106
|
+
* Preserves the content shape (string vs array).
|
|
107
|
+
*/
|
|
108
|
+
function replaceText(block, newText) {
|
|
109
|
+
if (typeof block.content === "string") {
|
|
110
|
+
return { ...block, content: newText };
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(block.content)) {
|
|
113
|
+
// Replace only text blocks, keep images etc.
|
|
114
|
+
const newContent = block.content.map((c) =>
|
|
115
|
+
c.type === "text" ? { ...c, text: newText } : c,
|
|
116
|
+
);
|
|
117
|
+
// If there were text blocks, replace the first and remove the rest
|
|
118
|
+
let seenText = false;
|
|
119
|
+
const filtered = newContent.filter((c) => {
|
|
120
|
+
if (c.type !== "text") return true;
|
|
121
|
+
if (!seenText) {
|
|
122
|
+
seenText = true;
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
});
|
|
127
|
+
return { ...block, content: filtered };
|
|
128
|
+
}
|
|
129
|
+
// No content field — add it as string
|
|
130
|
+
return { ...block, content: newText };
|
|
131
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini message format adapter.
|
|
3
|
+
*
|
|
4
|
+
* Gemini API uses `contents[]` where each content has `parts[]`.
|
|
5
|
+
* Function responses appear as parts with a `functionResponse` field:
|
|
6
|
+
* `{ functionResponse: { name: "...", response: { content: "..." } } }`
|
|
7
|
+
*
|
|
8
|
+
* Gemini does not use explicit IDs for function calls/responses. Instead,
|
|
9
|
+
* the function name serves as the correlation key. When multiple calls to
|
|
10
|
+
* the same function exist, we generate synthetic IDs combining turnIndex
|
|
11
|
+
* and part index for disambiguation.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract tool results from Gemini-format messages (contents array).
|
|
18
|
+
*
|
|
19
|
+
* @param {Array<object>} messages - Gemini contents array
|
|
20
|
+
* @returns {Array<{id: string, toolName: string, text: string, turnIndex: number}>}
|
|
21
|
+
*/
|
|
22
|
+
export function extractToolResults(messages) {
|
|
23
|
+
const results = [];
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < messages.length; i++) {
|
|
26
|
+
const content = messages[i];
|
|
27
|
+
if (!content || !Array.isArray(content.parts)) continue;
|
|
28
|
+
|
|
29
|
+
for (let p = 0; p < content.parts.length; p++) {
|
|
30
|
+
const part = content.parts[p];
|
|
31
|
+
if (!part.functionResponse) continue;
|
|
32
|
+
|
|
33
|
+
const fr = part.functionResponse;
|
|
34
|
+
const toolName = fr.name || "unknown";
|
|
35
|
+
const text = extractResponseText(fr.response);
|
|
36
|
+
// Synthetic ID: turn index + part index (Gemini has no explicit IDs)
|
|
37
|
+
const id = `gemini-${i}-${p}`;
|
|
38
|
+
|
|
39
|
+
results.push({ id, toolName, text, turnIndex: i });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rebuild messages replacing functionResponse content with compressed versions.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array<object>} messages - Original Gemini contents array
|
|
50
|
+
* @param {Map<string, string>|Record<string, string>} compressedMap - id → compressed text
|
|
51
|
+
* @returns {Array<object>} Deep-cloned messages with replacements applied
|
|
52
|
+
*/
|
|
53
|
+
export function rebuildMessages(messages, compressedMap) {
|
|
54
|
+
const map = compressedMap instanceof Map ? compressedMap : new Map(Object.entries(compressedMap));
|
|
55
|
+
|
|
56
|
+
return messages.map((content, i) => {
|
|
57
|
+
if (!content || !Array.isArray(content.parts)) return content;
|
|
58
|
+
|
|
59
|
+
const newParts = content.parts.map((part, p) => {
|
|
60
|
+
if (!part.functionResponse) return part;
|
|
61
|
+
|
|
62
|
+
const id = `gemini-${i}-${p}`;
|
|
63
|
+
if (!map.has(id)) return part;
|
|
64
|
+
|
|
65
|
+
const compressed = map.get(id);
|
|
66
|
+
return {
|
|
67
|
+
...part,
|
|
68
|
+
functionResponse: {
|
|
69
|
+
...part.functionResponse,
|
|
70
|
+
response: replaceResponseText(part.functionResponse.response, compressed),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return { ...content, parts: newParts };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── helpers ──────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract text from a Gemini function response object.
|
|
83
|
+
* The response can have `content` (string), `result` (string),
|
|
84
|
+
* or be a nested object that we JSON-serialize.
|
|
85
|
+
*/
|
|
86
|
+
function extractResponseText(response) {
|
|
87
|
+
if (!response) return "";
|
|
88
|
+
if (typeof response === "string") return response;
|
|
89
|
+
if (typeof response.content === "string") return response.content;
|
|
90
|
+
if (typeof response.result === "string") return response.result;
|
|
91
|
+
// For structured responses, serialize to JSON
|
|
92
|
+
return JSON.stringify(response);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Replace text in a Gemini function response object.
|
|
97
|
+
* Preserves the original shape (content field, result field, etc.).
|
|
98
|
+
*/
|
|
99
|
+
function replaceResponseText(response, newText) {
|
|
100
|
+
if (!response) return { content: newText };
|
|
101
|
+
if (typeof response === "string") return newText;
|
|
102
|
+
if (typeof response.content === "string") return { ...response, content: newText };
|
|
103
|
+
if (typeof response.result === "string") return { ...response, result: newText };
|
|
104
|
+
// Default: replace with content field
|
|
105
|
+
return { content: newText };
|
|
106
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider adapter registry.
|
|
3
|
+
*
|
|
4
|
+
* Each adapter implements the contract:
|
|
5
|
+
* extractToolResults(messages) → [{id, toolName, text, turnIndex}]
|
|
6
|
+
* rebuildMessages(messages, compressedMap) → messages
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as anthropic from "./anthropic.js";
|
|
12
|
+
import * as openai from "./openai.js";
|
|
13
|
+
import * as gemini from "./gemini.js";
|
|
14
|
+
|
|
15
|
+
const adapters = {
|
|
16
|
+
anthropic,
|
|
17
|
+
openai,
|
|
18
|
+
gemini,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hostname → provider mapping.
|
|
23
|
+
* @type {Record<string, string>}
|
|
24
|
+
*/
|
|
25
|
+
const HOST_MAP = {
|
|
26
|
+
"api.anthropic.com": "anthropic",
|
|
27
|
+
"api.openai.com": "openai",
|
|
28
|
+
"generativelanguage.googleapis.com": "gemini",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the adapter for a given provider name.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} provider - "anthropic" | "openai" | "gemini"
|
|
35
|
+
* @returns {object|null} Adapter with extractToolResults and rebuildMessages, or null
|
|
36
|
+
*/
|
|
37
|
+
export function getAdapter(provider) {
|
|
38
|
+
return adapters[provider] || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Detect the provider from a hostname.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} hostname - e.g. "api.anthropic.com", "api.openai.com:443"
|
|
45
|
+
* @returns {string} Provider name or "unknown"
|
|
46
|
+
*/
|
|
47
|
+
export function detectProvider(hostname) {
|
|
48
|
+
// Strip port if present
|
|
49
|
+
const host = (hostname || "").split(":")[0];
|
|
50
|
+
return HOST_MAP[host] || "unknown";
|
|
51
|
+
}
|