karajan-code 2.4.1 → 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.
Files changed (115) hide show
  1. package/README.md +20 -4
  2. package/package.json +5 -2
  3. package/src/agents/aider-agent.js +1 -2
  4. package/src/agents/base-agent.js +63 -1
  5. package/src/agents/claude-agent.js +3 -4
  6. package/src/agents/codex-agent.js +1 -2
  7. package/src/agents/gemini-agent.js +1 -2
  8. package/src/agents/index.js +11 -2
  9. package/src/agents/opencode-agent.js +1 -2
  10. package/src/brain/decision-tracker.js +123 -0
  11. package/src/brain/decisor.js +243 -0
  12. package/src/brain/solomon-consult.js +163 -0
  13. package/src/budget/comparison.js +96 -0
  14. package/src/checks/binaries.js +123 -0
  15. package/src/checks/ci.js +105 -0
  16. package/src/checks/config-files.js +217 -0
  17. package/src/checks/dir-setup.js +63 -0
  18. package/src/checks/mcp-health.js +157 -0
  19. package/src/checks/node.js +61 -0
  20. package/src/checks/ports.js +110 -0
  21. package/src/checks/rtk.js +36 -0
  22. package/src/checks/runner.js +267 -0
  23. package/src/checks/skills.js +49 -0
  24. package/src/checks/sonar.js +67 -0
  25. package/src/checks/system.js +53 -0
  26. package/src/checks/tokens.js +96 -0
  27. package/src/checks/types.js +105 -0
  28. package/src/cli.js +90 -7
  29. package/src/commands/board.js +23 -0
  30. package/src/commands/doctor.js +130 -362
  31. package/src/commands/plan.js +221 -37
  32. package/src/commands/resume.js +1 -1
  33. package/src/commands/run.js +2 -1
  34. package/src/commands/skills.js +49 -0
  35. package/src/config/schema.js +222 -0
  36. package/src/config.js +32 -3
  37. package/src/guards/policy-resolver.js +8 -0
  38. package/src/hu/store.js +9 -0
  39. package/src/infrastructure/command-runner.js +44 -0
  40. package/src/infrastructure/environment.js +38 -0
  41. package/src/infrastructure/file-system-service.js +63 -0
  42. package/src/infrastructure/mocks.js +182 -0
  43. package/src/mcp/handlers/management-handlers.js +7 -2
  44. package/src/mcp/tools.js +10 -3
  45. package/src/orchestrator/config-init.js +16 -1
  46. package/src/orchestrator/flow-runner.js +2085 -0
  47. package/src/orchestrator/pipeline-context.js +17 -0
  48. package/src/orchestrator/preflight-checks.js +78 -4
  49. package/src/orchestrator/solomon-escalation.js +21 -0
  50. package/src/orchestrator/stages/stage-executor.js +155 -0
  51. package/src/orchestrator.js +19 -1884
  52. package/src/plan/plan-executor.js +69 -0
  53. package/src/plan/plan-hu-ops.js +134 -0
  54. package/src/plan/plan-id.js +25 -0
  55. package/src/plan/plan-schema.js +139 -0
  56. package/src/plan/plan-store.js +58 -25
  57. package/src/prompts/architect.js +2 -2
  58. package/src/prompts/coder.js +2 -2
  59. package/src/prompts/prompt-resolver.js +127 -0
  60. package/src/prompts/reviewer.js +2 -2
  61. package/src/roles/architect-role.js +3 -1
  62. package/src/roles/base-role.js +23 -2
  63. package/src/roles/coder-role.js +3 -1
  64. package/src/roles/security-role.js +12 -0
  65. package/src/roles/tester-role.js +11 -0
  66. package/src/session/journal/decisions-writer.js +200 -0
  67. package/src/session/journal/iteration-logger.js +194 -0
  68. package/src/session/journal/summary-writer.js +176 -0
  69. package/src/session/journal/tree-writer.js +164 -0
  70. package/src/skills/semantic-detector.js +147 -0
  71. package/src/skills/skill-detector.js +186 -8
  72. package/src/skills/skill-loader.js +131 -1
  73. package/src/skills/skills-cache.js +128 -0
  74. package/src/types/agent.js +55 -0
  75. package/src/types/config.js +114 -0
  76. package/src/types/hu.js +47 -0
  77. package/src/types/index.js +34 -0
  78. package/src/types/policy.js +43 -0
  79. package/src/types/session.js +74 -0
  80. package/src/types/stage.js +68 -0
  81. package/src/utils/display/event-handlers.js +389 -0
  82. package/src/utils/display/formatters.js +132 -0
  83. package/src/utils/display/header.js +48 -0
  84. package/src/utils/display/session-summary.js +108 -0
  85. package/src/utils/display/solomon.js +42 -0
  86. package/src/utils/port-check.js +45 -0
  87. package/src/utils/port-occupant.js +95 -0
  88. package/src/utils/provider-env.js +116 -0
  89. package/src/utils/with-timeout.js +42 -0
  90. package/templates/roles/architect/anthropic.md +65 -0
  91. package/templates/roles/architect/default.md +63 -0
  92. package/templates/roles/architect/google.md +52 -0
  93. package/templates/roles/architect/openai.md +51 -0
  94. package/templates/roles/architect/opencode.md +50 -0
  95. package/templates/roles/coder/anthropic.md +72 -0
  96. package/templates/roles/coder/default.md +69 -0
  97. package/templates/roles/coder/google.md +78 -0
  98. package/templates/roles/coder/openai.md +59 -0
  99. package/templates/roles/coder/opencode.md +49 -0
  100. package/templates/roles/planner/anthropic.md +50 -0
  101. package/templates/roles/planner/default.md +48 -0
  102. package/templates/roles/planner/google.md +66 -0
  103. package/templates/roles/planner/openai.md +46 -0
  104. package/templates/roles/planner/opencode.md +53 -0
  105. package/templates/roles/reviewer/anthropic.md +61 -0
  106. package/templates/roles/reviewer/default.md +62 -0
  107. package/templates/roles/reviewer/google.md +63 -0
  108. package/templates/roles/reviewer/openai.md +57 -0
  109. package/templates/roles/reviewer/opencode.md +42 -0
  110. package/templates/roles/solomon/anthropic.md +71 -0
  111. package/templates/roles/solomon/default.md +83 -0
  112. package/templates/roles/solomon/google.md +61 -0
  113. package/templates/roles/solomon/openai.md +53 -0
  114. package/templates/roles/solomon/opencode.md +55 -0
  115. 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
- // Short-circuit: nothing to check
177
- if (!sonarEnabled && !securityEnabled) {
178
- logger.info("Preflight: skipped (no sonar, no security)");
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
+ }