karajan-code 2.6.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 +19 -0
- package/package.json +1 -1
- package/src/brain/solomon-consult.js +9 -1
- package/src/orchestrator/solomon-escalation.js +34 -0
- package/src/roles/agent-role.js +9 -0
- package/src/roles/base-role.js +17 -0
- package/src/types/index.js +4 -0
- package/src/types/solomon.js +87 -0
package/README.md
CHANGED
|
@@ -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):
|
package/package.json
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* This is intentionally conservative. A future iteration can make Solomon a
|
|
10
10
|
* binding arbiter for routing (like it is for pipeline-control decisions).
|
|
11
|
+
*
|
|
12
|
+
* Architectural invariant: this module is the ONLY sanctioned path from
|
|
13
|
+
* Brain to Solomon for routing concerns. Stages / rules / agents must not
|
|
14
|
+
* bypass Brain to reach Solomon directly — see the note on `invokeSolomon`
|
|
15
|
+
* in `src/orchestrator/solomon-escalation.js` and docs/ARCHITECTURE.md.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {import("../types/solomon.js").Conflict} Conflict
|
|
18
|
+
* @typedef {import("../types/solomon.js").SolomonResult} SolomonResult
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -17,7 +25,7 @@
|
|
|
17
25
|
* @param {import("./decisor.js").Decision} args.decision
|
|
18
26
|
* @param {Object} args.triage
|
|
19
27
|
* @param {string} args.task
|
|
20
|
-
* @returns {
|
|
28
|
+
* @returns {Conflict} conflict
|
|
21
29
|
*/
|
|
22
30
|
export function buildRoutingConflict({ decision, triage, task }) {
|
|
23
31
|
return {
|
|
@@ -3,6 +3,23 @@ import { addCheckpoint, pauseSession, saveSession } from "../session-store.js";
|
|
|
3
3
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
4
4
|
import { msg, getLang } from "../utils/messages.js";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import("../types/solomon.js").Conflict} Conflict
|
|
8
|
+
* @typedef {import("../types/solomon.js").SolomonResult} SolomonResult
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Architectural invariant (DO NOT BREAK):
|
|
13
|
+
* `invokeSolomon` MUST NOT be called directly from stages, rules or agent
|
|
14
|
+
* code. The only callers should be:
|
|
15
|
+
* - Karajan Brain (via `src/brain/solomon-consult.js`)
|
|
16
|
+
* - The orchestrator's escalation path (`src/orchestrator/flow-runner.js`)
|
|
17
|
+
* on behalf of Brain.
|
|
18
|
+
* The v2.3.0 audit fixed 21 violations of this rule. Keeping Solomon behind
|
|
19
|
+
* Brain is what the "Brain-as-gateway, Solomon-as-judge" architecture
|
|
20
|
+
* depends on — see docs/ARCHITECTURE.md §Brain/Solomon.
|
|
21
|
+
*/
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* Build a human-readable escalation message from raw Solomon conflict data.
|
|
8
25
|
* Replaces the raw JSON dump with structured, plain-text output.
|
|
@@ -81,6 +98,23 @@ function extractSolomonHistory(session) {
|
|
|
81
98
|
}));
|
|
82
99
|
}
|
|
83
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Consult Solomon (or fall back to human escalation) for a given conflict.
|
|
103
|
+
* See the Conflict typedef for the shape of the `conflict` argument and
|
|
104
|
+
* the invariant above for who is allowed to call this.
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} params
|
|
107
|
+
* @param {Object} params.config
|
|
108
|
+
* @param {Object} params.logger
|
|
109
|
+
* @param {Object} [params.emitter]
|
|
110
|
+
* @param {Object} [params.eventBase]
|
|
111
|
+
* @param {string} params.stage
|
|
112
|
+
* @param {Conflict} params.conflict
|
|
113
|
+
* @param {Function} [params.askQuestion]
|
|
114
|
+
* @param {Object} [params.session]
|
|
115
|
+
* @param {number} [params.iteration]
|
|
116
|
+
* @returns {Promise<SolomonResult>}
|
|
117
|
+
*/
|
|
84
118
|
export async function invokeSolomon({ config, logger, emitter, eventBase, stage, conflict, askQuestion, session, iteration }) {
|
|
85
119
|
const solomonEnabled = Boolean(config.pipeline?.solomon?.enabled);
|
|
86
120
|
|
package/src/roles/agent-role.js
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
* Handles the common execute() flow: resolve provider → create agent → call LLM → parse output.
|
|
4
4
|
* Subclasses override hooks (buildPrompt, parseOutput, buildSuccessResult, buildSummary)
|
|
5
5
|
* instead of reimplementing the full execute() boilerplate.
|
|
6
|
+
*
|
|
7
|
+
* Conceptual mapping: `AgentRole` is a Karajan Role that delegates to an AI CLI
|
|
8
|
+
* subprocess (not to a provider HTTP API). From the outside it plays the same
|
|
9
|
+
* role as a Genkit Flow — named unit of work with a prompt and a parsed output —
|
|
10
|
+
* but every invocation spawns a fresh `execa` subprocess (claude, codex, gemini,
|
|
11
|
+
* aider, opencode) and the state lives in Karajan's session store, not in a
|
|
12
|
+
* provider-side conversation. See `docs/COMPARISON.md`.
|
|
13
|
+
*
|
|
14
|
+
* @alias Flow — similar to Genkit Flow but executed over subprocess CLIs, not API calls.
|
|
6
15
|
*/
|
|
7
16
|
import { BaseRole } from "./base-role.js";
|
|
8
17
|
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
package/src/roles/base-role.js
CHANGED
|
@@ -49,6 +49,23 @@ async function loadFirstExisting(paths) {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Base class for every pipeline role (coder, reviewer, planner, refactorer,
|
|
54
|
+
* researcher, architect, triage, discover, tester, security, impeccable,
|
|
55
|
+
* hu-reviewer, audit, solomon, commiter, karajan-brain, domain-curator).
|
|
56
|
+
*
|
|
57
|
+
* Conceptual mapping for developers coming from other ecosystems:
|
|
58
|
+
* `BaseRole` (and every subclass) is Karajan's equivalent of a **Flow** in the
|
|
59
|
+
* Genkit sense — a named, composable unit of work with a prompt, an invocation
|
|
60
|
+
* and a parsed output. The important difference: a Karajan Role executes over
|
|
61
|
+
* a **subprocess CLI** (`claude`, `codex`, `gemini`, `aider`, `opencode`), not
|
|
62
|
+
* by calling an HTTP provider API. No PTY, no long-lived session — each run
|
|
63
|
+
* is a fresh subprocess with state on disk under `~/.karajan/sessions/`.
|
|
64
|
+
*
|
|
65
|
+
* See `docs/COMPARISON.md` for the full positioning.
|
|
66
|
+
*
|
|
67
|
+
* @alias Flow — similar to Genkit Flow but executed over subprocess CLIs, not API calls.
|
|
68
|
+
*/
|
|
52
69
|
export class BaseRole {
|
|
53
70
|
constructor({ name, config, logger, emitter = null }) {
|
|
54
71
|
if (!name) throw new Error("Role name is required");
|
package/src/types/index.js
CHANGED
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
*
|
|
30
30
|
* @typedef {import("./policy.js").ResolvedPolicies} ResolvedPolicies
|
|
31
31
|
* @typedef {import("./policy.js").QualityGateResult} QualityGateResult
|
|
32
|
+
*
|
|
33
|
+
* @typedef {import("./solomon.js").Conflict} SolomonConflict
|
|
34
|
+
* @typedef {import("./solomon.js").ConflictHistoryEntry} SolomonConflictHistoryEntry
|
|
35
|
+
* @typedef {import("./solomon.js").SolomonResult} SolomonResult
|
|
32
36
|
*/
|
|
33
37
|
|
|
34
38
|
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typedefs for Solomon — the AI judge consulted by Brain on genuine dilemmas.
|
|
3
|
+
*
|
|
4
|
+
* Architectural invariant (enforced by convention, see docs/ARCHITECTURE.md):
|
|
5
|
+
* Solomon is **NEVER** invoked directly from stages, rules or roles.
|
|
6
|
+
* The only legitimate entry points are:
|
|
7
|
+
* - `src/brain/solomon-consult.js#consultSolomonForRouting`
|
|
8
|
+
* - `src/orchestrator/solomon-escalation.js#invokeSolomon`
|
|
9
|
+
* Both are called by Brain (or by the orchestrator on Brain's behalf).
|
|
10
|
+
* The v2.3.0 audit fixed 21 violations where stages bypassed Brain and
|
|
11
|
+
* called Solomon directly. Don't reintroduce that pattern.
|
|
12
|
+
*
|
|
13
|
+
* This module is runtime-empty — it only carries JSDoc annotations.
|
|
14
|
+
*
|
|
15
|
+
* @module types/solomon
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A single turn of feedback attributed to a pipeline agent. Used to build
|
|
20
|
+
* `Conflict.history` when Brain escalates a decision to Solomon.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Object} ConflictHistoryEntry
|
|
23
|
+
* @property {string} agent - slug of the agent that produced the feedback
|
|
24
|
+
* (e.g. "reviewer", "sonar", "tester",
|
|
25
|
+
* "coder_rate_limit").
|
|
26
|
+
* @property {string} feedback - raw textual feedback to be weighted by Solomon.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The bundle of context Brain hands to Solomon when it cannot decide on its
|
|
31
|
+
* own. Solomon reads only this object + the stage + task; it never sees
|
|
32
|
+
* session internals beyond what is packed here.
|
|
33
|
+
*
|
|
34
|
+
* Historical shape: this matches the argument already used by
|
|
35
|
+
* `invokeSolomon({ ..., conflict })` — the typedef formalizes the contract
|
|
36
|
+
* so future drift is caught by `npm run typecheck`.
|
|
37
|
+
*
|
|
38
|
+
* @typedef {Object} Conflict
|
|
39
|
+
* @property {string} stage
|
|
40
|
+
* Pipeline stage that triggered escalation (e.g. "reviewer",
|
|
41
|
+
* "sonar", "tester", "init_error", "coder_rate_limit").
|
|
42
|
+
* @property {string} task
|
|
43
|
+
* Original user task (denormalized so Solomon sees what the user
|
|
44
|
+
* actually asked for, not just the stage context).
|
|
45
|
+
* @property {number} iterationCount
|
|
46
|
+
* Current iteration number inside the escalating sub-loop.
|
|
47
|
+
* @property {number} maxIterations
|
|
48
|
+
* Configured cap for that sub-loop (reviewer retries, tester
|
|
49
|
+
* retries, full pipeline iterations, etc.). Hitting this cap is
|
|
50
|
+
* the canonical trigger for Solomon consultation.
|
|
51
|
+
* @property {string|null} [cooldownUntil]
|
|
52
|
+
* ISO timestamp for rate-limit / standby escalations. Null when
|
|
53
|
+
* the conflict is not a rate-limit.
|
|
54
|
+
* @property {number|null} [cooldownMs]
|
|
55
|
+
* Remaining cooldown in milliseconds. Null outside rate-limit
|
|
56
|
+
* escalations.
|
|
57
|
+
* @property {ConflictHistoryEntry[]} history
|
|
58
|
+
* Ordered list of agent feedback Solomon should weigh. Most
|
|
59
|
+
* recent last. Must be non-empty — Solomon should always see at
|
|
60
|
+
* least one piece of evidence.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Solomon's ruling on a `Conflict`. Returned by `invokeSolomon()` and
|
|
65
|
+
* consumed by the orchestrator to decide what happens next.
|
|
66
|
+
*
|
|
67
|
+
* @typedef {Object} SolomonResult
|
|
68
|
+
* @property {"continue"|"pause"|"subtask"} action
|
|
69
|
+
* - `continue`: retry the sub-loop with optional `humanGuidance`
|
|
70
|
+
* appended to the next feedback round.
|
|
71
|
+
* - `pause`: stop the pipeline and surface a question to the
|
|
72
|
+
* human operator (`SolomonResult.question`).
|
|
73
|
+
* - `subtask`: bifurcate into a named sub-task defined in
|
|
74
|
+
* `SolomonResult.subtask`.
|
|
75
|
+
* @property {string} [rationale] - human-readable reasoning for the
|
|
76
|
+
* chosen action. Logged to the
|
|
77
|
+
* decisions journal.
|
|
78
|
+
* @property {string} [humanGuidance] - text to merge into the next
|
|
79
|
+
* coder/reviewer prompt when
|
|
80
|
+
* action === "continue".
|
|
81
|
+
* @property {string} [question] - prompt shown to the human when
|
|
82
|
+
* action === "pause".
|
|
83
|
+
* @property {Object} [subtask] - sub-task descriptor when
|
|
84
|
+
* action === "subtask".
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
export {};
|