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
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared context object for pipeline execution.
|
|
3
3
|
* Replaces destructured parameter objects across orchestrator functions.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {import("../types/config.js").KarajanConfig} KarajanConfig
|
|
6
|
+
* @typedef {import("../types/session.js").Session} Session
|
|
7
|
+
* @typedef {import("../types/stage.js").Logger} Logger
|
|
8
|
+
* @typedef {import("../types/stage.js").Emitter} Emitter
|
|
9
|
+
* @typedef {import("../types/stage.js").EventBase} EventBase
|
|
10
|
+
* @typedef {import("../types/config.js").PipelineFlags} PipelineFlags
|
|
11
|
+
* @typedef {import("../types/stage.js").StageContext} StageContext
|
|
4
12
|
*/
|
|
5
13
|
export class PipelineContext {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} args
|
|
16
|
+
* @param {KarajanConfig} args.config
|
|
17
|
+
* @param {Session} args.session
|
|
18
|
+
* @param {Logger} args.logger
|
|
19
|
+
* @param {Emitter|null} args.emitter
|
|
20
|
+
* @param {string} args.task
|
|
21
|
+
* @param {Object} [args.flags]
|
|
22
|
+
*/
|
|
6
23
|
constructor({ config, session, logger, emitter, task, flags = {} }) {
|
|
7
24
|
this.config = config;
|
|
8
25
|
this.session = session;
|
|
@@ -22,6 +22,14 @@ import {
|
|
|
22
22
|
} from "../sonar/config-resolver.js";
|
|
23
23
|
import { saveSonarToken } from "../sonar/credentials.js";
|
|
24
24
|
import { withDocLink } from "../utils/doc-links.js";
|
|
25
|
+
import { runChecks as runCheckPipeline } from "../checks/runner.js";
|
|
26
|
+
import { STATUS } from "../checks/types.js";
|
|
27
|
+
import { getNodeChecks } from "../checks/node.js";
|
|
28
|
+
import { getDirSetupChecks } from "../checks/dir-setup.js";
|
|
29
|
+
import { getPortChecks } from "../checks/ports.js";
|
|
30
|
+
import { getTokenChecks } from "../checks/tokens.js";
|
|
31
|
+
import { getMcpHealthChecks } from "../checks/mcp-health.js";
|
|
32
|
+
import { getSkillsChecks } from "../checks/skills.js";
|
|
25
33
|
|
|
26
34
|
function parseJsonSafe(text) {
|
|
27
35
|
try {
|
|
@@ -173,9 +181,14 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
173
181
|
errors: [],
|
|
174
182
|
};
|
|
175
183
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
// Resolve extended preflight opt-in early so the short-circuit below can see it.
|
|
185
|
+
const extendedDefault = globalThis.__KJ_DEFAULT_PREFLIGHT_EXTENDED ?? true;
|
|
186
|
+
const extendedEnabled = config?.preflight?.extended ?? extendedDefault;
|
|
187
|
+
|
|
188
|
+
// Preserve legacy short-circuit: if nothing at all needs checking, skip
|
|
189
|
+
// emitting the whole preflight bracket.
|
|
190
|
+
if (!sonarEnabled && !securityEnabled && !extendedEnabled) {
|
|
191
|
+
logger.info("Preflight: skipped (no sonar, no security, extended disabled)");
|
|
179
192
|
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
180
193
|
message: "Preflight skipped (no checks needed)",
|
|
181
194
|
detail: { ...result, executorType: "local" }
|
|
@@ -185,7 +198,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
185
198
|
|
|
186
199
|
emitProgress(emitter, makeEvent("preflight:start", { ...eventBase, stage: "preflight" }, {
|
|
187
200
|
message: "Running preflight environment checks",
|
|
188
|
-
detail: { sonarEnabled, securityEnabled, executorType: "local" }
|
|
201
|
+
detail: { sonarEnabled, securityEnabled, extendedEnabled, executorType: "local" }
|
|
189
202
|
}));
|
|
190
203
|
|
|
191
204
|
// --- 1. Docker (only if sonar enabled and not external) ---
|
|
@@ -288,6 +301,15 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
288
301
|
}
|
|
289
302
|
}
|
|
290
303
|
|
|
304
|
+
// --- 5. Complementary doctor-style checks (Node version, ports, tokens, MCP
|
|
305
|
+
// health, ~/.karajan dirs, openskills). These are auto-remediated
|
|
306
|
+
// with yes:true because preflight runs non-interactively inside kj run.
|
|
307
|
+
// Opt-out via `config.preflight.extended: false`. Defaults to true
|
|
308
|
+
// in production; the test harness (tests/setup.js) defaults it off.
|
|
309
|
+
if (extendedEnabled) {
|
|
310
|
+
await runExtendedPreflight({ config, result, emitter, eventBase, logger });
|
|
311
|
+
}
|
|
312
|
+
|
|
291
313
|
const hasErrors = result.errors.length > 0;
|
|
292
314
|
const hasWarnings = result.warnings.length > 0;
|
|
293
315
|
const preflightLang = getLang(config);
|
|
@@ -303,3 +325,55 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
303
325
|
|
|
304
326
|
return result;
|
|
305
327
|
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run the doctor-style complementary checks and merge their outcomes into
|
|
331
|
+
* the existing preflight result shape. Runs with yes:true (non-interactive)
|
|
332
|
+
* and auto-remediation enabled. FAIL/TIMEOUT are appended to errors (blocking),
|
|
333
|
+
* WARN are appended to warnings (non-blocking), FIXED status is recorded as a
|
|
334
|
+
* remediation, and runtime overrides from auto-fixes are merged into
|
|
335
|
+
* configOverrides.
|
|
336
|
+
*/
|
|
337
|
+
async function runExtendedPreflight({ config, result, emitter, eventBase, logger }) {
|
|
338
|
+
const checks = [
|
|
339
|
+
...getNodeChecks(),
|
|
340
|
+
...getDirSetupChecks(),
|
|
341
|
+
...getPortChecks(),
|
|
342
|
+
...getTokenChecks(config),
|
|
343
|
+
...getMcpHealthChecks(),
|
|
344
|
+
...getSkillsChecks(),
|
|
345
|
+
];
|
|
346
|
+
let report;
|
|
347
|
+
try {
|
|
348
|
+
report = await runCheckPipeline(checks, { config }, { mode: "fix", yes: true, timeoutMs: 3000, logger });
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logger.warn(`Preflight: extended checks failed to run (${err.message})`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const c of report.checks) {
|
|
355
|
+
result.checks.push({ name: c.name, ok: c.status === STATUS.OK || c.status === STATUS.FIXED || c.status === STATUS.SKIPPED, detail: c.detail });
|
|
356
|
+
const eventStatus = c.status === STATUS.FAIL || c.status === STATUS.TIMEOUT
|
|
357
|
+
? "fail"
|
|
358
|
+
: c.status === STATUS.WARN
|
|
359
|
+
? "warn"
|
|
360
|
+
: "ok";
|
|
361
|
+
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
362
|
+
status: eventStatus,
|
|
363
|
+
message: `${c.label}: ${c.detail}`,
|
|
364
|
+
detail: { name: c.name, status: c.status, detail: c.detail, fix: c.fix }
|
|
365
|
+
}));
|
|
366
|
+
if (c.status === STATUS.FIXED) {
|
|
367
|
+
result.remediations.push(`${c.label}: ${c.detail}`);
|
|
368
|
+
} else if (c.status === STATUS.FAIL || c.status === STATUS.TIMEOUT) {
|
|
369
|
+
result.ok = false;
|
|
370
|
+
result.errors.push({ check: c.name, message: `${c.label}: ${c.detail}`, fix: c.fix });
|
|
371
|
+
} else if (c.status === STATUS.WARN) {
|
|
372
|
+
result.warnings.push(`${c.label}: ${c.detail}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (report.overrides && Object.keys(report.overrides).length > 0) {
|
|
377
|
+
Object.assign(result.configOverrides, report.overrides);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -156,6 +156,27 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
156
156
|
subtask: ruling.result?.subtask?.title || null
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
// Record the ruling for the session journal (decisions.md). Lazy-import so
|
|
160
|
+
// this module stays free of journal-specific deps for callers that don't
|
|
161
|
+
// need them.
|
|
162
|
+
try {
|
|
163
|
+
const { recordSolomonRuling } = await import("../session/journal/decisions-writer.js");
|
|
164
|
+
recordSolomonRuling(session, {
|
|
165
|
+
stage: conflict?.stage || stage,
|
|
166
|
+
trigger: conflict?.reason || conflict?.summary || "solomon invoked",
|
|
167
|
+
conflict: {
|
|
168
|
+
task: conflict?.task,
|
|
169
|
+
reason: conflict?.reason,
|
|
170
|
+
summary: conflict?.summary,
|
|
171
|
+
iterationCount: conflict?.iterationCount,
|
|
172
|
+
},
|
|
173
|
+
ruling: ruling.result?.ruling || (ruling.ok ? "continue" : "escalate_human"),
|
|
174
|
+
reasoning: ruling.result?.reasoning || ruling.summary || "",
|
|
175
|
+
conditions: ruling.result?.conditions || [],
|
|
176
|
+
alternativeAgent: ruling.result?.alternativeAgent || null,
|
|
177
|
+
});
|
|
178
|
+
} catch { /* best-effort, don't break the pipeline on a journal hiccup */ }
|
|
179
|
+
|
|
159
180
|
if (!ruling.ok) {
|
|
160
181
|
const reason = ruling.result?.escalate_reason || solomonError || ruling.summary;
|
|
161
182
|
return escalateToHuman({
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StageExecutor — contract for per-stage orchestration logic (KJC-TSK-0315).
|
|
3
|
+
*
|
|
4
|
+
* The current orchestrator branches on a flat `pipelineFlags` object inside a
|
|
5
|
+
* monolithic `runFlow`. This contract is the first step toward replacing those
|
|
6
|
+
* branches with self-describing stage objects: each stage declares whether it
|
|
7
|
+
* wants to run (`canRun`), how to run (`execute`), and what to do on failure
|
|
8
|
+
* (`onFailure`). The orchestrator iterates a registry of stages instead of
|
|
9
|
+
* owning knowledge of every pipeline flag.
|
|
10
|
+
*
|
|
11
|
+
* Scope for this PR: define the contract + a tiny registry. Existing stage
|
|
12
|
+
* functions (e.g. src/orchestrator/stages/triage-stage.js) continue to be
|
|
13
|
+
* called directly by flow-runner.js; future refactors can migrate them to
|
|
14
|
+
* StageExecutor class form incrementally without touching the orchestrator.
|
|
15
|
+
*
|
|
16
|
+
* @typedef {Object} StageContext
|
|
17
|
+
* @property {Object} config
|
|
18
|
+
* @property {Object} session
|
|
19
|
+
* @property {Object} logger
|
|
20
|
+
* @property {Object} emitter
|
|
21
|
+
* @property {Object} eventBase
|
|
22
|
+
* @property {Object} pipelineFlags
|
|
23
|
+
* @property {Object} stageResults
|
|
24
|
+
* @property {Function} [trackBudget]
|
|
25
|
+
* @property {Function} [askQuestion]
|
|
26
|
+
* @property {Object} [brainCtx]
|
|
27
|
+
* @property {number} [iteration]
|
|
28
|
+
*
|
|
29
|
+
* @typedef {Object} StageOutcome
|
|
30
|
+
* @property {string} status - "ok" | "skipped" | "failed"
|
|
31
|
+
* @property {*} [result]
|
|
32
|
+
* @property {string} [detail]
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export class StageExecutor {
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} name - unique stage slug (e.g. "triage", "reviewer")
|
|
38
|
+
*/
|
|
39
|
+
constructor(name) {
|
|
40
|
+
if (!name || typeof name !== "string") {
|
|
41
|
+
throw new Error("StageExecutor requires a non-empty name");
|
|
42
|
+
}
|
|
43
|
+
this.name = name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether this stage should run for the current context. Default is
|
|
48
|
+
* true; subclasses override to gate on pipelineFlags or task type.
|
|
49
|
+
* @param {StageContext} _ctx
|
|
50
|
+
* @returns {boolean | Promise<boolean>}
|
|
51
|
+
*/
|
|
52
|
+
canRun(_ctx) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Run the stage. MUST be overridden. Return a StageOutcome describing what
|
|
58
|
+
* happened; the runner persists `result` under ctx.stageResults[this.name].
|
|
59
|
+
* @param {StageContext} _ctx
|
|
60
|
+
* @returns {Promise<StageOutcome>}
|
|
61
|
+
*/
|
|
62
|
+
async execute(_ctx) {
|
|
63
|
+
throw new Error(`StageExecutor(${this.name}).execute() not implemented`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Error hook. Default behaviour rethrows so the orchestrator's global
|
|
68
|
+
* failure handling kicks in; subclasses can override to recover silently,
|
|
69
|
+
* return a partial result, or escalate to Solomon before rethrowing.
|
|
70
|
+
* @param {Error} err
|
|
71
|
+
* @param {StageContext} _ctx
|
|
72
|
+
* @returns {Promise<StageOutcome | void>}
|
|
73
|
+
*/
|
|
74
|
+
// eslint-disable-next-line no-unused-vars
|
|
75
|
+
async onFailure(err, _ctx) {
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Registry of StageExecutor instances keyed by stage name. Kept intentionally
|
|
82
|
+
* small — the orchestrator iterates `list()` in declared order and invokes
|
|
83
|
+
* `canRun` / `execute` / `onFailure` as needed.
|
|
84
|
+
*/
|
|
85
|
+
export class StageRegistry {
|
|
86
|
+
constructor() {
|
|
87
|
+
/** @type {Map<string, StageExecutor>} */
|
|
88
|
+
this._stages = new Map();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {StageExecutor} stage
|
|
93
|
+
*/
|
|
94
|
+
register(stage) {
|
|
95
|
+
if (!(stage instanceof StageExecutor)) {
|
|
96
|
+
throw new Error("StageRegistry.register(): argument must be a StageExecutor");
|
|
97
|
+
}
|
|
98
|
+
if (this._stages.has(stage.name)) {
|
|
99
|
+
throw new Error(`StageRegistry: stage "${stage.name}" already registered`);
|
|
100
|
+
}
|
|
101
|
+
this._stages.set(stage.name, stage);
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} name
|
|
107
|
+
* @returns {StageExecutor | undefined}
|
|
108
|
+
*/
|
|
109
|
+
get(name) {
|
|
110
|
+
return this._stages.get(name);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @returns {StageExecutor[]}
|
|
115
|
+
*/
|
|
116
|
+
list() {
|
|
117
|
+
return [...this._stages.values()];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} name
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
has(name) {
|
|
125
|
+
return this._stages.has(name);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get size() {
|
|
129
|
+
return this._stages.size;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Run a stage via the contract. Centralizes the canRun/execute/onFailure
|
|
135
|
+
* dance so future call sites don't reinvent it. Returns null when `canRun`
|
|
136
|
+
* returns false (stage skipped).
|
|
137
|
+
*
|
|
138
|
+
* @param {StageExecutor} stage
|
|
139
|
+
* @param {StageContext} ctx
|
|
140
|
+
* @returns {Promise<StageOutcome | null>}
|
|
141
|
+
*/
|
|
142
|
+
export async function runStage(stage, ctx) {
|
|
143
|
+
if (!(stage instanceof StageExecutor)) {
|
|
144
|
+
throw new Error("runStage(): first argument must be a StageExecutor");
|
|
145
|
+
}
|
|
146
|
+
const shouldRun = await stage.canRun(ctx);
|
|
147
|
+
if (!shouldRun) return null;
|
|
148
|
+
try {
|
|
149
|
+
return await stage.execute(ctx);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const recovered = await stage.onFailure(err, ctx);
|
|
152
|
+
if (recovered !== undefined) return recovered;
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|