karajan-code 2.5.0 → 2.6.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/README.md +39 -4
- package/package.json +5 -2
- package/src/agents/aider-agent.js +1 -2
- package/src/agents/base-agent.js +63 -1
- package/src/agents/claude-agent.js +3 -4
- package/src/agents/codex-agent.js +1 -2
- package/src/agents/gemini-agent.js +1 -2
- package/src/agents/index.js +11 -2
- package/src/agents/opencode-agent.js +1 -2
- package/src/brain/decision-tracker.js +123 -0
- package/src/brain/decisor.js +243 -0
- package/src/brain/solomon-consult.js +171 -0
- package/src/budget/comparison.js +96 -0
- package/src/checks/binaries.js +123 -0
- package/src/checks/ci.js +105 -0
- package/src/checks/config-files.js +217 -0
- package/src/checks/dir-setup.js +63 -0
- package/src/checks/mcp-health.js +157 -0
- package/src/checks/node.js +61 -0
- package/src/checks/ports.js +110 -0
- package/src/checks/rtk.js +36 -0
- package/src/checks/runner.js +267 -0
- package/src/checks/skills.js +49 -0
- package/src/checks/sonar.js +67 -0
- package/src/checks/system.js +53 -0
- package/src/checks/tokens.js +96 -0
- package/src/checks/types.js +105 -0
- package/src/cli.js +29 -3
- package/src/commands/board.js +23 -0
- package/src/commands/doctor.js +130 -362
- package/src/commands/resume.js +1 -1
- package/src/commands/run.js +2 -1
- package/src/commands/skills.js +49 -0
- package/src/config/schema.js +222 -0
- package/src/config.js +32 -3
- package/src/guards/policy-resolver.js +8 -0
- package/src/hu/store.js +9 -0
- package/src/infrastructure/command-runner.js +44 -0
- package/src/infrastructure/environment.js +38 -0
- package/src/infrastructure/file-system-service.js +63 -0
- package/src/infrastructure/mocks.js +182 -0
- package/src/mcp/handlers/management-handlers.js +7 -2
- package/src/mcp/tools.js +10 -3
- package/src/orchestrator/config-init.js +16 -1
- package/src/orchestrator/flow-runner.js +2085 -0
- package/src/orchestrator/pipeline-context.js +17 -0
- package/src/orchestrator/preflight-checks.js +78 -4
- package/src/orchestrator/solomon-escalation.js +55 -0
- package/src/orchestrator/stages/stage-executor.js +155 -0
- package/src/orchestrator.js +19 -1923
- package/src/prompts/architect.js +2 -2
- package/src/prompts/coder.js +2 -2
- package/src/prompts/prompt-resolver.js +127 -0
- package/src/prompts/reviewer.js +2 -2
- package/src/roles/agent-role.js +9 -0
- package/src/roles/architect-role.js +3 -1
- package/src/roles/base-role.js +40 -2
- package/src/roles/coder-role.js +3 -1
- package/src/roles/security-role.js +12 -0
- package/src/roles/tester-role.js +11 -0
- package/src/session/journal/decisions-writer.js +200 -0
- package/src/session/journal/iteration-logger.js +194 -0
- package/src/session/journal/summary-writer.js +176 -0
- package/src/session/journal/tree-writer.js +164 -0
- package/src/skills/semantic-detector.js +147 -0
- package/src/skills/skill-detector.js +186 -8
- package/src/skills/skill-loader.js +131 -1
- package/src/skills/skills-cache.js +128 -0
- package/src/types/agent.js +55 -0
- package/src/types/config.js +114 -0
- package/src/types/hu.js +47 -0
- package/src/types/index.js +38 -0
- package/src/types/policy.js +43 -0
- package/src/types/session.js +74 -0
- package/src/types/solomon.js +87 -0
- package/src/types/stage.js +68 -0
- package/src/utils/display/event-handlers.js +389 -0
- package/src/utils/display/formatters.js +132 -0
- package/src/utils/display/header.js +48 -0
- package/src/utils/display/session-summary.js +108 -0
- package/src/utils/display/solomon.js +42 -0
- package/src/utils/port-check.js +45 -0
- package/src/utils/port-occupant.js +95 -0
- package/src/utils/provider-env.js +116 -0
- package/src/utils/with-timeout.js +42 -0
- package/templates/roles/architect/anthropic.md +65 -0
- package/templates/roles/architect/default.md +63 -0
- package/templates/roles/architect/google.md +52 -0
- package/templates/roles/architect/openai.md +51 -0
- package/templates/roles/architect/opencode.md +50 -0
- package/templates/roles/coder/anthropic.md +72 -0
- package/templates/roles/coder/default.md +69 -0
- package/templates/roles/coder/google.md +78 -0
- package/templates/roles/coder/openai.md +59 -0
- package/templates/roles/coder/opencode.md +49 -0
- package/templates/roles/planner/anthropic.md +50 -0
- package/templates/roles/planner/default.md +48 -0
- package/templates/roles/planner/google.md +66 -0
- package/templates/roles/planner/openai.md +46 -0
- package/templates/roles/planner/opencode.md +53 -0
- package/templates/roles/reviewer/anthropic.md +61 -0
- package/templates/roles/reviewer/default.md +62 -0
- package/templates/roles/reviewer/google.md +63 -0
- package/templates/roles/reviewer/openai.md +57 -0
- package/templates/roles/reviewer/opencode.md +42 -0
- package/templates/roles/solomon/anthropic.md +71 -0
- package/templates/roles/solomon/default.md +83 -0
- package/templates/roles/solomon/google.md +61 -0
- package/templates/roles/solomon/openai.md +53 -0
- package/templates/roles/solomon/opencode.md +55 -0
- package/src/utils/display.js +0 -669
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/karajan-
|
|
2
|
+
<img src="docs/karajan-orbit.svg" alt="Karajan Code" width="220">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Karajan Code</h1>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> **v2.
|
|
26
|
+
> **v2.6.0 released** — Modular orchestrator + infrastructure DI + Valibot config validation. `src/orchestrator.js` split into a thin barrel + `flow-runner.js` + new `StageExecutor` contract under `src/orchestrator/stages/`. `FileSystemService` and `CommandRunner` adapters let tests swap in mocks instead of spawning real subprocesses. Config now validated with Valibot (catches `review_mode` typos, `max_iterations: 0`, bad ports at load time). HU Board auto-start gate simplified to `hu_board.auto_start` alone with a prominent cyan URL banner at pipeline init. Plus: budget comparison (With-KJ vs Without-KJ), rich session journal (decisions / iterations / summary / tree), Brain decisor with intent-driven routing, and Karajan-wide JSDoc typedefs. See [CHANGELOG.md](./CHANGELOG.md) for full history and [MIGRATION-v2.md](./MIGRATION-v2.md) for v1→v2 breaking changes.
|
|
27
27
|
|
|
28
28
|
You describe what you want to build. Karajan orchestrates multiple AI agents to plan it, implement it, test it, review it with SonarQube, and iterate. No babysitting required.
|
|
29
29
|
|
|
@@ -53,6 +53,25 @@ Use Karajan when you want:
|
|
|
53
53
|
|
|
54
54
|
If Claude Code is a smart pair programmer, Karajan is the CI/CD pipeline for AI-assisted development. They work great together: Karajan is designed to be used as an MCP server inside Claude Code.
|
|
55
55
|
|
|
56
|
+
## How Karajan differs from AI frameworks
|
|
57
|
+
|
|
58
|
+
While Genkit, Mastra, LangChain and Vercel AI SDK call `/v1/messages`, Karajan orchestrates the AI CLIs your developers already use in their terminals.
|
|
59
|
+
|
|
60
|
+
| Axis | Karajan | Genkit / Mastra / LangChain / Vercel AI SDK |
|
|
61
|
+
|------|---------|---------------------------------------------|
|
|
62
|
+
| Calls provider HTTP API (`/v1/messages`, etc.) | ❌ Delegates to CLIs | ✅ |
|
|
63
|
+
| Orchestrates existing AI CLIs (claude, codex, gemini, aider, opencode) as subprocesses | ✅ | ❌ |
|
|
64
|
+
| Depends on cloud infrastructure | ❌ Fully local | ⚠️ Varies |
|
|
65
|
+
| Vanilla JS (no TypeScript required) | ✅ | ⚠️ TS-first |
|
|
66
|
+
| Token billing | **Uses your existing CLI subscriptions** | Pay per API call |
|
|
67
|
+
|
|
68
|
+
Two technical facts worth keeping straight:
|
|
69
|
+
|
|
70
|
+
1. **Subprocess, not PTY.** Karajan spawns each CLI via `execa` / `child_process` with plain `stdin` / `stdout` / `stderr` — see `src/infrastructure/command-runner.js` and `src/agents/*.js`. There is no PTY emulation.
|
|
71
|
+
2. **Fresh subprocess per invocation + state on disk.** Every coder run is a new process; the state lives in `~/.karajan/sessions/` (see `src/session-store.js`) and the per-session journal under `.reviews/<session-id>/`. This is what makes pipelines **reproducible** and **resumable** with `kj resume`.
|
|
72
|
+
|
|
73
|
+
Full write-up with mental mapping for Genkit / Mastra / LangChain / Vercel AI SDK developers: **[docs/COMPARISON.md](./docs/COMPARISON.md)**.
|
|
74
|
+
|
|
56
75
|
## Install
|
|
57
76
|
|
|
58
77
|
**npm** (recommended):
|
|
@@ -107,7 +126,23 @@ kj run "Create a utility function that validates Spanish DNI numbers, with tests
|
|
|
107
126
|
kj code "Add input validation to the signup form" # Coder only
|
|
108
127
|
kj review "Check the authentication changes" # Review current diff
|
|
109
128
|
kj audit "Full health analysis of this codebase" # Read-only audit
|
|
110
|
-
|
|
129
|
+
|
|
130
|
+
# Planning workflow (v2.5+)
|
|
131
|
+
kj plan "Refactor the database layer" # Generate plan + HUs
|
|
132
|
+
kj plan list # List plans for this project
|
|
133
|
+
kj plan show <planId> # Show plan details + HU table
|
|
134
|
+
kj plan validate <planId> # Check structure and deps
|
|
135
|
+
kj plan ready <planId> # Certify all HUs, mark ready
|
|
136
|
+
kj plan add-hu <planId> --title "..." --type feat # Add HU to plan
|
|
137
|
+
kj plan remove-hu <planId> <huId> # Remove HU from plan
|
|
138
|
+
kj plan delete <planId> # Delete plan from disk
|
|
139
|
+
kj run --plan <planId> "task" # Execute an approved plan
|
|
140
|
+
|
|
141
|
+
# HU Board dashboard (v1.34.0+)
|
|
142
|
+
kj board start # Start web dashboard (port 4000)
|
|
143
|
+
kj board open # Start + open in browser
|
|
144
|
+
kj board status # Check if running
|
|
145
|
+
kj board stop # Stop the board
|
|
111
146
|
```
|
|
112
147
|
|
|
113
148
|
### 2. MCP: inside your AI agent
|
|
@@ -218,7 +253,7 @@ After `npm install -g karajan-code`, the MCP server auto-registers in Claude and
|
|
|
218
253
|
# command = "karajan-mcp"
|
|
219
254
|
```
|
|
220
255
|
|
|
221
|
-
**
|
|
256
|
+
**24 tools** available: `kj_run`, `kj_code`, `kj_review`, `kj_plan`, `kj_board`, `kj_audit`, `kj_scan`, `kj_doctor`, `kj_config`, `kj_report`, `kj_resume`, `kj_roles`, `kj_agents`, `kj_preflight`, `kj_status`, `kj_init`, `kj_discover`, `kj_triage`, `kj_researcher`, `kj_architect`, `kj_impeccable`, `kj_hu`, `kj_skills`, `kj_suggest`.
|
|
222
257
|
|
|
223
258
|
Use `kj-tail` in a separate terminal to see what the pipeline is doing in real time (see [Three ways to use Karajan](#three-ways-to-use-karajan)).
|
|
224
259
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
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",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"test": "vitest run",
|
|
48
48
|
"test:watch": "vitest",
|
|
49
49
|
"lint": "((command -v rg >/dev/null 2>&1 && rg --files src tests | rg '\\.js$') || find src tests -type f -name '*.js') | while IFS= read -r f; do node --check \"$f\"; done",
|
|
50
|
+
"typecheck": "npx -p typescript@5 tsc --noEmit -p tsconfig.json",
|
|
50
51
|
"validate": "npm run lint && npm test",
|
|
51
52
|
"mcp": "node src/mcp/server.js"
|
|
52
53
|
},
|
|
@@ -54,9 +55,11 @@
|
|
|
54
55
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
55
56
|
"commander": "^14.0.0",
|
|
56
57
|
"execa": "^9.6.0",
|
|
57
|
-
"js-yaml": "^4.1.0"
|
|
58
|
+
"js-yaml": "^4.1.0",
|
|
59
|
+
"valibot": "^1.3.1"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
62
|
+
"@changesets/cli": "^2.30.0",
|
|
60
63
|
"@vitest/coverage-v8": "^4.0.18",
|
|
61
64
|
"vitest": "^4.0.0"
|
|
62
65
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
|
-
import { runCommand } from "../utils/process.js";
|
|
3
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
3
|
|
|
5
4
|
export class AiderAgent extends BaseAgent {
|
|
@@ -28,7 +27,7 @@ export class AiderAgent extends BaseAgent {
|
|
|
28
27
|
async _exec(task, model) {
|
|
29
28
|
const args = ["--yes", "--message", task.prompt];
|
|
30
29
|
if (model) args.push("--model", model);
|
|
31
|
-
const res = await runCommand(resolveBin("aider"), args, {
|
|
30
|
+
const res = await this.runCommand(resolveBin("aider"), args, {
|
|
32
31
|
onOutput: task.onOutput,
|
|
33
32
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
34
33
|
timeout: task.timeoutMs
|
package/src/agents/base-agent.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for provider-specific agent adapters (claude, codex, gemini, ...).
|
|
3
|
+
* Every concrete agent must implement runTask() and may override reviewTask().
|
|
4
|
+
*
|
|
5
|
+
* @typedef {import("../types/config.js").KarajanConfig} KarajanConfig
|
|
6
|
+
* @typedef {import("../types/stage.js").Logger} Logger
|
|
7
|
+
* @typedef {import("../types/agent.js").AgentResult} AgentResult
|
|
8
|
+
* @typedef {import("../infrastructure/environment.js").Environment} Environment
|
|
9
|
+
* @typedef {import("../infrastructure/command-runner.js").CommandRunResult} CommandRunResult
|
|
10
|
+
* @typedef {import("../infrastructure/command-runner.js").CommandRunOptions} CommandRunOptions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { defaultEnvironment } from "../infrastructure/environment.js";
|
|
14
|
+
|
|
1
15
|
const MODEL_NOT_SUPPORTED_PATTERNS = [
|
|
2
16
|
/model.{0,30}is not supported/i,
|
|
3
17
|
/model.{0,30}not available/i,
|
|
@@ -8,20 +22,58 @@ const MODEL_NOT_SUPPORTED_PATTERNS = [
|
|
|
8
22
|
];
|
|
9
23
|
|
|
10
24
|
export class BaseAgent {
|
|
11
|
-
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} name - agent slug (claude|codex|gemini|aider|opencode)
|
|
27
|
+
* @param {KarajanConfig} config
|
|
28
|
+
* @param {Logger} logger
|
|
29
|
+
* @param {Environment} [environment] - DI-friendly fs + runner bundle. Defaults
|
|
30
|
+
* to the native filesystem and execa-based
|
|
31
|
+
* command runner. Tests pass a mock env to
|
|
32
|
+
* unit-test agent paths without spawning
|
|
33
|
+
* real processes.
|
|
34
|
+
*/
|
|
35
|
+
constructor(name, config, logger, environment = defaultEnvironment) {
|
|
12
36
|
this.name = name;
|
|
13
37
|
this.config = config;
|
|
14
38
|
this.logger = logger;
|
|
39
|
+
this.environment = environment;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a shell command through the injected CommandRunner. Subclasses should
|
|
44
|
+
* call this instead of importing runCommand directly so tests can swap in
|
|
45
|
+
* MockCommandRunner.
|
|
46
|
+
* @param {string} command
|
|
47
|
+
* @param {string[]} [args]
|
|
48
|
+
* @param {CommandRunOptions} [options]
|
|
49
|
+
* @returns {Promise<CommandRunResult>}
|
|
50
|
+
*/
|
|
51
|
+
async runCommand(command, args = [], options = {}) {
|
|
52
|
+
return this.environment.runner.run(command, args, options);
|
|
15
53
|
}
|
|
16
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Execute a coding/agentic task. Subclasses MUST override.
|
|
57
|
+
* @param {string|Object} _task
|
|
58
|
+
* @returns {Promise<AgentResult>}
|
|
59
|
+
*/
|
|
17
60
|
async runTask(_task) {
|
|
18
61
|
throw new Error("runTask not implemented");
|
|
19
62
|
}
|
|
20
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Execute a review task. Subclasses MAY override for different behaviour.
|
|
66
|
+
* @param {string|Object} _task
|
|
67
|
+
* @returns {Promise<AgentResult>}
|
|
68
|
+
*/
|
|
21
69
|
async reviewTask(_task) {
|
|
22
70
|
throw new Error("reviewTask not implemented");
|
|
23
71
|
}
|
|
24
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} role
|
|
75
|
+
* @returns {string|null}
|
|
76
|
+
*/
|
|
25
77
|
getRoleModel(role) {
|
|
26
78
|
const roleModel = this.config?.roles?.[role]?.model;
|
|
27
79
|
if (roleModel) return roleModel;
|
|
@@ -29,11 +81,21 @@ export class BaseAgent {
|
|
|
29
81
|
return this.config?.coder_options?.model || null;
|
|
30
82
|
}
|
|
31
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} role
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
32
88
|
isAutoApproveEnabled(role) {
|
|
33
89
|
if (role === "reviewer") return false;
|
|
34
90
|
return Boolean(this.config?.coder_options?.auto_approve);
|
|
35
91
|
}
|
|
36
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Heuristic: does the agent's error look like "model not supported"?
|
|
95
|
+
* Used by the retry/fallback path.
|
|
96
|
+
* @param {Partial<AgentResult>} result
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
37
99
|
isModelNotSupportedError(result) {
|
|
38
100
|
const text = [result?.error, result?.output, result?.stderr, result?.stdout]
|
|
39
101
|
.filter(Boolean).join("\n");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
|
-
import { runCommand } from "../utils/process.js";
|
|
3
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -244,7 +243,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
244
243
|
if (task.onOutput) {
|
|
245
244
|
args.push("--output-format", "stream-json", "--verbose");
|
|
246
245
|
const streamFilter = createStreamJsonFilter(task.onOutput);
|
|
247
|
-
const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
|
246
|
+
const res = await this.runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
|
248
247
|
onOutput: streamFilter,
|
|
249
248
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
250
249
|
timeout: task.timeoutMs
|
|
@@ -257,7 +256,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
257
256
|
|
|
258
257
|
// Without streaming, use json output to get structured response via stderr
|
|
259
258
|
args.push("--output-format", "json");
|
|
260
|
-
const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts());
|
|
259
|
+
const res = await this.runCommand(resolveBin("claude"), args, cleanExecaOpts());
|
|
261
260
|
const raw = pickOutput(res);
|
|
262
261
|
const output = extractTextFromStreamJson(raw);
|
|
263
262
|
const usage = extractUsageFromStreamJson(raw);
|
|
@@ -267,7 +266,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
267
266
|
async _reviewTaskExec(task, model) {
|
|
268
267
|
const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS, "--output-format", "stream-json", "--verbose"];
|
|
269
268
|
if (model) args.push("--model", model);
|
|
270
|
-
const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
|
269
|
+
const res = await this.runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
|
271
270
|
onOutput: task.onOutput,
|
|
272
271
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
273
272
|
timeout: task.timeoutMs
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
|
-
import { runCommand } from "../utils/process.js";
|
|
3
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -45,7 +44,7 @@ export class CodexAgent extends BaseAgent {
|
|
|
45
44
|
if (model) args.push("--model", model);
|
|
46
45
|
if (role !== "reviewer" && this.isAutoApproveEnabled(role)) args.push("--full-auto");
|
|
47
46
|
args.push("-");
|
|
48
|
-
const res = await runCommand(resolveBin("codex"), args, {
|
|
47
|
+
const res = await this.runCommand(resolveBin("codex"), args, {
|
|
49
48
|
onOutput: task.onOutput,
|
|
50
49
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
51
50
|
timeout: task.timeoutMs,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
|
-
import { runCommand } from "../utils/process.js";
|
|
3
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
3
|
|
|
5
4
|
export class GeminiAgent extends BaseAgent {
|
|
@@ -29,7 +28,7 @@ export class GeminiAgent extends BaseAgent {
|
|
|
29
28
|
const args = ["-p", task.prompt];
|
|
30
29
|
if (mode === "review") args.push("--output-format", "json");
|
|
31
30
|
if (model) args.push("--model", model);
|
|
32
|
-
const res = await runCommand(resolveBin("gemini"), args, {
|
|
31
|
+
const res = await this.runCommand(resolveBin("gemini"), args, {
|
|
33
32
|
onOutput: task.onOutput,
|
|
34
33
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
35
34
|
timeout: task.timeoutMs
|
package/src/agents/index.js
CHANGED
|
@@ -26,12 +26,21 @@ export function getAgentMeta(name) {
|
|
|
26
26
|
return entry ? { ...entry.meta } : null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} agentName
|
|
31
|
+
* @param {import("../types/config.js").KarajanConfig} config
|
|
32
|
+
* @param {import("../types/stage.js").Logger} logger
|
|
33
|
+
* @param {import("../infrastructure/environment.js").Environment} [environment]
|
|
34
|
+
* Optional DI environment. Defaults to native fs + execa. Tests pass a mock
|
|
35
|
+
* environment built via `buildMockEnvironment()` to exercise agent paths
|
|
36
|
+
* without spawning real processes.
|
|
37
|
+
*/
|
|
38
|
+
export function createAgent(agentName, config, logger, environment) {
|
|
30
39
|
const entry = agentRegistry.get(agentName);
|
|
31
40
|
if (!entry) {
|
|
32
41
|
throw new Error(`Unsupported agent: ${agentName}`);
|
|
33
42
|
}
|
|
34
|
-
return new entry.AgentClass(agentName, config, logger);
|
|
43
|
+
return new entry.AgentClass(agentName, config, logger, environment);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
// Auto-register built-in agents with CLI metadata
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
|
-
import { runCommand } from "../utils/process.js";
|
|
3
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
3
|
|
|
5
4
|
export class OpenCodeAgent extends BaseAgent {
|
|
@@ -30,7 +29,7 @@ export class OpenCodeAgent extends BaseAgent {
|
|
|
30
29
|
if (jsonFormat) args.push("--format", "json");
|
|
31
30
|
if (model) args.push("--model", model);
|
|
32
31
|
args.push(task.prompt);
|
|
33
|
-
const res = await runCommand(resolveBin("opencode"), args, {
|
|
32
|
+
const res = await this.runCommand(resolveBin("opencode"), args, {
|
|
34
33
|
onOutput: task.onOutput,
|
|
35
34
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
36
35
|
timeout: task.timeoutMs
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks every routing decision the Brain makes during a session and enforces
|
|
3
|
+
* safety limits (max decisions per session, indecision-loop detection).
|
|
4
|
+
*
|
|
5
|
+
* Decisions are appended to `session.brainDecisions`. Consumers call
|
|
6
|
+
* `recordDecision(tracker, decision, context)` after a buildDecision() call
|
|
7
|
+
* and `checkLimits(tracker)` before making another decision.
|
|
8
|
+
*
|
|
9
|
+
* The limits are soft guards, not hard failures: hitting a limit surfaces
|
|
10
|
+
* a status that the orchestrator can use to escalate to Solomon or pause
|
|
11
|
+
* the session for human input.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_MAX_DECISIONS = 20;
|
|
15
|
+
export const INDECISION_REPEAT_THRESHOLD = 3; // 3 identical classifications in a row
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} RecordedDecision
|
|
19
|
+
* @property {number} seq
|
|
20
|
+
* @property {string} at - ISO timestamp
|
|
21
|
+
* @property {string} intent - e.g. "sw:medium" (taskType:level)
|
|
22
|
+
* @property {string[]} rolesOn
|
|
23
|
+
* @property {"low"|"medium"|"high"} confidence
|
|
24
|
+
* @property {string} rationale
|
|
25
|
+
* @property {string[]} appliedOverrides
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} TrackerStatus
|
|
30
|
+
* @property {"ok"|"max-reached"|"indecision"} status
|
|
31
|
+
* @property {number} decisionCount
|
|
32
|
+
* @property {string} [detail]
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize a tracker bound to a session. If the session already has a
|
|
37
|
+
* `brainDecisions` array (e.g. from a resumed session), keep it.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} session
|
|
40
|
+
* @param {{ max?: number }} [options]
|
|
41
|
+
* @returns {{ session: Object, max: number }}
|
|
42
|
+
*/
|
|
43
|
+
export function createTracker(session, { max = DEFAULT_MAX_DECISIONS } = {}) {
|
|
44
|
+
if (!Array.isArray(session.brainDecisions)) {
|
|
45
|
+
session.brainDecisions = [];
|
|
46
|
+
}
|
|
47
|
+
return { session, max };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Append a decision to the tracker.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ session: Object, max: number }} tracker
|
|
54
|
+
* @param {Object} decision - output of buildDecision()
|
|
55
|
+
* @param {{ taskType?: string, level?: string }} intent
|
|
56
|
+
* @returns {RecordedDecision}
|
|
57
|
+
*/
|
|
58
|
+
export function recordDecision(tracker, decision, intent = {}) {
|
|
59
|
+
const seq = tracker.session.brainDecisions.length + 1;
|
|
60
|
+
const entry = {
|
|
61
|
+
seq,
|
|
62
|
+
at: new Date().toISOString(),
|
|
63
|
+
intent: `${intent.taskType || "?"}:${intent.level || "?"}`,
|
|
64
|
+
rolesOn: Array.isArray(decision.rolesOn) ? [...decision.rolesOn] : [],
|
|
65
|
+
confidence: decision.confidence || "medium",
|
|
66
|
+
rationale: decision.rationale || "",
|
|
67
|
+
appliedOverrides: Array.isArray(decision.appliedOverrides) ? [...decision.appliedOverrides] : [],
|
|
68
|
+
};
|
|
69
|
+
tracker.session.brainDecisions.push(entry);
|
|
70
|
+
return entry;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compute status of the tracker. Called BEFORE recording a new decision so
|
|
75
|
+
* the orchestrator can decide to escalate instead of calling the Brain again.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ session: Object, max: number }} tracker
|
|
78
|
+
* @returns {TrackerStatus}
|
|
79
|
+
*/
|
|
80
|
+
export function checkLimits(tracker) {
|
|
81
|
+
const decisions = tracker.session.brainDecisions || [];
|
|
82
|
+
const decisionCount = decisions.length;
|
|
83
|
+
|
|
84
|
+
if (decisionCount >= tracker.max) {
|
|
85
|
+
return {
|
|
86
|
+
status: "max-reached",
|
|
87
|
+
decisionCount,
|
|
88
|
+
detail: `Brain made ${decisionCount} decisions (limit ${tracker.max}). Escalate to Solomon or human.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Indecision loop: last N decisions share the exact same intent.
|
|
93
|
+
const window = decisions.slice(-INDECISION_REPEAT_THRESHOLD);
|
|
94
|
+
if (window.length >= INDECISION_REPEAT_THRESHOLD && window.every((d) => d.intent === window[0].intent)) {
|
|
95
|
+
return {
|
|
96
|
+
status: "indecision",
|
|
97
|
+
decisionCount,
|
|
98
|
+
detail: `Brain classified the same intent (${window[0].intent}) ${INDECISION_REPEAT_THRESHOLD} times in a row — loop detected.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { status: "ok", decisionCount };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build a short summary of all decisions made in the session. Useful for the
|
|
107
|
+
* final report and for debugging.
|
|
108
|
+
*
|
|
109
|
+
* @param {{ session: Object }} tracker
|
|
110
|
+
* @returns {{ total: number, byIntent: Record<string,number>, lastRationale: string }}
|
|
111
|
+
*/
|
|
112
|
+
export function summarize(tracker) {
|
|
113
|
+
const decisions = tracker.session.brainDecisions || [];
|
|
114
|
+
const byIntent = {};
|
|
115
|
+
for (const d of decisions) {
|
|
116
|
+
byIntent[d.intent] = (byIntent[d.intent] || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
total: decisions.length,
|
|
120
|
+
byIntent,
|
|
121
|
+
lastRationale: decisions.at(-1)?.rationale ?? "",
|
|
122
|
+
};
|
|
123
|
+
}
|