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,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration initialization and pipeline setup helpers.
|
|
3
|
+
* Extracted from orchestrator.js — pure functions, no orchestration state.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { computeBaseRef } from "../review/diff-generator.js";
|
|
8
|
+
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
9
|
+
import { buildReviewerPrompt } from "../prompts/reviewer.js";
|
|
10
|
+
import { resolveRole } from "../config.js";
|
|
11
|
+
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
12
|
+
import { BudgetTracker, extractUsageMetrics } from "../utils/budget.js";
|
|
13
|
+
import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
14
|
+
import { applyPolicies } from "../guards/policy-resolver.js";
|
|
15
|
+
import { resolveReviewProfile } from "../review/profiles.js";
|
|
16
|
+
import { createSession } from "../session-store.js";
|
|
17
|
+
import { exists, ensureDir } from "../utils/fs.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Auto-initialize .karajan/ in projectDir if missing.
|
|
21
|
+
* Copies essential templates (coder-rules, review-rules) without running the full wizard.
|
|
22
|
+
* Called by the orchestrator before the pipeline starts.
|
|
23
|
+
*/
|
|
24
|
+
export async function autoInit(projectDir, logger) {
|
|
25
|
+
// Ensure git repo exists — without git, diff/reviewer/commit won't work
|
|
26
|
+
const gitDir = path.join(projectDir, ".git");
|
|
27
|
+
if (!(await exists(gitDir))) {
|
|
28
|
+
const { execSync } = await import("node:child_process");
|
|
29
|
+
try {
|
|
30
|
+
execSync("git init", { cwd: projectDir, stdio: "pipe" });
|
|
31
|
+
execSync("git commit --allow-empty -m 'initial commit'", { cwd: projectDir, stdio: "pipe" });
|
|
32
|
+
logger.info("Initialized git repository with empty initial commit");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.warn(`Failed to init git repo: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure .gitignore exists with universal entries only (stack-specific added after planner)
|
|
39
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
40
|
+
const universalIgnores = [".env", "*.log", ".DS_Store", ".karajan/", ".reviews/"];
|
|
41
|
+
try {
|
|
42
|
+
let content = "";
|
|
43
|
+
if (await exists(gitignorePath)) {
|
|
44
|
+
content = await fs.readFile(gitignorePath, "utf8");
|
|
45
|
+
}
|
|
46
|
+
const missing = universalIgnores.filter(entry => !content.includes(entry));
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
const append = (content && !content.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
|
|
49
|
+
await fs.appendFile(gitignorePath, append, "utf8");
|
|
50
|
+
logger.info(`Created .gitignore with universal entries`);
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.warn(`Failed to create .gitignore: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const karajanDir = path.join(projectDir, ".karajan");
|
|
57
|
+
if (await exists(karajanDir)) return;
|
|
58
|
+
|
|
59
|
+
logger.info("No .karajan/ found — auto-initializing project scaffolding");
|
|
60
|
+
await ensureDir(karajanDir);
|
|
61
|
+
|
|
62
|
+
const templatesDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..", "templates");
|
|
63
|
+
|
|
64
|
+
const filesToCopy = [
|
|
65
|
+
{ src: "coder-rules.md", dest: "coder-rules.md" },
|
|
66
|
+
{ src: "review-rules.md", dest: "review-rules.md" }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const { src, dest } of filesToCopy) {
|
|
70
|
+
const srcPath = path.join(templatesDir, src);
|
|
71
|
+
const destPath = path.join(karajanDir, dest);
|
|
72
|
+
try {
|
|
73
|
+
if (!(await exists(destPath))) {
|
|
74
|
+
const content = await fs.readFile(srcPath, "utf8");
|
|
75
|
+
await fs.writeFile(destPath, content, "utf8");
|
|
76
|
+
logger.info(` Created .karajan/${dest}`);
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logger.warn(` Failed to copy ${src}: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Copy role templates directory
|
|
84
|
+
const rolesTemplateDir = path.join(templatesDir, "roles");
|
|
85
|
+
const rolesDestDir = path.join(karajanDir, "roles");
|
|
86
|
+
try {
|
|
87
|
+
if (await exists(rolesTemplateDir)) {
|
|
88
|
+
await ensureDir(rolesDestDir);
|
|
89
|
+
const roleFiles = await fs.readdir(rolesTemplateDir);
|
|
90
|
+
for (const rf of roleFiles) {
|
|
91
|
+
if (!rf.endsWith(".md")) continue;
|
|
92
|
+
const destFile = path.join(rolesDestDir, rf);
|
|
93
|
+
if (!(await exists(destFile))) {
|
|
94
|
+
await fs.copyFile(path.join(rolesTemplateDir, rf), destFile);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
logger.info(` Copied ${roleFiles.filter(f => f.endsWith(".md")).length} role templates to .karajan/roles/`);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
logger.warn(` Failed to copy role templates: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Stack-specific .gitignore patterns keyed by language/framework
|
|
105
|
+
const STACK_GITIGNORE = {
|
|
106
|
+
javascript: ["node_modules/", "dist/", "build/", "coverage/", ".cache/", "*.tsbuildinfo"],
|
|
107
|
+
typescript: ["node_modules/", "dist/", "build/", "coverage/", ".cache/", "*.tsbuildinfo"],
|
|
108
|
+
python: ["__pycache__/", "*.pyc", ".venv/", "venv/", "*.egg-info/", ".pytest_cache/", "htmlcov/", ".mypy_cache/"],
|
|
109
|
+
java: ["target/", "*.class", "*.jar", "*.war", ".gradle/", "build/", ".settings/", ".classpath", ".project"],
|
|
110
|
+
kotlin: ["target/", "*.class", "build/", ".gradle/", ".kotlin/"],
|
|
111
|
+
go: ["bin/", "*.exe", "vendor/"],
|
|
112
|
+
rust: ["target/", "Cargo.lock"],
|
|
113
|
+
ruby: ["vendor/bundle/", ".bundle/", "coverage/", "tmp/"],
|
|
114
|
+
php: ["vendor/", ".phpunit.result.cache", "storage/logs/"],
|
|
115
|
+
csharp: ["bin/", "obj/", "*.suo", "*.user", "packages/", ".vs/"],
|
|
116
|
+
swift: [".build/", "Packages/", "*.xcodeproj/", "DerivedData/"],
|
|
117
|
+
dart: [".dart_tool/", "build/", ".packages"],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update .gitignore with stack-specific entries after planner/architect decides the stack.
|
|
122
|
+
* Detects stack from: triage taskType, architect output, planner output, or task keywords.
|
|
123
|
+
*/
|
|
124
|
+
export async function updateGitignoreForStack(projectDir, { stageResults, task, logger }) {
|
|
125
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
126
|
+
const detected = new Set();
|
|
127
|
+
|
|
128
|
+
// From architect — if it chose layers/patterns, it may hint at the language
|
|
129
|
+
const arch = stageResults?.architect?.architecture;
|
|
130
|
+
if (arch) {
|
|
131
|
+
const archText = JSON.stringify(arch).toLowerCase();
|
|
132
|
+
for (const lang of Object.keys(STACK_GITIGNORE)) {
|
|
133
|
+
if (archText.includes(lang)) detected.add(lang);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// From planner — scan plan text for language keywords
|
|
138
|
+
const planText = (stageResults?.planner?.plan || "").toLowerCase();
|
|
139
|
+
// From task description
|
|
140
|
+
const taskText = (task || "").toLowerCase();
|
|
141
|
+
const combined = `${planText} ${taskText}`;
|
|
142
|
+
|
|
143
|
+
const langKeywords = {
|
|
144
|
+
javascript: ["node", "npm", "express", "react", "vue", "next", "vite", "vitest", "jest", "pnpm", "yarn", "javascript", "js"],
|
|
145
|
+
typescript: ["typescript", "tsx", "tsc"],
|
|
146
|
+
python: ["python", "django", "flask", "fastapi", "pip", "pytest", "poetry"],
|
|
147
|
+
java: ["java", "spring", "maven", "gradle", "junit"],
|
|
148
|
+
kotlin: ["kotlin", "ktor"],
|
|
149
|
+
go: ["golang", "go mod", "gin", "fiber"],
|
|
150
|
+
rust: ["rust", "cargo", "tokio"],
|
|
151
|
+
ruby: ["ruby", "rails", "gem", "bundler", "rspec"],
|
|
152
|
+
php: ["php", "laravel", "composer", "symfony"],
|
|
153
|
+
csharp: ["c#", "csharp", "dotnet", ".net", "aspnet"],
|
|
154
|
+
swift: ["swift", "swiftui", "vapor"],
|
|
155
|
+
dart: ["dart", "flutter"],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
for (const [lang, keywords] of Object.entries(langKeywords)) {
|
|
159
|
+
if (keywords.some(kw => combined.includes(kw))) detected.add(lang);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (detected.size === 0) return;
|
|
163
|
+
|
|
164
|
+
// Collect all entries for detected stacks
|
|
165
|
+
const entries = [];
|
|
166
|
+
for (const lang of detected) {
|
|
167
|
+
entries.push(...(STACK_GITIGNORE[lang] || []));
|
|
168
|
+
}
|
|
169
|
+
const unique = [...new Set(entries)];
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
let content = "";
|
|
173
|
+
if (await exists(gitignorePath)) {
|
|
174
|
+
content = await fs.readFile(gitignorePath, "utf8");
|
|
175
|
+
}
|
|
176
|
+
const missing = unique.filter(entry => !content.includes(entry));
|
|
177
|
+
if (missing.length > 0) {
|
|
178
|
+
const header = `\n# ${[...detected].join(" + ")} project\n`;
|
|
179
|
+
const append = header + missing.join("\n") + "\n";
|
|
180
|
+
await fs.appendFile(gitignorePath, append, "utf8");
|
|
181
|
+
logger.info(`Updated .gitignore for ${[...detected].join(" + ")}: ${missing.join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
logger.warn(`Failed to update .gitignore for stack: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Load product context from well-known file locations.
|
|
190
|
+
*/
|
|
191
|
+
export async function loadProductContext(projectDir) {
|
|
192
|
+
const base = projectDir || process.cwd();
|
|
193
|
+
const candidates = [
|
|
194
|
+
path.join(base, ".karajan", "context.md"),
|
|
195
|
+
path.join(base, "product-vision.md")
|
|
196
|
+
];
|
|
197
|
+
for (const file of candidates) {
|
|
198
|
+
try {
|
|
199
|
+
const content = await fs.readFile(file, "utf8");
|
|
200
|
+
return { content, source: file };
|
|
201
|
+
} catch { /* not found, try next */ }
|
|
202
|
+
}
|
|
203
|
+
return { content: null, source: null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function resolvePipelineFlags(config) {
|
|
207
|
+
return {
|
|
208
|
+
plannerEnabled: Boolean(config.pipeline?.planner?.enabled),
|
|
209
|
+
refactorerEnabled: Boolean(config.pipeline?.refactorer?.enabled),
|
|
210
|
+
researcherEnabled: Boolean(config.pipeline?.researcher?.enabled),
|
|
211
|
+
testerEnabled: Boolean(config.pipeline?.tester?.enabled),
|
|
212
|
+
securityEnabled: Boolean(config.pipeline?.security?.enabled),
|
|
213
|
+
impeccableEnabled: Boolean(config.pipeline?.impeccable?.enabled),
|
|
214
|
+
reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
|
|
215
|
+
discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
|
|
216
|
+
architectEnabled: Boolean(config.pipeline?.architect?.enabled),
|
|
217
|
+
huReviewerEnabled: Boolean(config.pipeline?.hu_reviewer?.enabled),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
|
|
222
|
+
const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, impeccableEnabled, reviewerEnabled, discoverEnabled, architectEnabled, huReviewerEnabled } = pipelineFlags;
|
|
223
|
+
const plannerRole = resolveRole(config, "planner");
|
|
224
|
+
const coderRole = resolveRole(config, "coder");
|
|
225
|
+
const reviewerRole = resolveRole(config, "reviewer");
|
|
226
|
+
const refactorerRole = resolveRole(config, "refactorer");
|
|
227
|
+
const triageEnabled = true;
|
|
228
|
+
|
|
229
|
+
const dryRunPolicies = applyPolicies({
|
|
230
|
+
taskType: flags.taskType || config.taskType || null,
|
|
231
|
+
policies: config.policies,
|
|
232
|
+
});
|
|
233
|
+
const projectDir = config.projectDir || process.cwd();
|
|
234
|
+
const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
|
|
235
|
+
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
|
236
|
+
const coderPrompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), productContext: config.productContext || null });
|
|
237
|
+
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 });
|
|
238
|
+
|
|
239
|
+
const summary = {
|
|
240
|
+
dry_run: true,
|
|
241
|
+
task,
|
|
242
|
+
policies: dryRunPolicies,
|
|
243
|
+
roles: { planner: plannerRole, coder: coderRole, reviewer: reviewerRole, refactorer: refactorerRole },
|
|
244
|
+
pipeline: {
|
|
245
|
+
discover_enabled: discoverEnabled,
|
|
246
|
+
architect_enabled: architectEnabled,
|
|
247
|
+
triage_enabled: triageEnabled,
|
|
248
|
+
planner_enabled: plannerEnabled,
|
|
249
|
+
refactorer_enabled: refactorerEnabled,
|
|
250
|
+
sonar_enabled: Boolean(config.sonarqube?.enabled),
|
|
251
|
+
reviewer_enabled: reviewerEnabled,
|
|
252
|
+
researcher_enabled: researcherEnabled,
|
|
253
|
+
tester_enabled: testerEnabled,
|
|
254
|
+
security_enabled: securityEnabled,
|
|
255
|
+
impeccable_enabled: impeccableEnabled,
|
|
256
|
+
solomon_enabled: Boolean(config.pipeline?.solomon?.enabled),
|
|
257
|
+
hu_reviewer_enabled: huReviewerEnabled
|
|
258
|
+
},
|
|
259
|
+
limits: {
|
|
260
|
+
max_iterations: config.max_iterations,
|
|
261
|
+
max_iteration_minutes: config.session?.max_iteration_minutes,
|
|
262
|
+
max_total_minutes: config.session?.max_total_minutes,
|
|
263
|
+
max_sonar_retries: config.session?.max_sonar_retries,
|
|
264
|
+
max_reviewer_retries: config.session?.max_reviewer_retries,
|
|
265
|
+
max_tester_retries: config.session?.max_tester_retries,
|
|
266
|
+
max_security_retries: config.session?.max_security_retries
|
|
267
|
+
},
|
|
268
|
+
prompts: { coder: coderPrompt, reviewer: reviewerPrompt },
|
|
269
|
+
git: config.git
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
emitProgress(
|
|
273
|
+
emitter,
|
|
274
|
+
makeEvent("dry-run:summary", { sessionId: null, iteration: 0, stage: "dry-run", startedAt: Date.now() }, {
|
|
275
|
+
message: "Dry-run complete — no changes made",
|
|
276
|
+
detail: summary
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
return summary;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function createBudgetManager({ config, emitter, eventBase }) {
|
|
284
|
+
const budgetTracker = new BudgetTracker({ pricing: config?.budget?.pricing });
|
|
285
|
+
const budgetLimit = Number(config?.max_budget_usd);
|
|
286
|
+
const hasBudgetLimit = Number.isFinite(budgetLimit) && budgetLimit >= 0;
|
|
287
|
+
const warnThresholdPct = Number(config?.budget?.warn_threshold_pct ?? 80);
|
|
288
|
+
let stageCounter = 0;
|
|
289
|
+
|
|
290
|
+
function budgetSummary() {
|
|
291
|
+
const s = budgetTracker.summary();
|
|
292
|
+
s.trace = budgetTracker.trace();
|
|
293
|
+
return s;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function trackBudget({ role, provider, model, result, duration_ms, promptSize }) {
|
|
297
|
+
const enrichedResult = promptSize && result ? { ...result, promptSize } : result;
|
|
298
|
+
const metrics = extractUsageMetrics(enrichedResult, model);
|
|
299
|
+
budgetTracker.record({ role, provider, ...metrics, duration_ms, stage_index: stageCounter++ });
|
|
300
|
+
|
|
301
|
+
if (!hasBudgetLimit) return;
|
|
302
|
+
const totalCost = budgetTracker.total().cost_usd;
|
|
303
|
+
const pctUsed = budgetLimit === 0 ? 100 : (totalCost / budgetLimit) * 100;
|
|
304
|
+
const warnOrOk = pctUsed >= warnThresholdPct ? "paused" : "ok";
|
|
305
|
+
const status = totalCost > budgetLimit ? "fail" : warnOrOk;
|
|
306
|
+
emitProgress(
|
|
307
|
+
emitter,
|
|
308
|
+
makeEvent("budget:update", { ...eventBase, stage: role }, {
|
|
309
|
+
status,
|
|
310
|
+
message: `Budget: $${totalCost.toFixed(2)} / $${budgetLimit.toFixed(2)}`,
|
|
311
|
+
detail: {
|
|
312
|
+
...budgetSummary(),
|
|
313
|
+
max_budget_usd: budgetLimit,
|
|
314
|
+
warn_threshold_pct: warnThresholdPct,
|
|
315
|
+
pct_used: Number(pctUsed.toFixed(2)),
|
|
316
|
+
remaining_usd: budgetTracker.remaining(budgetLimit),
|
|
317
|
+
executorType: "system"
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { budgetTracker, budgetLimit, budgetSummary, trackBudget };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function initializeSession({ task, config, flags, pgTaskId, pgProject }) {
|
|
327
|
+
const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
|
|
328
|
+
|
|
329
|
+
if (baseRef === "__snapshot__") {
|
|
330
|
+
const { takeSnapshot } = await import("../review/snapshot-diff.js");
|
|
331
|
+
const { setSnapshot } = await import("../review/diff-generator.js");
|
|
332
|
+
const snapshot = await takeSnapshot(config.projectDir || process.cwd());
|
|
333
|
+
setSnapshot(snapshot);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const sessionInit = {
|
|
337
|
+
task,
|
|
338
|
+
config_snapshot: config,
|
|
339
|
+
base_ref: baseRef,
|
|
340
|
+
session_start_sha: baseRef,
|
|
341
|
+
last_reviewer_feedback: null,
|
|
342
|
+
repeated_issue_count: 0,
|
|
343
|
+
sonar_retry_count: 0,
|
|
344
|
+
reviewer_retry_count: 0,
|
|
345
|
+
standby_retry_count: 0,
|
|
346
|
+
last_sonar_issue_signature: null,
|
|
347
|
+
sonar_repeat_count: 0,
|
|
348
|
+
last_reviewer_issue_signature: null,
|
|
349
|
+
reviewer_repeat_count: 0,
|
|
350
|
+
deferred_issues: []
|
|
351
|
+
};
|
|
352
|
+
if (pgTaskId) sessionInit.pg_task_id = pgTaskId;
|
|
353
|
+
if (pgProject) sessionInit.pg_project_id = pgProject;
|
|
354
|
+
return createSession(sessionInit);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function applyTriageOverrides(pipelineFlags, roleOverrides) {
|
|
358
|
+
const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled", "impeccableEnabled"];
|
|
359
|
+
for (const key of keys) {
|
|
360
|
+
if (roleOverrides[key] !== undefined) {
|
|
361
|
+
pipelineFlags[key] = roleOverrides[key];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const SIMPLE_LEVELS = new Set(["trivial", "simple"]);
|
|
367
|
+
|
|
368
|
+
export function applyAutoSimplify({ pipelineFlags, triageLevel, config, flags, logger, emitter, eventBase }) {
|
|
369
|
+
if (!config.pipeline?.auto_simplify) return false;
|
|
370
|
+
if (!triageLevel || !SIMPLE_LEVELS.has(triageLevel)) return false;
|
|
371
|
+
if (flags.mode) return false;
|
|
372
|
+
if (flags.enableReviewer !== undefined || flags.enableTester !== undefined) return false;
|
|
373
|
+
|
|
374
|
+
pipelineFlags.reviewerEnabled = false;
|
|
375
|
+
pipelineFlags.testerEnabled = false;
|
|
376
|
+
|
|
377
|
+
const disabledRoles = ["reviewer", "tester"];
|
|
378
|
+
logger.info(`Simple task (${triageLevel}) — lightweight pipeline (disabled: ${disabledRoles.join(", ")})`);
|
|
379
|
+
emitProgress(
|
|
380
|
+
emitter,
|
|
381
|
+
makeEvent("pipeline:simplify", { ...eventBase, stage: "triage" }, {
|
|
382
|
+
message: `Simple task (${triageLevel}) — lightweight pipeline`,
|
|
383
|
+
detail: { level: triageLevel, disabledRoles }
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function applyFlagOverrides(pipelineFlags, flags) {
|
|
390
|
+
if (flags.enablePlanner !== undefined) pipelineFlags.plannerEnabled = Boolean(flags.enablePlanner);
|
|
391
|
+
if (flags.enableResearcher !== undefined) pipelineFlags.researcherEnabled = Boolean(flags.enableResearcher);
|
|
392
|
+
if (flags.enableArchitect !== undefined) pipelineFlags.architectEnabled = Boolean(flags.enableArchitect);
|
|
393
|
+
if (flags.enableRefactorer !== undefined) pipelineFlags.refactorerEnabled = Boolean(flags.enableRefactorer);
|
|
394
|
+
if (flags.enableReviewer !== undefined) pipelineFlags.reviewerEnabled = Boolean(flags.enableReviewer);
|
|
395
|
+
if (flags.enableTester !== undefined) pipelineFlags.testerEnabled = Boolean(flags.enableTester);
|
|
396
|
+
if (flags.enableSecurity !== undefined) pipelineFlags.securityEnabled = Boolean(flags.enableSecurity);
|
|
397
|
+
if (flags.enableImpeccable !== undefined) pipelineFlags.impeccableEnabled = Boolean(flags.enableImpeccable);
|
|
398
|
+
|
|
399
|
+
if (flags.design) {
|
|
400
|
+
pipelineFlags.impeccableEnabled = true;
|
|
401
|
+
pipelineFlags.impeccableMode = "refactoring";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
|
|
406
|
+
const resolvedPolicies = applyPolicies({
|
|
407
|
+
taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || stageResults.intent?.taskType || null,
|
|
408
|
+
policies: config.policies,
|
|
409
|
+
});
|
|
410
|
+
session.resolved_policies = resolvedPolicies;
|
|
411
|
+
|
|
412
|
+
let updatedConfig = config;
|
|
413
|
+
if (!resolvedPolicies.tdd) {
|
|
414
|
+
updatedConfig = { ...updatedConfig, development: { ...updatedConfig.development, methodology: "standard", require_test_changes: false } };
|
|
415
|
+
}
|
|
416
|
+
if (!resolvedPolicies.sonar) {
|
|
417
|
+
updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
|
|
418
|
+
}
|
|
419
|
+
if (!resolvedPolicies.reviewer) {
|
|
420
|
+
pipelineFlags.reviewerEnabled = false;
|
|
421
|
+
}
|
|
422
|
+
if (resolvedPolicies.coderRequired === false) {
|
|
423
|
+
pipelineFlags.coderRequired = false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
emitProgress(
|
|
427
|
+
emitter,
|
|
428
|
+
makeEvent("policies:resolved", eventBase, {
|
|
429
|
+
message: `Policies resolved for taskType="${resolvedPolicies.taskType}"`,
|
|
430
|
+
detail: resolvedPolicies
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
return updatedConfig;
|
|
435
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Direct actions: commands Karajan Brain can execute without invoking a full role.
|
|
2
|
+
// Keeps the action catalog small, auditable, and safe.
|
|
3
|
+
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} DirectAction
|
|
10
|
+
* @property {string} type - action type identifier
|
|
11
|
+
* @property {Object} params - action-specific parameters
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} ActionResult
|
|
16
|
+
* @property {boolean} ok
|
|
17
|
+
* @property {string} [output]
|
|
18
|
+
* @property {string} [error]
|
|
19
|
+
* @property {string} action
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const ALLOWED_COMMANDS = [
|
|
23
|
+
"npm install", "npm ci", "pnpm install", "yarn install",
|
|
24
|
+
"pip install -r requirements.txt", "poetry install",
|
|
25
|
+
"go mod download", "cargo fetch",
|
|
26
|
+
"bundle install", "composer install", "dotnet restore"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate that a shell command is in the allow-list.
|
|
31
|
+
* Prevents arbitrary command execution.
|
|
32
|
+
*/
|
|
33
|
+
function isCommandAllowed(cmd) {
|
|
34
|
+
if (!cmd || typeof cmd !== "string") return false;
|
|
35
|
+
return ALLOWED_COMMANDS.some(allowed => cmd.trim().startsWith(allowed));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute an allowed shell command (install deps, etc.).
|
|
40
|
+
*/
|
|
41
|
+
async function runCommand({ cmd, cwd }) {
|
|
42
|
+
if (!isCommandAllowed(cmd)) {
|
|
43
|
+
return { ok: false, error: `Command not in allow-list: ${cmd}`, action: "run_command" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const output = execSync(cmd, {
|
|
48
|
+
cwd: cwd || process.cwd(),
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
51
|
+
timeout: 120_000
|
|
52
|
+
});
|
|
53
|
+
return { ok: true, output: output.trim(), action: "run_command" };
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return { ok: false, error: err.message, action: "run_command" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a file with given content (only if it doesn't exist).
|
|
61
|
+
*/
|
|
62
|
+
async function createFile({ filePath, content, cwd, overwrite = false }) {
|
|
63
|
+
try {
|
|
64
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd || process.cwd(), filePath);
|
|
65
|
+
|
|
66
|
+
// Path traversal guard
|
|
67
|
+
const resolved = path.resolve(fullPath);
|
|
68
|
+
const base = path.resolve(cwd || process.cwd());
|
|
69
|
+
if (!resolved.startsWith(base)) {
|
|
70
|
+
return { ok: false, error: "Path traversal denied", action: "create_file" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await fs.access(fullPath);
|
|
75
|
+
if (!overwrite) {
|
|
76
|
+
return { ok: false, error: `File already exists: ${filePath}`, action: "create_file" };
|
|
77
|
+
}
|
|
78
|
+
} catch { /* file doesn't exist, proceed */ }
|
|
79
|
+
|
|
80
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
81
|
+
await fs.writeFile(fullPath, content || "", "utf8");
|
|
82
|
+
return { ok: true, output: `Created ${filePath}`, action: "create_file" };
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return { ok: false, error: err.message, action: "create_file" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Append entries to .gitignore (creates if missing, skips duplicates).
|
|
90
|
+
*/
|
|
91
|
+
async function updateGitignore({ entries, cwd }) {
|
|
92
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
93
|
+
return { ok: false, error: "No entries provided", action: "update_gitignore" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const gitignorePath = path.join(cwd || process.cwd(), ".gitignore");
|
|
98
|
+
let content = "";
|
|
99
|
+
try {
|
|
100
|
+
content = await fs.readFile(gitignorePath, "utf8");
|
|
101
|
+
} catch { /* will create */ }
|
|
102
|
+
|
|
103
|
+
const missing = entries.filter(e => !content.includes(e));
|
|
104
|
+
if (missing.length === 0) {
|
|
105
|
+
return { ok: true, output: "All entries already present", action: "update_gitignore" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const append = (content && !content.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
|
|
109
|
+
await fs.appendFile(gitignorePath, append, "utf8");
|
|
110
|
+
return { ok: true, output: `Added: ${missing.join(", ")}`, action: "update_gitignore" };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return { ok: false, error: err.message, action: "update_gitignore" };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stage files with git add.
|
|
118
|
+
*/
|
|
119
|
+
async function gitAdd({ files, cwd }) {
|
|
120
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
121
|
+
return { ok: false, error: "No files to add", action: "git_add" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Only allow relative paths (no absolute paths, no shell metacharacters)
|
|
126
|
+
for (const f of files) {
|
|
127
|
+
if (typeof f !== "string" || f.includes("..") || /[;&|`$]/.test(f)) {
|
|
128
|
+
return { ok: false, error: `Invalid file path: ${f}`, action: "git_add" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const args = files.map(f => `"${f}"`).join(" ");
|
|
132
|
+
execSync(`git add ${args}`, {
|
|
133
|
+
cwd: cwd || process.cwd(),
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
136
|
+
});
|
|
137
|
+
return { ok: true, output: `Staged ${files.length} file(s)`, action: "git_add" };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return { ok: false, error: err.message, action: "git_add" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ACTION_HANDLERS = {
|
|
144
|
+
run_command: runCommand,
|
|
145
|
+
create_file: createFile,
|
|
146
|
+
update_gitignore: updateGitignore,
|
|
147
|
+
git_add: gitAdd
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Execute a single direct action.
|
|
152
|
+
*/
|
|
153
|
+
export async function executeAction(action, { cwd } = {}) {
|
|
154
|
+
if (!action || typeof action !== "object") {
|
|
155
|
+
return { ok: false, error: "Invalid action", action: "unknown" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const handler = ACTION_HANDLERS[action.type];
|
|
159
|
+
if (!handler) {
|
|
160
|
+
return { ok: false, error: `Unknown action type: ${action.type}`, action: action.type };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return handler({ ...action.params, cwd: cwd || action.params?.cwd });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Execute a list of actions in sequence. Stops on first failure unless continueOnError.
|
|
168
|
+
*/
|
|
169
|
+
export async function executeActions(actions, { cwd, continueOnError = false } = {}) {
|
|
170
|
+
const results = [];
|
|
171
|
+
for (const action of actions || []) {
|
|
172
|
+
const result = await executeAction(action, { cwd });
|
|
173
|
+
results.push(result);
|
|
174
|
+
if (!result.ok && !continueOnError) break;
|
|
175
|
+
}
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get list of allowed action types.
|
|
181
|
+
*/
|
|
182
|
+
export function getAllowedActionTypes() {
|
|
183
|
+
return Object.keys(ACTION_HANDLERS);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export { ALLOWED_COMMANDS, isCommandAllowed };
|