karajan-code 1.18.0 → 1.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -2,6 +2,7 @@ import { ClaudeAgent } from "./claude-agent.js";
2
2
  import { CodexAgent } from "./codex-agent.js";
3
3
  import { GeminiAgent } from "./gemini-agent.js";
4
4
  import { AiderAgent } from "./aider-agent.js";
5
+ import { OpenCodeAgent } from "./opencode-agent.js";
5
6
 
6
7
  const agentRegistry = new Map();
7
8
 
@@ -38,3 +39,4 @@ registerAgent("claude", ClaudeAgent, { bin: "claude", installUrl: "https://docs.
38
39
  registerAgent("codex", CodexAgent, { bin: "codex", installUrl: "https://developers.openai.com/codex/cli" });
39
40
  registerAgent("gemini", GeminiAgent, { bin: "gemini", installUrl: "https://github.com/google-gemini/gemini-cli" });
40
41
  registerAgent("aider", AiderAgent, { bin: "aider", installUrl: "https://aider.chat/docs/install.html" });
42
+ registerAgent("opencode", OpenCodeAgent, { bin: "opencode", installUrl: "https://opencode.ai" });
@@ -60,3 +60,4 @@ registerModel("gemini", { provider: "google", pricing: { input_per_million: 1.25
60
60
  registerModel("gemini/pro", { provider: "google", pricing: { input_per_million: 1.25, output_per_million: 5 } });
61
61
  registerModel("gemini/flash", { provider: "google", pricing: { input_per_million: 0.075, output_per_million: 0.3 } });
62
62
  registerModel("aider", { provider: "aider", pricing: { input_per_million: 3, output_per_million: 15 } });
63
+ registerModel("opencode", { provider: "opencode", pricing: { input_per_million: 0, output_per_million: 0 } });
@@ -0,0 +1,33 @@
1
+ import { BaseAgent } from "./base-agent.js";
2
+ import { runCommand } from "../utils/process.js";
3
+ import { resolveBin } from "./resolve-bin.js";
4
+
5
+ export class OpenCodeAgent extends BaseAgent {
6
+ async runTask(task) {
7
+ const role = task.role || "coder";
8
+ const args = ["run"];
9
+ const model = this.getRoleModel(role);
10
+ if (model) args.push("--model", model);
11
+ args.push(task.prompt);
12
+ const res = await runCommand(resolveBin("opencode"), args, {
13
+ onOutput: task.onOutput,
14
+ silenceTimeoutMs: task.silenceTimeoutMs,
15
+ timeout: task.timeoutMs
16
+ });
17
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
18
+ }
19
+
20
+ async reviewTask(task) {
21
+ const role = task.role || "reviewer";
22
+ const args = ["run", "--format", "json"];
23
+ const model = this.getRoleModel(role);
24
+ if (model) args.push("--model", model);
25
+ args.push(task.prompt);
26
+ const res = await runCommand(resolveBin("opencode"), args, {
27
+ onOutput: task.onOutput,
28
+ silenceTimeoutMs: task.silenceTimeoutMs,
29
+ timeout: task.timeoutMs
30
+ });
31
+ return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
32
+ }
33
+ }
package/src/cli.js CHANGED
@@ -18,6 +18,10 @@ import { resumeCommand } from "./commands/resume.js";
18
18
  import { sonarCommand, sonarOpenCommand } from "./commands/sonar.js";
19
19
  import { rolesCommand } from "./commands/roles.js";
20
20
  import { agentsCommand } from "./commands/agents.js";
21
+ import { discoverCommand } from "./commands/discover.js";
22
+ import { triageCommand } from "./commands/triage.js";
23
+ import { researcherCommand } from "./commands/researcher.js";
24
+ import { architectCommand } from "./commands/architect.js";
21
25
 
22
26
  const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
23
27
  const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
@@ -191,6 +195,59 @@ program
191
195
  });
192
196
  });
193
197
 
198
+ program
199
+ .command("discover")
200
+ .description("Analyze task for gaps, ambiguities and missing info")
201
+ .argument("<task>")
202
+ .option("--mode <name>", "Discovery mode: gaps|momtest|wendel|classify|jtbd", "gaps")
203
+ .option("--discover <name>", "Override discover agent")
204
+ .option("--discover-model <name>", "Override discover model")
205
+ .option("--json", "Output raw JSON")
206
+ .action(async (task, flags) => {
207
+ await withConfig("discover", flags, async ({ config, logger }) => {
208
+ await discoverCommand({ task, config, logger, mode: flags.mode, json: flags.json });
209
+ });
210
+ });
211
+
212
+ program
213
+ .command("triage")
214
+ .description("Classify task complexity and recommend pipeline roles")
215
+ .argument("<task>")
216
+ .option("--triage <name>", "Override triage agent")
217
+ .option("--triage-model <name>", "Override triage model")
218
+ .option("--json", "Output raw JSON")
219
+ .action(async (task, flags) => {
220
+ await withConfig("triage", flags, async ({ config, logger }) => {
221
+ await triageCommand({ task, config, logger, json: flags.json });
222
+ });
223
+ });
224
+
225
+ program
226
+ .command("researcher")
227
+ .description("Research codebase for a task (files, patterns, constraints)")
228
+ .argument("<task>")
229
+ .option("--researcher <name>", "Override researcher agent")
230
+ .option("--researcher-model <name>", "Override researcher model")
231
+ .action(async (task, flags) => {
232
+ await withConfig("researcher", flags, async ({ config, logger }) => {
233
+ await researcherCommand({ task, config, logger });
234
+ });
235
+ });
236
+
237
+ program
238
+ .command("architect")
239
+ .description("Design solution architecture (layers, patterns, contracts)")
240
+ .argument("<task>")
241
+ .option("--architect <name>", "Override architect agent")
242
+ .option("--architect-model <name>", "Override architect model")
243
+ .option("--context <text>", "Additional context (e.g. researcher output)")
244
+ .option("--json", "Output raw JSON")
245
+ .action(async (task, flags) => {
246
+ await withConfig("architect", flags, async ({ config, logger }) => {
247
+ await architectCommand({ task, config, logger, context: flags.context, json: flags.json });
248
+ });
249
+ });
250
+
194
251
  program
195
252
  .command("resume")
196
253
  .description("Resume a paused session")
@@ -0,0 +1,90 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+ import { buildArchitectPrompt, parseArchitectOutput } from "../prompts/architect.js";
5
+
6
+ function formatArchitect(result) {
7
+ const lines = [];
8
+ lines.push(`## Architecture Design`);
9
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
10
+
11
+ const arch = result.architecture;
12
+ if (arch) {
13
+ if (arch.type) lines.push(`**Type:** ${arch.type}`, "");
14
+
15
+ if (arch.layers?.length) {
16
+ lines.push("### Layers");
17
+ for (const l of arch.layers) {
18
+ if (typeof l === "string") {
19
+ lines.push(`- ${l}`);
20
+ } else {
21
+ lines.push(`- **${l.name}**: ${l.responsibility || ""}`);
22
+ }
23
+ }
24
+ lines.push("");
25
+ }
26
+
27
+ if (arch.patterns?.length) {
28
+ lines.push("### Patterns");
29
+ for (const p of arch.patterns) lines.push(`- ${p}`);
30
+ lines.push("");
31
+ }
32
+
33
+ if (arch.tradeoffs?.length) {
34
+ lines.push("### Tradeoffs");
35
+ for (const t of arch.tradeoffs) {
36
+ lines.push(`- **${t.decision}**: ${t.rationale || ""}`);
37
+ if (t.alternatives?.length) lines.push(` Alternatives: ${t.alternatives.join(", ")}`);
38
+ }
39
+ lines.push("");
40
+ }
41
+
42
+ if (arch.apiContracts?.length) {
43
+ lines.push("### API Contracts");
44
+ for (const c of arch.apiContracts) {
45
+ lines.push(`- \`${c.method || "GET"} ${c.endpoint}\``);
46
+ }
47
+ lines.push("");
48
+ }
49
+ }
50
+
51
+ if (result.questions?.length) {
52
+ lines.push("### Clarification Questions");
53
+ for (const q of result.questions) {
54
+ lines.push(`- ${q.question || q}`);
55
+ }
56
+ lines.push("");
57
+ }
58
+
59
+ if (result.summary) lines.push(`---\n${result.summary}`);
60
+ return lines.join("\n");
61
+ }
62
+
63
+ export async function architectCommand({ task, config, logger, context, json }) {
64
+ const architectRole = resolveRole(config, "architect");
65
+ await assertAgentsAvailable([architectRole.provider]);
66
+ logger.info(`Architect (${architectRole.provider}) starting...`);
67
+
68
+ const agent = createAgent(architectRole.provider, config, logger);
69
+ const prompt = buildArchitectPrompt({ task, researchContext: context });
70
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
71
+ const result = await agent.runTask({ prompt, onOutput, role: "architect" });
72
+
73
+ if (!result.ok) {
74
+ throw new Error(result.error || result.output || "Architect failed");
75
+ }
76
+
77
+ const parsed = parseArchitectOutput(result.output);
78
+
79
+ if (json) {
80
+ console.log(JSON.stringify(parsed || result.output, null, 2));
81
+ return;
82
+ }
83
+
84
+ if (parsed?.verdict) {
85
+ console.log(formatArchitect(parsed));
86
+ } else {
87
+ console.log(result.output);
88
+ }
89
+ logger.info("Architect completed.");
90
+ }
@@ -0,0 +1,87 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+ import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
5
+ import { parseMaybeJsonString } from "../review/parser.js";
6
+
7
+ function formatDiscover(result, mode) {
8
+ const lines = [];
9
+ lines.push(`## Discovery (${mode})`);
10
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
11
+
12
+ if (result.gaps?.length) {
13
+ lines.push("## Gaps");
14
+ for (const g of result.gaps) {
15
+ const sev = g.severity ? ` [${g.severity}]` : "";
16
+ lines.push(`- ${g.description || g}${sev}`);
17
+ if (g.suggestedQuestion) lines.push(` → ${g.suggestedQuestion}`);
18
+ }
19
+ lines.push("");
20
+ }
21
+
22
+ if (result.momTestQuestions?.length) {
23
+ lines.push("## Mom Test Questions");
24
+ for (const q of result.momTestQuestions) {
25
+ lines.push(`- ${q.question || q}`);
26
+ if (q.rationale) lines.push(` _${q.rationale}_`);
27
+ }
28
+ lines.push("");
29
+ }
30
+
31
+ if (result.wendelChecklist?.length) {
32
+ lines.push("## Wendel Checklist");
33
+ for (const w of result.wendelChecklist) {
34
+ const icon = w.status === "pass" ? "✓" : w.status === "fail" ? "✗" : "?";
35
+ lines.push(`- [${icon}] ${w.condition}: ${w.justification || ""}`);
36
+ }
37
+ lines.push("");
38
+ }
39
+
40
+ if (result.classification) {
41
+ lines.push("## Classification");
42
+ lines.push(`- Type: ${result.classification.type}`);
43
+ if (result.classification.adoptionRisk) lines.push(`- Adoption risk: ${result.classification.adoptionRisk}`);
44
+ if (result.classification.frictionEstimate) lines.push(`- Friction: ${result.classification.frictionEstimate}`);
45
+ lines.push("");
46
+ }
47
+
48
+ if (result.jtbds?.length) {
49
+ lines.push("## Jobs-to-be-Done");
50
+ for (const j of result.jtbds) {
51
+ lines.push(`- **${j.id || ""}**: ${j.functional || j}`);
52
+ }
53
+ lines.push("");
54
+ }
55
+
56
+ if (result.summary) lines.push(`---\n${result.summary}`);
57
+ return lines.join("\n");
58
+ }
59
+
60
+ export async function discoverCommand({ task, config, logger, mode, json }) {
61
+ const discoverRole = resolveRole(config, "discover");
62
+ await assertAgentsAvailable([discoverRole.provider]);
63
+ logger.info(`Discover (${discoverRole.provider}) starting — mode: ${mode || "gaps"}...`);
64
+
65
+ const agent = createAgent(discoverRole.provider, config, logger);
66
+ const prompt = buildDiscoverPrompt({ task, mode: mode || "gaps" });
67
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
68
+ const result = await agent.runTask({ prompt, onOutput, role: "discover" });
69
+
70
+ if (!result.ok) {
71
+ throw new Error(result.error || result.output || "Discover failed");
72
+ }
73
+
74
+ const parsed = parseDiscoverOutput(result.output);
75
+
76
+ if (json) {
77
+ console.log(JSON.stringify(parsed || result.output, null, 2));
78
+ return;
79
+ }
80
+
81
+ if (parsed?.verdict) {
82
+ console.log(formatDiscover(parsed, mode || "gaps"));
83
+ } else {
84
+ console.log(result.output);
85
+ }
86
+ logger.info("Discover completed.");
87
+ }
@@ -0,0 +1,40 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+
5
+ const SUBAGENT_PREAMBLE = [
6
+ "IMPORTANT: You are running as a Karajan sub-agent.",
7
+ "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
8
+ "Do NOT use any MCP tools. Focus only on researching the codebase."
9
+ ].join(" ");
10
+
11
+ function buildResearchPrompt(task) {
12
+ return [
13
+ SUBAGENT_PREAMBLE,
14
+ "Investigate the codebase for the following task.",
15
+ "Identify affected files, patterns, constraints, prior decisions, risks, and test coverage.",
16
+ "Return a single valid JSON object with your findings and nothing else.",
17
+ '{"affected_files":[string],"patterns":[string],"constraints":[string],"prior_decisions":[string],"risks":[string],"test_coverage":string}',
18
+ `## Task\n${task}`
19
+ ].join("\n\n");
20
+ }
21
+
22
+ export async function researcherCommand({ task, config, logger }) {
23
+ const researcherRole = resolveRole(config, "researcher");
24
+ await assertAgentsAvailable([researcherRole.provider]);
25
+ logger.info(`Researcher (${researcherRole.provider}) starting...`);
26
+
27
+ const agent = createAgent(researcherRole.provider, config, logger);
28
+ const prompt = buildResearchPrompt(task);
29
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
30
+ const result = await agent.runTask({ prompt, onOutput, role: "researcher" });
31
+
32
+ if (!result.ok) {
33
+ throw new Error(result.error || result.output || "Researcher failed");
34
+ }
35
+
36
+ if (result.output) {
37
+ console.log(result.output);
38
+ }
39
+ logger.info("Researcher completed.");
40
+ }
@@ -0,0 +1,63 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+ import { buildTriagePrompt } from "../prompts/triage.js";
5
+ import { parseMaybeJsonString } from "../review/parser.js";
6
+
7
+ function formatTriage(result) {
8
+ const lines = [];
9
+ lines.push(`## Triage Result`);
10
+ lines.push(`- **Level:** ${result.level || "unknown"}`);
11
+ if (result.taskType) lines.push(`- **Task type:** ${result.taskType}`);
12
+ if (result.reasoning) lines.push(`- **Reasoning:** ${result.reasoning}`);
13
+ lines.push("");
14
+
15
+ if (result.roles?.length) {
16
+ lines.push("### Recommended Roles");
17
+ for (const r of result.roles) {
18
+ lines.push(`- ${r}`);
19
+ }
20
+ lines.push("");
21
+ }
22
+
23
+ if (result.shouldDecompose) {
24
+ lines.push("### Decomposition Suggested");
25
+ if (result.subtasks?.length) {
26
+ for (const s of result.subtasks) {
27
+ lines.push(`- ${typeof s === "string" ? s : s.title || s}`);
28
+ }
29
+ }
30
+ lines.push("");
31
+ }
32
+
33
+ return lines.join("\n");
34
+ }
35
+
36
+ export async function triageCommand({ task, config, logger, json }) {
37
+ const triageRole = resolveRole(config, "triage");
38
+ await assertAgentsAvailable([triageRole.provider]);
39
+ logger.info(`Triage (${triageRole.provider}) starting...`);
40
+
41
+ const agent = createAgent(triageRole.provider, config, logger);
42
+ const prompt = buildTriagePrompt({ task });
43
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
44
+ const result = await agent.runTask({ prompt, onOutput, role: "triage" });
45
+
46
+ if (!result.ok) {
47
+ throw new Error(result.error || result.output || "Triage failed");
48
+ }
49
+
50
+ const parsed = parseMaybeJsonString(result.output);
51
+
52
+ if (json) {
53
+ console.log(JSON.stringify(parsed || result.output, null, 2));
54
+ return;
55
+ }
56
+
57
+ if (parsed?.level) {
58
+ console.log(formatTriage(parsed));
59
+ } else {
60
+ console.log(result.output);
61
+ }
62
+ logger.info("Triage completed.");
63
+ }
@@ -5,7 +5,8 @@ const KNOWN_AGENTS = [
5
5
  { name: "claude", install: "npm install -g @anthropic-ai/claude-code" },
6
6
  { name: "codex", install: "npm install -g @openai/codex" },
7
7
  { name: "gemini", install: "npm install -g @anthropic-ai/gemini-code (or check Gemini CLI docs)" },
8
- { name: "aider", install: "pip install aider-chat" }
8
+ { name: "aider", install: "pip install aider-chat" },
9
+ { name: "opencode", install: "curl -fsSL https://opencode.ai/install | bash (or see https://opencode.ai)" }
9
10
  ];
10
11
 
11
12
  export async function checkBinary(name, versionArg = "--version") {