karajan-code 2.5.0 → 2.6.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 +20 -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 +163 -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 +21 -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/architect-role.js +3 -1
- package/src/roles/base-role.js +23 -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 +34 -0
- package/src/types/policy.js +43 -0
- package/src/types/session.js +74 -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
|
|
|
@@ -107,7 +107,23 @@ kj run "Create a utility function that validates Spanish DNI numbers, with tests
|
|
|
107
107
|
kj code "Add input validation to the signup form" # Coder only
|
|
108
108
|
kj review "Check the authentication changes" # Review current diff
|
|
109
109
|
kj audit "Full health analysis of this codebase" # Read-only audit
|
|
110
|
-
|
|
110
|
+
|
|
111
|
+
# Planning workflow (v2.5+)
|
|
112
|
+
kj plan "Refactor the database layer" # Generate plan + HUs
|
|
113
|
+
kj plan list # List plans for this project
|
|
114
|
+
kj plan show <planId> # Show plan details + HU table
|
|
115
|
+
kj plan validate <planId> # Check structure and deps
|
|
116
|
+
kj plan ready <planId> # Certify all HUs, mark ready
|
|
117
|
+
kj plan add-hu <planId> --title "..." --type feat # Add HU to plan
|
|
118
|
+
kj plan remove-hu <planId> <huId> # Remove HU from plan
|
|
119
|
+
kj plan delete <planId> # Delete plan from disk
|
|
120
|
+
kj run --plan <planId> "task" # Execute an approved plan
|
|
121
|
+
|
|
122
|
+
# HU Board dashboard (v1.34.0+)
|
|
123
|
+
kj board start # Start web dashboard (port 4000)
|
|
124
|
+
kj board open # Start + open in browser
|
|
125
|
+
kj board status # Check if running
|
|
126
|
+
kj board stop # Stop the board
|
|
111
127
|
```
|
|
112
128
|
|
|
113
129
|
### 2. MCP: inside your AI agent
|
|
@@ -218,7 +234,7 @@ After `npm install -g karajan-code`, the MCP server auto-registers in Claude and
|
|
|
218
234
|
# command = "karajan-mcp"
|
|
219
235
|
```
|
|
220
236
|
|
|
221
|
-
**
|
|
237
|
+
**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
238
|
|
|
223
239
|
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
240
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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",
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain decisor — intent-driven pipeline routing.
|
|
3
|
+
*
|
|
4
|
+
* Takes the triage output + task text + config and produces a Decision that
|
|
5
|
+
* the orchestrator applies as pipelineFlags overrides. The existing stage
|
|
6
|
+
* if/else in runFlow continues to drive execution; Brain just tells it which
|
|
7
|
+
* roles are relevant for this particular task.
|
|
8
|
+
*
|
|
9
|
+
* This is an MVP that keeps the current architecture. A future iteration
|
|
10
|
+
* (Pipeline Sovereignty / KJC-PCS-0022) can promote the Decision into a
|
|
11
|
+
* true DAG with parallel stage execution.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} TriageOutput
|
|
16
|
+
* @property {"trivial"|"simple"|"medium"|"complex"} level
|
|
17
|
+
* @property {string[]} roles - roles triage suggests running
|
|
18
|
+
* @property {string} [taskType] - sw | infra | doc | add-tests | refactor | no-code | audit
|
|
19
|
+
* @property {string} [reasoning]
|
|
20
|
+
* @property {number} [confidence] - 0..1, often absent
|
|
21
|
+
* @property {string[]} [domainHints]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} Decision
|
|
26
|
+
* @property {string[]} rolesOn - roles to enable in pipelineFlags
|
|
27
|
+
* @property {string[]} rolesOff - roles to explicitly disable
|
|
28
|
+
* @property {"low"|"medium"|"high"} confidence
|
|
29
|
+
* @property {string} rationale - human-readable reasoning
|
|
30
|
+
* @property {boolean} consultSolomon - true if the Brain is unsure
|
|
31
|
+
* @property {string} [solomonReason] - why Solomon is needed
|
|
32
|
+
* @property {string[]} appliedOverrides - names of manual overrides respected
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const VALID_ROLES = new Set([
|
|
36
|
+
"planner", "researcher", "architect", "coder", "reviewer", "refactorer",
|
|
37
|
+
"tester", "security", "impeccable", "triage", "discover", "hu_reviewer", "solomon",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Translate triage level → base set of roles. Conservative baseline that the
|
|
42
|
+
* decisor then adjusts via taskType and domain hints.
|
|
43
|
+
*/
|
|
44
|
+
function baselineForLevel(level) {
|
|
45
|
+
switch (level) {
|
|
46
|
+
case "trivial": return new Set(["coder"]);
|
|
47
|
+
case "simple": return new Set(["coder", "reviewer"]);
|
|
48
|
+
case "medium": return new Set(["coder", "reviewer", "tester"]);
|
|
49
|
+
case "complex": return new Set(["researcher", "architect", "planner", "coder", "reviewer", "tester", "security"]);
|
|
50
|
+
default: return new Set(["coder", "reviewer"]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Apply taskType adjustments on top of the baseline set (mutates in place).
|
|
56
|
+
*/
|
|
57
|
+
function applyTaskTypeAdjustments(roles, taskType) {
|
|
58
|
+
switch (taskType) {
|
|
59
|
+
case "doc":
|
|
60
|
+
// Documentation tasks: skip testing and security, keep reviewer for tone/clarity.
|
|
61
|
+
roles.delete("tester");
|
|
62
|
+
roles.delete("security");
|
|
63
|
+
roles.delete("refactorer");
|
|
64
|
+
break;
|
|
65
|
+
case "add-tests":
|
|
66
|
+
// Adding tests: tester is primary, security less relevant.
|
|
67
|
+
roles.add("tester");
|
|
68
|
+
roles.delete("security");
|
|
69
|
+
break;
|
|
70
|
+
case "refactor":
|
|
71
|
+
// Refactor: no new behavior; tester verifies nothing broke; reviewer checks structure.
|
|
72
|
+
roles.add("refactorer");
|
|
73
|
+
roles.add("tester");
|
|
74
|
+
break;
|
|
75
|
+
case "infra":
|
|
76
|
+
// Infra: architect helps; security critical.
|
|
77
|
+
roles.add("architect");
|
|
78
|
+
roles.add("security");
|
|
79
|
+
break;
|
|
80
|
+
case "audit":
|
|
81
|
+
// Audit: no coder; reviewer + security.
|
|
82
|
+
roles.delete("coder");
|
|
83
|
+
roles.add("reviewer");
|
|
84
|
+
roles.add("security");
|
|
85
|
+
break;
|
|
86
|
+
case "no-code":
|
|
87
|
+
// No-code tasks (e.g. running SQL queries): drop coder+reviewer+tester.
|
|
88
|
+
roles.delete("coder");
|
|
89
|
+
roles.delete("reviewer");
|
|
90
|
+
roles.delete("tester");
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Apply explicit overrides (CLI flags / config) on top of the decision.
|
|
97
|
+
* force = add; skip = remove. Tracks what was applied.
|
|
98
|
+
*
|
|
99
|
+
* @param {Set<string>} roles
|
|
100
|
+
* @param {{ forceRoles?: string[], skipRoles?: string[] }} overrides
|
|
101
|
+
* @returns {string[]} applied override names
|
|
102
|
+
*/
|
|
103
|
+
function applyOverrides(roles, overrides) {
|
|
104
|
+
const applied = [];
|
|
105
|
+
for (const r of overrides.forceRoles || []) {
|
|
106
|
+
if (!VALID_ROLES.has(r)) continue;
|
|
107
|
+
roles.add(r);
|
|
108
|
+
applied.push(`force:${r}`);
|
|
109
|
+
}
|
|
110
|
+
for (const r of overrides.skipRoles || []) {
|
|
111
|
+
if (!VALID_ROLES.has(r)) continue;
|
|
112
|
+
roles.delete(r);
|
|
113
|
+
applied.push(`skip:${r}`);
|
|
114
|
+
}
|
|
115
|
+
return applied;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build a routing Decision from triage output + task text + config.
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} args
|
|
122
|
+
* @param {TriageOutput} args.triage
|
|
123
|
+
* @param {string} [args.task]
|
|
124
|
+
* @param {Object} [args.config] - resolved config (for rolesEnabled in pipeline)
|
|
125
|
+
* @param {{ forceRoles?: string[], skipRoles?: string[] }} [args.overrides]
|
|
126
|
+
* @returns {Decision}
|
|
127
|
+
*/
|
|
128
|
+
export function buildDecision({ triage, task, config = {}, overrides = {} } = {}) {
|
|
129
|
+
const level = triage?.level || "simple";
|
|
130
|
+
const taskType = triage?.taskType || "sw";
|
|
131
|
+
|
|
132
|
+
// 1. Baseline by complexity.
|
|
133
|
+
const roles = baselineForLevel(level);
|
|
134
|
+
|
|
135
|
+
// 2. Include triage's own suggested roles (triage already factors domain hints).
|
|
136
|
+
if (Array.isArray(triage?.roles)) {
|
|
137
|
+
for (const r of triage.roles) {
|
|
138
|
+
if (VALID_ROLES.has(r)) roles.add(r);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 3. Task-type-specific adjustments.
|
|
143
|
+
applyTaskTypeAdjustments(roles, taskType);
|
|
144
|
+
|
|
145
|
+
// 4. Config pipeline.X.enabled=false forces a role off (respect user config).
|
|
146
|
+
const pipeline = config?.pipeline || {};
|
|
147
|
+
for (const r of Array.from(roles)) {
|
|
148
|
+
if (pipeline[r]?.enabled === false) {
|
|
149
|
+
roles.delete(r);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 5. Manual CLI/flag overrides (highest priority).
|
|
154
|
+
const appliedOverrides = applyOverrides(roles, overrides);
|
|
155
|
+
|
|
156
|
+
// 6. Invariants: coder is always on unless explicitly audit/no-code AND not force-added.
|
|
157
|
+
if (taskType !== "audit" && taskType !== "no-code" && !overrides.skipRoles?.includes("coder")) {
|
|
158
|
+
roles.add("coder");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 7. Confidence + Solomon consultation.
|
|
162
|
+
const explicitConfidence = typeof triage?.confidence === "number" ? triage.confidence : null;
|
|
163
|
+
let confidence = "medium";
|
|
164
|
+
let consultSolomon = false;
|
|
165
|
+
let solomonReason;
|
|
166
|
+
if (explicitConfidence !== null) {
|
|
167
|
+
if (explicitConfidence < 0.6) {
|
|
168
|
+
confidence = "low";
|
|
169
|
+
consultSolomon = true;
|
|
170
|
+
solomonReason = `triage confidence ${explicitConfidence.toFixed(2)} < 0.6`;
|
|
171
|
+
} else if (explicitConfidence >= 0.85) {
|
|
172
|
+
confidence = "high";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Ambiguous level (absent or off-vocabulary) → low confidence regardless.
|
|
176
|
+
if (!triage?.level) {
|
|
177
|
+
confidence = "low";
|
|
178
|
+
consultSolomon = true;
|
|
179
|
+
solomonReason = "triage returned no level";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const rolesOn = Array.from(roles).sort();
|
|
183
|
+
const rolesOff = Array.from(VALID_ROLES).filter((r) => !roles.has(r)).sort();
|
|
184
|
+
|
|
185
|
+
const rationale = buildRationale({ level, taskType, rolesOn, appliedOverrides, triage });
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
rolesOn,
|
|
189
|
+
rolesOff,
|
|
190
|
+
confidence,
|
|
191
|
+
rationale,
|
|
192
|
+
consultSolomon,
|
|
193
|
+
solomonReason,
|
|
194
|
+
appliedOverrides,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildRationale({ level, taskType, rolesOn, appliedOverrides, triage }) {
|
|
199
|
+
const parts = [
|
|
200
|
+
`level=${level}`,
|
|
201
|
+
`taskType=${taskType}`,
|
|
202
|
+
`roles=[${rolesOn.join(",")}]`,
|
|
203
|
+
];
|
|
204
|
+
if (triage?.domainHints?.length) {
|
|
205
|
+
parts.push(`hints=[${triage.domainHints.slice(0, 3).join(",")}]`);
|
|
206
|
+
}
|
|
207
|
+
if (appliedOverrides.length) {
|
|
208
|
+
parts.push(`overrides=[${appliedOverrides.join(",")}]`);
|
|
209
|
+
}
|
|
210
|
+
return parts.join(" | ");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Apply a Decision onto an existing pipelineFlags object. Returns a NEW
|
|
215
|
+
* object — the input is not mutated. Setting roleEnabled flags via a
|
|
216
|
+
* consistent naming pattern: `${role}Enabled`.
|
|
217
|
+
*
|
|
218
|
+
* Unknown roles are silently ignored; the orchestrator only consumes a
|
|
219
|
+
* fixed set of *Enabled flags.
|
|
220
|
+
*
|
|
221
|
+
* @param {Decision} decision
|
|
222
|
+
* @param {Object} pipelineFlags
|
|
223
|
+
* @returns {Object} new flags object
|
|
224
|
+
*/
|
|
225
|
+
export function applyDecisionToFlags(decision, pipelineFlags = {}) {
|
|
226
|
+
const next = { ...pipelineFlags };
|
|
227
|
+
const flagFor = (role) => {
|
|
228
|
+
if (role === "hu_reviewer") return "huReviewerEnabled";
|
|
229
|
+
return `${role}Enabled`;
|
|
230
|
+
};
|
|
231
|
+
for (const role of decision.rolesOn) {
|
|
232
|
+
next[flagFor(role)] = true;
|
|
233
|
+
}
|
|
234
|
+
for (const role of decision.rolesOff) {
|
|
235
|
+
next[flagFor(role)] = false;
|
|
236
|
+
}
|
|
237
|
+
// Coder/security/reviewer aren't always toggled via flags in the current
|
|
238
|
+
// orchestrator (coder is implicit). Don't touch fields the orchestrator
|
|
239
|
+
// doesn't know; next simply carries them.
|
|
240
|
+
return next;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export { VALID_ROLES };
|