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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.6.0",
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",
@@ -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 {Object} conflict
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
 
@@ -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";
@@ -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");
@@ -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 {};