karajan-code 2.7.3 → 2.7.4

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
@@ -23,7 +23,7 @@
23
23
 
24
24
  ---
25
25
 
26
- > **v2.7.3 released** — Three dogfooding fixes: (1) task can now come from a `.md` file — `kj run --task-file spec.md` or MCP `kj_run({ taskFile: "spec.md" })`, same for `kj plan/audit/code/review/discover/triage/researcher/architect`; (2) CLI commands finally write `.kj/run.log` like MCP does, so `kj-tail` works whether Claude Code launches `kj` via Bash or the MCP tool; (3) Node 18 LTS is really supported now (preflight no longer refuses 18 with a bogus "needs Node 20" message), and `kj-tail` v1.38.0 waits for the log file to appear instead of exiting. Carries the full v2.7 skills story: [`addyosmani/agent-skills`](https://github.com/addyosmani/agent-skills) as Karajan's first-source process catalog (20+ lifecycle workflows) injected per role **before** OpenSkills stack skills, `summary.md` now surfaces which slugs shaped each role's prompt. See [CHANGELOG.md](./CHANGELOG.md) for full history and [MIGRATION-v2.md](./MIGRATION-v2.md) for v1→v2 breaking changes.
26
+ > **v2.7.4 released** — Three contract fixes: (1) **Sonar is now intrinsic** to Karajan for code tasks (`sw`/`refactor`/`add-tests`) — `sonarqube.enabled` config and `--no-sonar` flag are ignored with a deprecation warning; a code task without a quality gate is not a job Karajan calls complete, like TDD; Solomon still may skip an iteration at runtime via rules. (2) **Preflight no longer demands API keys Karajan doesn't use** — the bogus `ANTHROPIC_API_KEY not set` check was blocking every Claude Code MCP run; replaced with a real CLI availability check (`claude`, `codex`, `gemini` on PATH). (3) **Orchestrator no longer crashes** with `Cannot read properties of undefined (reading 'push')` on the Solomon init-error path. Adds two architectural invariants (`no-provider-apis`, `sonar-intrinsic`) to CI so the contracts can't silently drift back. New `docs/TESTS.md` test-suite guide with diagrams. 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
 
@@ -297,9 +297,9 @@ Not nostalgia, not stubbornness. I've been using JavaScript since 1997, when Bre
297
297
  | Tool | Why |
298
298
  |------|-----|
299
299
  | [**RTK**](https://github.com/rtk-ai/rtk) | Reduces token consumption by 60-90% on Bash command outputs |
300
- | [**Planning Game MCP**](https://github.com/AgenteIA-Geniova/planning-game-mcp) | Agile project management (tasks, sprints, estimation), XP-native |
301
- | [**GitHub MCP**](https://github.com/modelcontextprotocol/servers/tree/main/src/github) | Create PRs, manage issues directly from the agent |
302
- | [**Chrome DevTools MCP**](https://github.com/anthropics/anthropic-quickstarts/tree/main/chrome-devtools-mcp) | Verify UI changes visually after frontend modifications |
300
+ | [**Planning Game MCP**](https://github.com/manufosela/planning-game-xp-mcp) | Agile project management (tasks, sprints, estimation), XP-native |
301
+ | [**GitHub MCP**](https://github.com/github/github-mcp-server) | Create PRs, manage issues directly from the agent |
302
+ | [**Chrome DevTools MCP**](https://github.com/ChromeDevTools/chrome-devtools-mcp) | Verify UI changes visually after frontend modifications |
303
303
 
304
304
  ## Contributing
305
305
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.7.3",
3
+ "version": "2.7.4",
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",
@@ -38,7 +38,18 @@ export function createSonarPortCheck() {
38
38
  name: "port:sonar",
39
39
  label: "SonarQube port",
40
40
  strategy: STRATEGY.MANUAL,
41
- applies: (config) => config?.sonarqube?.enabled !== false,
41
+ // Since v2.7.4 Sonar is intrinsic to Karajan for code tasks, so the
42
+ // port check almost always applies. The only legitimate skip is when
43
+ // the user runs their own external Sonar instance (`external: true`),
44
+ // in which case the port is managed outside Karajan. The legacy
45
+ // `sonarqube.enabled` field is ignored (deprecation warning emitted
46
+ // at config load time — see emitConfigDeprecations in flow-runner.js).
47
+ applies: (config) => {
48
+ if (config?.sonarqube?.external === true) {
49
+ return { applies: false, reason: "sonarqube.external is true — port managed outside Karajan" };
50
+ }
51
+ return true;
52
+ },
42
53
  async detect({ config }) {
43
54
  const port = extractPortFromHost(config?.sonarqube?.host, DEFAULT_SONAR_PORT);
44
55
  const free = await isPortAvailable(port);
@@ -73,7 +84,12 @@ export function createHuBoardPortCheck() {
73
84
  name: "port:hu-board",
74
85
  label: "HU Board port",
75
86
  strategy: STRATEGY.AUTO,
76
- applies: (config) => config?.hu_board?.enabled !== false,
87
+ applies: (config) => {
88
+ if (config?.hu_board?.enabled === false) {
89
+ return { applies: false, reason: "hu_board.enabled is false in kj.config.yml — HU Board dashboard skipped" };
90
+ }
91
+ return true;
92
+ },
77
93
  describe: "Rebind HU Board to the next free port",
78
94
  async detect({ config }) {
79
95
  const port = config?.hu_board?.port || DEFAULT_HU_BOARD_PORT;
@@ -55,19 +55,35 @@ export async function runChecks(checks, ctx, options = {}) {
55
55
  const overrides = {};
56
56
 
57
57
  // Build applicable entries (skipped checks noted up front).
58
+ // A check.applies(config) may return:
59
+ // - true / false — boolean form (back-compat).
60
+ // - an object { applies: boolean, reason?: string } — when false, `reason`
61
+ // is shown verbatim so users see WHY (e.g. "sonarqube.enabled is false
62
+ // in kj.config.yml") instead of the opaque "Not applicable" message
63
+ // that confused users debugging their config.
58
64
  for (const check of checks) {
59
- if (typeof check.applies === "function" && !check.applies(config)) {
60
- entries.push({
61
- check,
62
- status: STATUS.SKIPPED,
63
- detail: "Not applicable for current configuration",
64
- runMs: 0,
65
- detectResult: null,
66
- remediated: false,
67
- });
68
- } else {
65
+ if (typeof check.applies !== "function") {
66
+ entries.push({ check, status: null, detail: null, runMs: 0, detectResult: null, remediated: false });
67
+ continue;
68
+ }
69
+ const verdict = check.applies(config);
70
+ const isObj = verdict && typeof verdict === "object" && "applies" in verdict;
71
+ const applies = isObj ? Boolean(verdict.applies) : Boolean(verdict);
72
+ if (applies) {
69
73
  entries.push({ check, status: null, detail: null, runMs: 0, detectResult: null, remediated: false });
74
+ continue;
70
75
  }
76
+ const reason = (isObj && typeof verdict.reason === "string" && verdict.reason.trim())
77
+ ? verdict.reason.trim()
78
+ : "Not applicable for current configuration";
79
+ entries.push({
80
+ check,
81
+ status: STATUS.SKIPPED,
82
+ detail: reason,
83
+ runMs: 0,
84
+ detectResult: null,
85
+ remediated: false,
86
+ });
71
87
  }
72
88
 
73
89
  // Phase 1: detect in parallel with timeout.
@@ -1,41 +1,83 @@
1
1
  /**
2
- * API token availability checks.
2
+ * Provider CLI availability checks (preflight).
3
3
  *
4
- * Provider tokens (ANTHROPIC_API_KEY / OPENAI_API_KEY / ...) are LAZY:
5
- * we only check them for providers that are actually used by at least one
6
- * active role in the current config.
4
+ * IMPORTANT — Karajan does NOT call provider APIs directly. There is no
5
+ * `@anthropic-ai/sdk`, no `openai`, no `@google/generative-ai` in
6
+ * package.json. Every active role spawns its agent CLI as a subprocess
7
+ * (`claude`, `codex`, `gemini`, `opencode`, `aider`). The CLI handles its
8
+ * own auth, typically via OAuth + on-disk token (Claude Code, Codex CLI,
9
+ * Gemini CLI). The user does NOT need to set ANTHROPIC_API_KEY etc. for
10
+ * Karajan to work.
7
11
  *
8
- * Strategy:
9
- * - Required providers with no key → FAIL, strategy manual (we can't
10
- * fetch an API key for the user). Fix hint includes the how-to URL.
11
- * - Optional providers (opencode, aider) without key → WARN.
12
- * - GH_TOKEN is checked when git.auto_pr=true, strategy prompt (we can
13
- * launch `gh auth login` with user approval).
12
+ * Pre-v2.7.4 this module checked for ANTHROPIC_API_KEY / OPENAI_API_KEY /
13
+ * GEMINI_API_KEY env vars and FAILed preflight when missing — a false
14
+ * positive that blocked every user running Karajan as an MCP child of
15
+ * Claude Code (where `apiKeySource: "none"` is the norm). Replaced with a
16
+ * CLI-availability check that mirrors what the orchestrator actually does.
17
+ *
18
+ * GH_TOKEN is still checked when `git.auto_pr=true`, because that flow uses
19
+ * `git push` directly with the env var.
14
20
  */
15
21
 
16
22
  import { checkBinary } from "../utils/agent-detect.js";
17
- import { getRequiredProviderEnvs, hasEnvVar } from "../utils/provider-env.js";
23
+ import { getRequiredProviderEnvs, normalizeProvider } from "../utils/provider-env.js";
18
24
  import { STRATEGY } from "./types.js";
19
25
 
20
26
  /**
21
- * A single provider env check. One is created per active provider.
27
+ * Map canonical provider name → the CLI binary Karajan actually spawns.
28
+ * Keep in sync with src/agents/index.js.
22
29
  */
23
- function createProviderTokenCheck(entry) {
30
+ const PROVIDER_TO_CLI = Object.freeze({
31
+ anthropic: "claude",
32
+ openai: "codex",
33
+ google: "gemini",
34
+ opencode: "opencode",
35
+ aider: "aider",
36
+ });
37
+
38
+ /**
39
+ * Build a CLI-availability check for one provider.
40
+ *
41
+ * The check runs `<cli> --version`. If the binary is on PATH and exits 0,
42
+ * preflight passes — Karajan can spawn it. If the CLI is missing, preflight
43
+ * fails with the install URL we already track in PROVIDER_ENV.
44
+ *
45
+ * Note: we do NOT verify that the CLI is *authenticated*. Doing so would
46
+ * require provider-specific introspection (`claude doctor`, `codex login
47
+ * status`, ...), each with its own quirks and exit codes. If the CLI is
48
+ * present but not logged in, the agent will fail at the first request with
49
+ * a clear error — and the orchestrator surfaces that error already.
50
+ */
51
+ function createProviderCliCheck(entry) {
52
+ const cli = PROVIDER_TO_CLI[entry.provider];
24
53
  return {
25
- name: `token:${entry.provider}`,
26
- label: `API token: ${entry.provider}`,
54
+ name: `cli:${entry.provider}`,
55
+ label: `${entry.provider} CLI (${cli})`,
27
56
  strategy: STRATEGY.MANUAL,
28
57
  async detect() {
29
- const ok = hasEnvVar(entry);
30
- if (ok) {
31
- return { ok: true, severity: "info", detail: `${entry.envVars[0]} is set` };
58
+ if (!cli) {
59
+ return {
60
+ ok: false,
61
+ severity: "warn",
62
+ detail: `Unknown CLI for provider ${entry.provider}`,
63
+ extra: { provider: entry.provider },
64
+ };
65
+ }
66
+ const result = await checkBinary(cli);
67
+ if (result.ok) {
68
+ return {
69
+ ok: true,
70
+ severity: "info",
71
+ detail: `${cli} on PATH (${result.version || "version unknown"})`,
72
+ extra: { provider: entry.provider, cli, version: result.version, path: result.path },
73
+ };
32
74
  }
33
75
  return {
34
76
  ok: false,
35
77
  severity: entry.optional ? "warn" : "fail",
36
- detail: `${entry.envVars.join(" / ")} not set`,
37
- fix: `Get an API key at ${entry.howToGetUrl} and export ${entry.envVars[0]}=...`,
38
- extra: { provider: entry.provider, envVars: entry.envVars, howToGetUrl: entry.howToGetUrl },
78
+ detail: `${cli} not found on PATH`,
79
+ fix: `Install the ${cli} CLI: see ${entry.howToGetUrl}`,
80
+ extra: { provider: entry.provider, cli, howToGetUrl: entry.howToGetUrl },
39
81
  };
40
82
  },
41
83
  };
@@ -43,6 +85,8 @@ function createProviderTokenCheck(entry) {
43
85
 
44
86
  /**
45
87
  * GH token check + auto-remediation via `gh auth login` when it's installed.
88
+ * Kept from the previous version: this one IS legitimate because Karajan
89
+ * uses `gh` (or raw `git push`) directly when auto_pr is on.
46
90
  */
47
91
  function createGhTokenCheck() {
48
92
  return {
@@ -82,15 +126,24 @@ function createGhTokenCheck() {
82
126
  }
83
127
 
84
128
  /**
85
- * Build the list of token checks for the active config. Each active provider
86
- * gets one, plus conditional GH token when auto_pr is on.
129
+ * Build the list of CLI/token checks for the active config.
130
+ * - One CLI availability check per active provider (claude / codex / gemini / ...).
131
+ * - One GH token check, conditional on git.auto_pr.
132
+ *
133
+ * Function name kept as `getTokenChecks` for backward compat with
134
+ * preflight-checks.js, even though the bulk of the checks no longer touch
135
+ * tokens at all. Tracked: rename to `getProviderChecks` in a follow-up
136
+ * once consumers are updated.
87
137
  *
88
138
  * @param {Object} config
89
139
  * @returns {import("./types.js").Check[]}
90
140
  */
91
141
  export function getTokenChecks(config) {
92
142
  const required = getRequiredProviderEnvs(config);
93
- const checks = required.map(createProviderTokenCheck);
143
+ const checks = required.map(createProviderCliCheck);
94
144
  checks.push(createGhTokenCheck());
95
145
  return checks;
96
146
  }
147
+
148
+ // Re-export for tests that need to introspect the mapping.
149
+ export { PROVIDER_TO_CLI, normalizeProvider };
package/src/config.js CHANGED
@@ -290,6 +290,20 @@ export async function loadConfig(projectDir) {
290
290
  throw new Error(`Invalid Karajan config:\n${formatConfigIssues(validation.issues)}`);
291
291
  }
292
292
 
293
+ // Deprecation tracking — `sonarqube.enabled` was a footgun: users would
294
+ // set it to `false` in their global ~/.karajan/kj.config.yml and then
295
+ // wonder why Sonar didn't run on a code task. Since v2.7.4 Sonar is
296
+ // intrinsic to Karajan for code tasks; the field is ignored. We surface
297
+ // a warning at run start (see emitConfigDeprecations in flow-runner.js)
298
+ // so users notice and clean up their config. Don't throw — keep
299
+ // back-compat so existing scripts keep working.
300
+ const userSetSonarEnabled = (globalConfig?.sonarqube?.enabled !== undefined)
301
+ || (projectConfig?.sonarqube?.enabled !== undefined);
302
+ if (userSetSonarEnabled) {
303
+ merged._deprecated = merged._deprecated || {};
304
+ merged._deprecated.sonarqubeEnabledKey = true;
305
+ }
306
+
293
307
  return { config: merged, path: configPath, exists: globalExists, hasProjectConfig: !!projectConfig };
294
308
  }
295
309
 
@@ -423,7 +437,16 @@ function applyOutputModeOverrides(out, flags) {
423
437
 
424
438
  function applyMiscOverrides(out, flags) {
425
439
  if (flags[AUTO_SIMPLIFY_FLAG] !== undefined) out.pipeline.auto_simplify = Boolean(flags[AUTO_SIMPLIFY_FLAG]);
426
- if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
440
+ // `--no-sonar` and `sonarqube.enabled: false` are deprecated since v2.7.4.
441
+ // Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests) and
442
+ // skipped for non-code tasks (audit/doc/infra/analysis/no-code) by policy.
443
+ // The flag is still accepted to avoid breaking scripts; it now emits a
444
+ // warning at run start and is otherwise ignored. See preflight-checks.js
445
+ // and flow-runner.js for the actual gate (resolvedPolicies.sonar).
446
+ if (flags.noSonar || flags.sonar === false) {
447
+ out._deprecated = out._deprecated || {};
448
+ out._deprecated.noSonarFlag = true;
449
+ }
427
450
  out.sonarcloud = out.sonarcloud || {};
428
451
  if (flags.enableSonarcloud === true) out.sonarcloud.enabled = true;
429
452
  if (flags.noSonarcloud === true || flags.sonarcloud === false) out.sonarcloud.enabled = false;
@@ -726,6 +726,12 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
726
726
 
727
727
  let updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
728
728
 
729
+ // Deprecation warnings recorded at config load time. Done here, after
730
+ // policies are resolved, so the message lands in context: if the user's
731
+ // legacy `sonarqube.enabled: false` is being ignored, this is the right
732
+ // moment for them to see it.
733
+ emitConfigDeprecations(updatedConfig, logger, emitter, eventBase);
734
+
729
735
  // --- Preflight environment checks ---
730
736
  const preflightResult = await runPreflightChecks({
731
737
  config: updatedConfig, logger, emitter, eventBase,
@@ -920,6 +926,46 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
920
926
  * Setting skills.addyosmani.enabled = false, or removing "addyosmani" from
921
927
  * skills.sources, disables this step entirely (reverts to OpenSkills-only).
922
928
  */
929
+ /**
930
+ * Print one-time deprecation warnings for config keys / CLI flags that
931
+ * Karajan no longer honours. Called once per run, after policy resolution
932
+ * so the message lands in context (the user sees what's being ignored
933
+ * RIGHT before the preflight that would have been affected).
934
+ *
935
+ * Currently surfaces:
936
+ * - `sonarqube.enabled` set in user config → ignored since v2.7.4
937
+ * - `--no-sonar` CLI flag → ignored since v2.7.4
938
+ */
939
+ function emitConfigDeprecations(config, logger, emitter, eventBase) {
940
+ const dep = config?._deprecated;
941
+ if (!dep) return;
942
+
943
+ if (dep.sonarqubeEnabledKey) {
944
+ const m =
945
+ "DEPRECATED: `sonarqube.enabled` in kj.config.yml is ignored since v2.7.4. " +
946
+ "Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests) and " +
947
+ "skipped for non-code tasks (audit/doc/infra/analysis/no-code) by policy. " +
948
+ "Remove the key from your config to silence this warning.";
949
+ logger.warn(m);
950
+ emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
951
+ message: m,
952
+ detail: { key: "sonarqube.enabled", since: "v2.7.4" },
953
+ }));
954
+ }
955
+
956
+ if (dep.noSonarFlag) {
957
+ const m =
958
+ "DEPRECATED: `--no-sonar` flag is ignored since v2.7.4. Sonar runs for code " +
959
+ "tasks by policy. To skip Sonar on a one-off run, use a non-code task type " +
960
+ "(e.g. `--task-type doc`) or rely on Solomon's runtime override.";
961
+ logger.warn(m);
962
+ emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
963
+ message: m,
964
+ detail: { flag: "--no-sonar", since: "v2.7.4" },
965
+ }));
966
+ }
967
+ }
968
+
923
969
  async function ensureAddyosmaniSkills({ task, config, logger, session, emitter, eventBase }) {
924
970
  const skillsConfig = config?.skills || {};
925
971
  const sources = Array.isArray(skillsConfig.sources) ? skillsConfig.sources : ["addyosmani", "openskills", "local"];
@@ -1173,9 +1219,17 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
1173
1219
  if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
1174
1220
  if (tddResult.action === "continue") return { action: "continue" };
1175
1221
 
1176
- const skipSonarForTaskType = new Set(["infra", "doc", "no-code"]);
1177
- const effectiveTaskType = session.resolved_policies?.taskType || null;
1178
- if (config.sonarqube.enabled && !skipSonarForTaskType.has(effectiveTaskType)) {
1222
+ // Sonar runs for code tasks per policy. Since v2.7.4 it is NOT
1223
+ // toggleable via config — that's intrinsic to Karajan. The taskType
1224
+ // policy (resolved_policies.sonar) is the single source of truth.
1225
+ // Solomon may skip a single iteration via rule alerts; that's a
1226
+ // runtime decision, not a config option.
1227
+ //
1228
+ // Test-only escape hatch: globalThis.__KJ_DISABLE_SONAR_STAGE forces
1229
+ // the sonar stage off so tests don't have to spin up Docker. Set in
1230
+ // tests/setup.js by default; production code never sees it.
1231
+ const sonarStageDisabledForTest = globalThis.__KJ_DISABLE_SONAR_STAGE === true;
1232
+ if (!sonarStageDisabledForTest && session.resolved_policies?.sonar !== false) {
1179
1233
  const sonarResult = await runSonarStage({
1180
1234
  config, logger, emitter, eventBase, session, trackBudget, iteration: i,
1181
1235
  repeatDetector, budgetSummary, sonarState, askQuestion, task, brainCtx
@@ -1864,7 +1918,11 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1864
1918
  } catch (initError) {
1865
1919
  // Pre-loop stage failure → Solomon decides
1866
1920
  logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
1867
- const tempSession = { id: "init-error", task, status: "failed" };
1921
+ // tempSession needs `checkpoints` because invokeSolomon → addCheckpoint
1922
+ // pushes into it. Pre-v2.7.4 this lacked the array and crashed with
1923
+ // "Cannot read properties of undefined (reading 'push')" the moment
1924
+ // Solomon was consulted on a preflight failure.
1925
+ const tempSession = { id: "init-error", task, status: "failed", checkpoints: [] };
1868
1926
  const solomonResult = await invokeSolomon({
1869
1927
  config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
1870
1928
  stage: "init_error", askQuestion, session: tempSession, iteration: 0,
@@ -168,7 +168,21 @@ async function checkSecurityAgent(config) {
168
168
  * @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[], errors: object[] }}
169
169
  */
170
170
  export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
171
- const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
171
+ // Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests).
172
+ // Since v2.7.4 it is NOT toggleable via `config.sonarqube.enabled` —
173
+ // that field is ignored (deprecation warning emitted at config load).
174
+ // The taskType policy (resolved_policies.sonar from DEFAULT_POLICIES in
175
+ // src/guards/policy-resolver.js) is the single source of truth: sw /
176
+ // refactor / add-tests run Sonar, audit / doc / infra / analysis /
177
+ // no-code skip it. Solomon may decide to skip a single iteration via
178
+ // rule alerts — that's a runtime decision, not a config option.
179
+ //
180
+ // Test-only escape hatch: globalThis.__KJ_DISABLE_SONAR_STAGE === true
181
+ // forces sonar OFF in preflight too, so tests that don't want sonar
182
+ // events in their expected sequence don't have to set up Docker mocks.
183
+ // Set in tests/setup.js by default; production code never sees it.
184
+ const sonarStageDisabledForTest = globalThis.__KJ_DISABLE_SONAR_STAGE === true;
185
+ const sonarEnabled = !sonarStageDisabledForTest && resolvedPolicies.sonar !== false;
172
186
  const isExternalSonar = Boolean(config.sonarqube?.external);
173
187
  const sonarHost = resolveSonarHost(config.sonarqube?.host);
174
188
 
@@ -44,6 +44,12 @@ export async function loadSession(sessionId) {
44
44
  }
45
45
 
46
46
  export async function addCheckpoint(session, checkpoint) {
47
+ // Defensive: any caller that builds a stub session (e.g. the init-error
48
+ // path in flow-runner.js, where `tempSession = { id, task, status }` has
49
+ // no checkpoints array) would otherwise crash with
50
+ // "Cannot read properties of undefined (reading 'push')". Guard once here
51
+ // so the bug class is gone instead of fixing every call site.
52
+ if (!Array.isArray(session.checkpoints)) session.checkpoints = [];
47
53
  session.checkpoints.push({ at: new Date().toISOString(), ...checkpoint });
48
54
  await saveSession(session);
49
55
  }