karajan-code 2.7.4 → 2.9.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 (192) hide show
  1. package/README.md +1 -1
  2. package/package.json +24 -4
  3. package/scripts/diet-baseline.mjs +170 -0
  4. package/scripts/regen-arch-stats.sh +71 -0
  5. package/src/agents/availability.js +15 -9
  6. package/src/agents/claude-agent.js +143 -45
  7. package/src/agents/model-registry.js +1 -1
  8. package/src/audit/agent-readiness.js +240 -0
  9. package/src/audit/basal-cost.js +92 -9
  10. package/src/audit/deterministic-summary.js +200 -0
  11. package/src/audit/osv-findings.js +162 -0
  12. package/src/audit/semgrep-findings.js +117 -0
  13. package/src/audit/sonar-findings.js +81 -0
  14. package/src/audit/webperf-input.js +90 -0
  15. package/src/bootstrap.js +26 -1
  16. package/src/brain/decision-tracker.js +3 -1
  17. package/src/brain/decisor.js +1 -1
  18. package/src/canvas/parse-md.js +238 -0
  19. package/src/canvas/schema.js +101 -0
  20. package/src/canvas/to-flat.js +85 -0
  21. package/src/canvas/validate.js +163 -0
  22. package/src/checks/binaries.js +3 -3
  23. package/src/checks/ci.js +2 -2
  24. package/src/checks/config-files.js +6 -6
  25. package/src/checks/dir-setup.js +4 -0
  26. package/src/checks/mcp-health.js +6 -0
  27. package/src/checks/ports.js +91 -29
  28. package/src/checks/rtk.js +1 -1
  29. package/src/checks/skills.js +5 -1
  30. package/src/checks/sonar.js +2 -2
  31. package/src/checks/system.js +3 -3
  32. package/src/checks/tokens.js +1 -1
  33. package/src/checks/types.js +0 -12
  34. package/src/cli/_shared.js +30 -0
  35. package/src/cli/register-meta.js +218 -0
  36. package/src/cli/register-pipeline.js +212 -0
  37. package/src/cli/register-plan.js +152 -0
  38. package/src/cli/register-roles-skills.js +38 -0
  39. package/src/cli/register-sonar.js +28 -0
  40. package/src/cli.js +83 -468
  41. package/src/commands/agents.js +18 -6
  42. package/src/commands/architect.js +29 -2
  43. package/src/commands/audit.js +265 -30
  44. package/src/commands/board.js +160 -30
  45. package/src/commands/clean.js +83 -0
  46. package/src/commands/code.js +7 -2
  47. package/src/commands/discover.js +7 -2
  48. package/src/commands/doctor.js +1 -1
  49. package/src/commands/init.js +2 -2
  50. package/src/commands/plan/_shared.js +40 -0
  51. package/src/commands/plan/add-hu.js +21 -0
  52. package/src/commands/plan/delete.js +9 -0
  53. package/src/commands/plan/generate.js +284 -0
  54. package/src/commands/plan/index.js +14 -0
  55. package/src/commands/plan/list.js +21 -0
  56. package/src/commands/plan/ready.js +46 -0
  57. package/src/commands/plan/remove-hu.js +17 -0
  58. package/src/commands/plan/show.js +81 -0
  59. package/src/commands/plan/validate.js +20 -0
  60. package/src/commands/plan.js +14 -291
  61. package/src/commands/researcher.js +7 -2
  62. package/src/commands/resume.js +24 -1
  63. package/src/commands/review.js +7 -2
  64. package/src/commands/run.js +52 -1
  65. package/src/commands/status.js +1 -1
  66. package/src/commands/sync.js +75 -0
  67. package/src/commands/triage.js +7 -2
  68. package/src/commands/undo.js +1 -1
  69. package/src/config/defaults.js +213 -0
  70. package/src/config/loader.js +137 -0
  71. package/src/config/overrides.js +199 -0
  72. package/src/config/role-resolver.js +159 -0
  73. package/src/config/test-harness.js +78 -0
  74. package/src/config.js +30 -632
  75. package/src/git/automation.js +2 -2
  76. package/src/guards/output-guard.js +0 -2
  77. package/src/hu/acceptance-runner.js +52 -10
  78. package/src/hu/store.js +11 -0
  79. package/src/mcp/handlers/direct-handlers.js +2 -5
  80. package/src/mcp/handlers/management-handlers.js +48 -4
  81. package/src/mcp/handlers/run-handler.js +9 -8
  82. package/src/mcp/preflight.js +32 -13
  83. package/src/mcp/server-handlers.js +4 -3
  84. package/src/mcp/suggest-handler.js +3 -2
  85. package/src/mcp/tools.js +15 -0
  86. package/src/orchestrator/agent-fallback.js +1 -1
  87. package/src/orchestrator/ci-integration.js +11 -11
  88. package/src/orchestrator/config-init.js +25 -7
  89. package/src/orchestrator/direct-actions.js +7 -3
  90. package/src/orchestrator/drivers/error-recovery.js +287 -0
  91. package/src/orchestrator/drivers/init-context.js +222 -0
  92. package/src/orchestrator/drivers/iteration-loop.js +301 -0
  93. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  94. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  95. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  96. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  97. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  98. package/src/orchestrator/drivers/post-loop.js +370 -0
  99. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  100. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  101. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  102. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  103. package/src/orchestrator/drivers/pre-loop.js +431 -0
  104. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  105. package/src/orchestrator/feedback-queue.js +0 -1
  106. package/src/orchestrator/flow-control.js +4 -3
  107. package/src/orchestrator/flow-runner.js +121 -2077
  108. package/src/orchestrator/fs-leak-detector.js +156 -0
  109. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  110. package/src/orchestrator/integrations.js +81 -0
  111. package/src/orchestrator/post-loop-stages.js +35 -8
  112. package/src/orchestrator/preflight-checks.js +20 -7
  113. package/src/orchestrator/repair/repair-detector.js +117 -0
  114. package/src/orchestrator/repair/repair-runner.js +142 -0
  115. package/src/orchestrator/reviewer-fallback.js +1 -1
  116. package/src/orchestrator/role-output-compressor.js +0 -1
  117. package/src/orchestrator/session-journal.js +4 -41
  118. package/src/orchestrator/solomon-escalation.js +3 -2
  119. package/src/orchestrator/solomon-rules.js +7 -3
  120. package/src/orchestrator/stages/architect-stage.js +12 -11
  121. package/src/orchestrator/stages/coder-stage.js +56 -7
  122. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  123. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  124. package/src/orchestrator/stages/planner-stage.js +1 -1
  125. package/src/orchestrator/stages/research-stage.js +1 -1
  126. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  127. package/src/orchestrator/stages/sonar-stage.js +94 -10
  128. package/src/orchestrator/stages/stage-classes.js +84 -0
  129. package/src/orchestrator/stages/stage-executor.js +1 -1
  130. package/src/orchestrator/stages/triage-stage.js +1 -1
  131. package/src/orchestrator/standby.js +3 -2
  132. package/src/orchestrator/verification-gate.js +20 -12
  133. package/src/plan/adr-generator.js +170 -0
  134. package/src/plan/adr-loader.js +60 -0
  135. package/src/plan/plan-executor.js +19 -1
  136. package/src/plan/plan-hu-ops.js +170 -1
  137. package/src/plan/plan-reviewer.js +199 -0
  138. package/src/plan/plan-schema.js +69 -1
  139. package/src/plan/plan-store.js +40 -17
  140. package/src/plan/spec-detector.js +61 -0
  141. package/src/plan/tests-synthesizer.js +116 -0
  142. package/src/planning-game/client.js +3 -28
  143. package/src/planning-game/decomposition.js +1 -1
  144. package/src/planning-game/pipeline-adapter.js +37 -6
  145. package/src/prompts/audit.js +206 -6
  146. package/src/prompts/coder.js +172 -1
  147. package/src/prompts/hu-reviewer.js +0 -1
  148. package/src/prompts/planner.js +81 -4
  149. package/src/prompts/rtk-snippet.js +1 -1
  150. package/src/review/diff-generator.js +35 -22
  151. package/src/review/snapshot-diff.js +1 -1
  152. package/src/roles/audit-role.js +125 -13
  153. package/src/roles/coder-role.js +25 -3
  154. package/src/roles/planner-role.js +42 -1
  155. package/src/roles/repairer-role.js +181 -0
  156. package/src/roles/tester-role.js +70 -4
  157. package/src/session/mutators.js +304 -0
  158. package/src/session/runtime-overrides.js +59 -0
  159. package/src/{session-store.js → session/store.js} +2 -2
  160. package/src/skills/semantic-detector.js +10 -5
  161. package/src/skills/skill-detector.js +1 -1
  162. package/src/sonar/api.js +1 -1
  163. package/src/sonar/cloud-scanner.js +0 -1
  164. package/src/sonar/credentials.js +14 -6
  165. package/src/sonar/discovery.js +123 -0
  166. package/src/sonar/manager.js +18 -0
  167. package/src/sync/detect-drift.js +113 -0
  168. package/src/tracker-bootstrap.js +44 -0
  169. package/src/utils/agent-detect.js +15 -11
  170. package/src/utils/board-prompt-bridge.js +109 -0
  171. package/src/utils/cli-progress.js +161 -0
  172. package/src/utils/cwd-confirm.js +39 -0
  173. package/src/utils/derive-project-name-from-cwd.js +119 -0
  174. package/src/utils/display/event-handlers.js +1 -1
  175. package/src/utils/display/header.js +1 -1
  176. package/src/utils/display/session-summary.js +3 -3
  177. package/src/utils/display/solomon.js +1 -1
  178. package/src/utils/garbage-collector.js +396 -0
  179. package/src/utils/git.js +16 -4
  180. package/src/utils/locale.js +1 -1
  181. package/src/utils/paths.js +21 -1
  182. package/src/utils/process.js +1 -1
  183. package/src/utils/prompt-project-name.js +35 -0
  184. package/src/utils/role-assigner.js +1 -1
  185. package/src/utils/run-context.js +75 -0
  186. package/src/utils/run-log.js +12 -0
  187. package/src/utils/task-file.js +1 -1
  188. package/src/utils/telemetry.js +13 -1
  189. package/src/webperf/devtools-detect.js +1 -1
  190. package/templates/coder-rules.md +13 -0
  191. package/templates/roles/coder.md +34 -0
  192. package/templates/roles/repairer.md +94 -0
@@ -48,16 +48,26 @@ export class TesterRole extends AgentRole {
48
48
  }
49
49
 
50
50
  extractInput(input) {
51
- if (typeof input === "string") return { task: input, diff: null, sonarIssues: null };
51
+ if (typeof input === "string") return { task: input, diff: null, sonarIssues: null, pendingGherkinTests: null, shellTestResults: null };
52
52
  return {
53
53
  task: input?.task || this.context?.task || "",
54
54
  diff: input?.diff || null,
55
55
  sonarIssues: input?.sonarIssues || null,
56
+ // Tests-first Phase 3 (v2.7.5): the tester now has two extra inputs.
57
+ // - pendingGherkinTests: HU-declared Gherkin scenarios that need
58
+ // translation into executable code tests in the project's
59
+ // framework. The tester writes the test files and includes them
60
+ // in the suite run before computing verdict.
61
+ // - shellTestResults: results of running the HU's shell-type
62
+ // acceptance_tests (already run upstream, passed at this point).
63
+ // Passed for context so the tester doesn't re-run them redundantly.
64
+ pendingGherkinTests: input?.pendingGherkinTests || null,
65
+ shellTestResults: input?.shellTestResults || null,
56
66
  onOutput: input?.onOutput || null
57
67
  };
58
68
  }
59
69
 
60
- async buildPrompt({ task, diff, sonarIssues }) {
70
+ async buildPrompt({ task, diff, sonarIssues, pendingGherkinTests, shellTestResults }) {
61
71
  const projectDir = this.config?.projectDir || process.cwd();
62
72
  const detection = await detectTestFramework(projectDir);
63
73
 
@@ -97,14 +107,65 @@ export class TesterRole extends AgentRole {
97
107
  );
98
108
  }
99
109
 
110
+ // Tests-first Phase 3 (v2.7.5): include the upstream shell-test results
111
+ // so the tester knows which part of the contract is already green.
112
+ if (Array.isArray(shellTestResults) && shellTestResults.length > 0) {
113
+ const passedCount = shellTestResults.filter((r) => r.passed).length;
114
+ sections.push(
115
+ "## Acceptance gate — shell tests",
116
+ `The HU's shell-type acceptance_tests already ran upstream: ${passedCount}/${shellTestResults.length} passed.`,
117
+ "Do NOT re-run them. Use the coverage/translation steps below to decide your verdict."
118
+ );
119
+ }
120
+
121
+ // Gherkin translation block — only when the HU carries Gherkin
122
+ // scenarios. Tester must produce real test files (in the detected
123
+ // framework's language) that encode Given/When/Then. This is how
124
+ // behaviour-level specs become enforceable.
125
+ if (Array.isArray(pendingGherkinTests) && pendingGherkinTests.length > 0) {
126
+ sections.push(
127
+ "## Gherkin acceptance tests to translate",
128
+ "These Gherkin scenarios are the HU's behavioural contract. They are NOT yet",
129
+ "executable — your job is to turn each one into a concrete test case in the",
130
+ "project's test framework, WRITE the test files, and INCLUDE them in the suite",
131
+ "run that determines your verdict.",
132
+ "",
133
+ "For each scenario below:",
134
+ "1. Pick the appropriate test file (use `target` hint if present; otherwise",
135
+ " put the test next to the module it exercises).",
136
+ "2. Write a test that asserts the Given/When/Then as-is. Do NOT soften the",
137
+ " assertion — if it's ambiguous, pick the stricter reading.",
138
+ "3. Make sure the test runs as part of the normal suite (no separate invocation).",
139
+ "",
140
+ "If ANY translated test fails on the current code, return verdict: \"fail\" with",
141
+ "a failing_scenarios array listing the scenarios that didn't pass. The coder",
142
+ "will iterate.",
143
+ ""
144
+ );
145
+ pendingGherkinTests.forEach((entry, i) => {
146
+ const content = typeof entry === "string" ? entry : entry?.content || "";
147
+ const target = entry && entry.file ? ` · target: \`${entry.file}\`` : "";
148
+ sections.push(
149
+ `### Scenario ${i + 1}${target}`,
150
+ "```gherkin",
151
+ content,
152
+ "```",
153
+ ""
154
+ );
155
+ });
156
+ }
157
+
100
158
  sections.push(
101
159
  "",
102
160
  "Return ONLY a single valid JSON object:",
103
- '{"tests_pass":boolean,"coverage":{"overall":number,"services":number,"utilities":number},"missing_scenarios":[string],"quality_issues":[string],"verdict":"pass"|"fail"}',
161
+ '{"tests_pass":boolean,"coverage":{"overall":number,"services":number,"utilities":number},"missing_scenarios":[string],"quality_issues":[string],"failing_scenarios":[string],"translated_scenarios":[string],"verdict":"pass"|"fail"}',
104
162
  "",
105
163
  "- coverage.overall MUST be a real number from the test runner output, NOT an estimate",
106
164
  "- If coverage tool is not available, set coverage.overall to null (not 0, not a guess)",
107
165
  "- tests_pass must reflect whether the actual test run succeeded",
166
+ "- failing_scenarios: when Gherkin was given, list the scenarios your translated tests CAUGHT as failing",
167
+ "- translated_scenarios: when Gherkin was given, list the scenarios you successfully turned into passing tests",
168
+ "- verdict MUST be \"fail\" if any translated scenario still fails (even if coverage is green)",
108
169
  `## Task\n${task}`
109
170
  );
110
171
  if (diff) sections.push(`## Git diff\n${diff}`);
@@ -136,6 +197,9 @@ export class TesterRole extends AgentRole {
136
197
  coverage: parsed.coverage || {},
137
198
  missing_scenarios: parsed.missing_scenarios || [],
138
199
  quality_issues: parsed.quality_issues || [],
200
+ // Tests-first Phase 3: Gherkin translation outcomes.
201
+ failing_scenarios: Array.isArray(parsed.failing_scenarios) ? parsed.failing_scenarios : [],
202
+ translated_scenarios: Array.isArray(parsed.translated_scenarios) ? parsed.translated_scenarios : [],
139
203
  verdict,
140
204
  provider
141
205
  };
@@ -147,6 +211,8 @@ export class TesterRole extends AgentRole {
147
211
  const coverageStr = coverage.overall != null ? `${coverage.overall}%` : "not measured";
148
212
  const missingPart = parsed.missing_scenarios?.length ? `; ${parsed.missing_scenarios.length} missing scenario(s)` : "";
149
213
  const qualityPart = parsed.quality_issues?.length ? `; ${parsed.quality_issues.length} quality issue(s)` : "";
150
- return `Verdict: ${verdict}; Coverage: ${coverageStr}${missingPart}${qualityPart}`;
214
+ const failingPart = parsed.failing_scenarios?.length ? `; ${parsed.failing_scenarios.length} failing scenario(s)` : "";
215
+ const translatedPart = parsed.translated_scenarios?.length ? `; ${parsed.translated_scenarios.length} scenario(s) translated` : "";
216
+ return `Verdict: ${verdict}; Coverage: ${coverageStr}${translatedPart}${failingPart}${missingPart}${qualityPart}`;
151
217
  }
152
218
  }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Session mutators — the single site where the orchestrator mutates a
3
+ * session object in memory.
4
+ *
5
+ * Rationale (TSK-0337, KJC-PCS-0040 Oleada 3): pre-v2.7.5 there were ~68
6
+ * direct `session.x = value` assignments scattered across 18 files, with
7
+ * one file (flow-runner.js) alone accounting for 142 touches. The audit
8
+ * called this out as a HIGH finding: when a reviewer-retry bug landed in
9
+ * production, a maintainer had to grep the whole tree to find every site
10
+ * that writes to `session.last_reviewer_feedback` or
11
+ * `session.reviewer_retry_count`.
12
+ *
13
+ * The mutators below are intentionally **mechanical** — each wraps a
14
+ * single assignment — so the migration from `session.x = y` to
15
+ * `setX(session, y)` is pure plumbing and preserves exact existing
16
+ * semantics. The value is NOT validation or side-effects; it's that
17
+ * grepping `setReviewerFeedback(` finds every writer in seconds. Adding
18
+ * validation, event emission or persistence hooks later becomes a
19
+ * one-liner inside the mutator rather than a tree-wide edit.
20
+ *
21
+ * Complementary architecture test:
22
+ * tests/architecture/session-write-boundary.test.js
23
+ * enforces a budget on the remaining direct `session.x = y` assignments
24
+ * outside `src/session/`. The budget only decreases; any new direct
25
+ * assignment requires either going through a mutator or explicit audit.
26
+ *
27
+ * Naming convention:
28
+ * - setX — overwrite field X with a new value.
29
+ * - incrementX — numeric +1 (returns the new value).
30
+ * - resetX — numeric 0 or null.
31
+ * - clearX — delete the field entirely (when `in` matters).
32
+ * - appendToX — push onto an array field.
33
+ * - assignX — shallow-merge object into an object field.
34
+ *
35
+ * No mutator persists the session — callers are still responsible for
36
+ * `await saveSession(session)` afterwards. This keeps the mutator
37
+ * functions synchronous and cheap.
38
+ */
39
+
40
+ // --- Retry counters -------------------------------------------------------
41
+
42
+ export const RETRY_KINDS = Object.freeze([
43
+ "reviewer",
44
+ "sonar",
45
+ "tester",
46
+ "security",
47
+ "standby",
48
+ "repeated_issue",
49
+ "auto_resume",
50
+ "checkpoint_stall",
51
+ ]);
52
+
53
+ const RETRY_FIELD = {
54
+ reviewer: "reviewer_retry_count",
55
+ sonar: "sonar_retry_count",
56
+ tester: "tester_retry_count",
57
+ security: "security_retry_count",
58
+ standby: "standby_retry_count",
59
+ repeated_issue: "repeated_issue_count",
60
+ auto_resume: "auto_resume_count",
61
+ checkpoint_stall: "_checkpoint_stall_count",
62
+ };
63
+
64
+ export function setRetryCount(session, kind, value) {
65
+ const field = RETRY_FIELD[kind];
66
+ if (!field) throw new Error(`setRetryCount: unknown retry kind "${kind}"`);
67
+ session[field] = Number(value) || 0;
68
+ return session[field];
69
+ }
70
+
71
+ export function incrementRetryCount(session, kind) {
72
+ const field = RETRY_FIELD[kind];
73
+ if (!field) throw new Error(`incrementRetryCount: unknown retry kind "${kind}"`);
74
+ session[field] = (session[field] || 0) + 1;
75
+ return session[field];
76
+ }
77
+
78
+ export function resetRetryCount(session, kind) {
79
+ return setRetryCount(session, kind, 0);
80
+ }
81
+
82
+ /**
83
+ * Reset all well-known retry/repeat counters to 0. Called at the top of
84
+ * every resume path so a resumed session doesn't inherit the previous
85
+ * run's stall state.
86
+ */
87
+ export function resetAllRetryCounters(session) {
88
+ for (const kind of RETRY_KINDS) {
89
+ // Skip checkpoint_stall: it's internal state owned by flow-control,
90
+ // not something resume should touch.
91
+ if (kind === "checkpoint_stall") continue;
92
+ setRetryCount(session, kind, 0);
93
+ }
94
+ }
95
+
96
+ // --- Feedback -------------------------------------------------------------
97
+
98
+ export function setReviewerFeedback(session, text) {
99
+ session.last_reviewer_feedback = text;
100
+ }
101
+
102
+ export function appendReviewerFeedback(session, text) {
103
+ session.last_reviewer_feedback = session.last_reviewer_feedback
104
+ ? `${session.last_reviewer_feedback}\n${text}`
105
+ : text;
106
+ }
107
+
108
+ export function setSonarSummary(session, text) {
109
+ session.last_sonar_summary = text;
110
+ }
111
+
112
+ export function setSonarIssueSignature(session, sig) {
113
+ session.last_sonar_issue_signature = sig;
114
+ }
115
+
116
+ export function setReviewerIssueSignature(session, sig) {
117
+ session.last_reviewer_issue_signature = sig;
118
+ }
119
+
120
+ // Issue-signature repetition counters (distinct from retry_count:
121
+ // reset only when the signature changes, not every iteration).
122
+ export function setSonarRepeatCount(session, value) {
123
+ session.sonar_repeat_count = Number(value) || 0;
124
+ }
125
+ export function setReviewerRepeatCount(session, value) {
126
+ session.reviewer_repeat_count = Number(value) || 0;
127
+ }
128
+
129
+ // --- Status ---------------------------------------------------------------
130
+
131
+ /**
132
+ * In-memory status change. The persisting variant is `markSessionStatus` in
133
+ * store.js, which also writes to disk; `setStatus` is for transient mutations
134
+ * inside the runner (e.g. standby enter/exit) where the caller persists
135
+ * later via `saveSession`.
136
+ */
137
+ export function setStatus(session, status) {
138
+ session.status = status;
139
+ }
140
+
141
+ // --- Alternative agent (rate-limit recovery) ------------------------------
142
+
143
+ export function setAlternativeAgent(session, stage, provider) {
144
+ session._alternative_agent = { stage, provider };
145
+ }
146
+
147
+ export function clearAlternativeAgent(session) {
148
+ delete session._alternative_agent;
149
+ }
150
+
151
+ // --- Standby state --------------------------------------------------------
152
+
153
+ export function setStandby(session, { agent, cooldownMs, cooldownUntil }) {
154
+ session.standby_agent = agent;
155
+ session.standby_cooldown_ms = cooldownMs;
156
+ session.standby_cooldown_until = cooldownUntil;
157
+ }
158
+
159
+ export function clearStandby(session) {
160
+ delete session.standby_agent;
161
+ delete session.standby_cooldown_ms;
162
+ delete session.standby_cooldown_until;
163
+ }
164
+
165
+ // --- CI state -------------------------------------------------------------
166
+
167
+ export function setCiEarlyPr(session, { prNumber, prUrl, commits }) {
168
+ session.ci_pr_number = prNumber;
169
+ session.ci_pr_url = prUrl;
170
+ session.ci_commits = commits;
171
+ }
172
+
173
+ export function appendCiCommits(session, commits) {
174
+ session.ci_commits = [...(session.ci_commits ?? []), ...commits];
175
+ }
176
+
177
+ // --- Pre-loop & policy metadata -------------------------------------------
178
+
179
+ export function setPreflight(session, result) {
180
+ session.preflight = result;
181
+ }
182
+
183
+ export function setResolvedPolicies(session, policies) {
184
+ session.resolved_policies = policies;
185
+ }
186
+
187
+ export function setPgCard(session, card) {
188
+ session.pg_card = card;
189
+ }
190
+
191
+ export function setPgTaskId(session, id) {
192
+ session.pg_task_id = id;
193
+ }
194
+
195
+ export function appendPgCommits(session, commits) {
196
+ session.pg_commits = [...(session.pg_commits ?? []), ...commits];
197
+ }
198
+
199
+ export function setPreLoopContext(session, { research, architect, plan }) {
200
+ if (research !== undefined) session.research_context = research;
201
+ if (architect !== undefined) session.architect_context = architect;
202
+ if (plan !== undefined) session.loaded_plan = plan;
203
+ }
204
+
205
+ export function setPlanRef(session, ref) {
206
+ session._planRef = ref;
207
+ }
208
+
209
+ export function setPlanSyncCallback(session, fn) {
210
+ session._syncResultsToPlan = fn;
211
+ }
212
+
213
+ /**
214
+ * Live status updater: invoked by hu-sub-pipeline after every status
215
+ * change so the plan JSON gets the new status immediately, not only at
216
+ * the end of the run. Without this, the board reads an unchanged plan
217
+ * file during execution and the columns appear frozen until the run
218
+ * completes (the bug we're fixing in PR1).
219
+ *
220
+ * Signature: async (huId, status) => void
221
+ */
222
+ export function setLiveStatusUpdater(session, fn) {
223
+ session._liveStatusUpdater = fn;
224
+ }
225
+
226
+ export function getLiveStatusUpdater(session) {
227
+ return session._liveStatusUpdater || null;
228
+ }
229
+
230
+ /**
231
+ * Live outcome updater (PR3): invoked by hu-sub-pipeline after each
232
+ * HU's runSingleHu finishes. The plan JSON gets the per-HU outcome
233
+ * stamped immediately so the board can show a "📄 ver resumen"
234
+ * indicator next to each card without waiting for the run to end.
235
+ *
236
+ * Signature: async (huId, outcome) => void
237
+ */
238
+ export function setLiveOutcomeUpdater(session, fn) {
239
+ session._liveOutcomeUpdater = fn;
240
+ }
241
+
242
+ export function getLiveOutcomeUpdater(session) {
243
+ return session._liveOutcomeUpdater || null;
244
+ }
245
+
246
+ // --- Skills ---------------------------------------------------------------
247
+
248
+ export function setAddyosmaniSkills(session, payload) {
249
+ session.addyosmaniSkills = payload;
250
+ }
251
+
252
+ export function setAutoInstalledSkills(session, skills) {
253
+ session.autoInstalledSkills = skills;
254
+ }
255
+
256
+ export function setSkillsRecommended(session, skills) {
257
+ session.skillsRecommended = skills;
258
+ }
259
+
260
+ // --- Journal breadcrumbs --------------------------------------------------
261
+
262
+ export function setJournalContext(session, { dir, files, iterations, decisions, startedAt }) {
263
+ if (dir !== undefined) session._journalDir = dir;
264
+ if (files !== undefined) session._journalFiles = files;
265
+ if (iterations !== undefined) session._journalIterations = iterations;
266
+ if (decisions !== undefined) session._journalDecisions = decisions;
267
+ if (startedAt !== undefined) session._startedAt = startedAt;
268
+ }
269
+
270
+ // --- Session outcome ------------------------------------------------------
271
+
272
+ export function setBudget(session, budget) {
273
+ session.budget = budget;
274
+ }
275
+
276
+ export function setDeferredIssues(session, issues) {
277
+ session.deferred_issues = issues;
278
+ }
279
+
280
+ export function setRtkSavings(session, savings) {
281
+ session.rtk_savings = savings;
282
+ }
283
+
284
+ // --- Paused state (resume) ------------------------------------------------
285
+
286
+ export function setPausedState(session, state) {
287
+ session.paused_state = state;
288
+ }
289
+
290
+ // --- Brain decisions (decisor tracker) -----------------------------------
291
+
292
+ export function setBrainDecisions(session, decisions) {
293
+ session.brainDecisions = decisions;
294
+ }
295
+
296
+ // --- Suggestions (MCP) ----------------------------------------------------
297
+
298
+ export function setSuggestions(session, suggestions) {
299
+ session.suggestions = suggestions;
300
+ }
301
+
302
+ export function touch(session) {
303
+ session.updated_at = new Date().toISOString();
304
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * In-memory runtime overrides (e.g. `roles.coder.provider` set for the
3
+ * current process session, not persisted to disk).
4
+ *
5
+ * Why this lives at the session layer (not under `src/mcp/`):
6
+ *
7
+ * Pre-v2.7.5, the overrides store lived in `src/mcp/preflight.js`. That
8
+ * made `src/commands/agents.js` (CLI) do
9
+ * `await import("../mcp/preflight.js")`
10
+ * which is a **layer violation**: commands are the top layer, MCP is a
11
+ * peer interface. A CLI command knowing about the MCP server's
12
+ * preflight state coupled two runtimes that should be independent.
13
+ *
14
+ * The "runtime override" concept is runtime-agnostic — both CLI and MCP
15
+ * need an in-memory scratchpad for "this process should use this
16
+ * provider for role X for the rest of its life". Putting it here makes
17
+ * both layers depend on `src/session/` (a neutral module) instead of on
18
+ * each other.
19
+ *
20
+ * Lifetime: the store lives for as long as the Node process. CLI
21
+ * invocations are short-lived (override is lost at exit — that's fine,
22
+ * they write to config if they want persistence). The MCP server process
23
+ * is long-lived; the override survives across tool calls until
24
+ * `clearRuntimeOverrides()` is called (typically by `ackPreflight()`).
25
+ *
26
+ * Contents: a flat `{ [key]: value }` map. Keys are typically role names
27
+ * (`coder`, `reviewer`, `planner`), values are provider slugs (`claude`,
28
+ * `codex`, `gemini`). Consumers shape-validate at read time.
29
+ */
30
+
31
+ let overrides = {};
32
+
33
+ /**
34
+ * Return a shallow copy of the current overrides. Never returns the
35
+ * internal reference so callers cannot mutate it by accident.
36
+ */
37
+ export function getRuntimeOverrides() {
38
+ return { ...overrides };
39
+ }
40
+
41
+ /**
42
+ * Set a single override. Silently rejects nullish keys.
43
+ *
44
+ * @param {string} key
45
+ * @param {unknown} value
46
+ */
47
+ export function setRuntimeOverride(key, value) {
48
+ if (!key) return;
49
+ overrides[key] = value;
50
+ }
51
+
52
+ /**
53
+ * Drop all overrides. Used by `ackPreflight()` after it transfers its
54
+ * configured overrides onto this store, to ensure we start from a clean
55
+ * slate per MCP session.
56
+ */
57
+ export function clearRuntimeOverrides() {
58
+ overrides = {};
59
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { ensureDir, exists } from "./utils/fs.js";
4
- import { getSessionRoot } from "./utils/paths.js";
3
+ import { ensureDir, exists } from "../utils/fs.js";
4
+ import { getSessionRoot } from "../utils/paths.js";
5
5
 
6
6
  const SESSION_ROOT = getSessionRoot();
7
7
 
@@ -129,18 +129,23 @@ export async function refineSkillsSemantically({ task, alreadyDetected = [], con
129
129
 
130
130
  /**
131
131
  * Resolve the effective skills mode from config + flags.
132
- * Precedence: flags.skillsMode > config.skills.mode > globalThis default > "auto".
132
+ * Precedence: flags.skillsMode > config.skills.mode > config.testHarness.defaultSkillsMode > "auto".
133
133
  *
134
- * The global default lets the test harness opt out of semantic detection
135
- * without each orchestrator test having to mock this module (tests/setup.js
136
- * sets globalThis.__KJ_DEFAULT_SKILLS_MODE = "regex").
134
+ * The testHarness default lets the test harness opt out of semantic
135
+ * detection without each orchestrator test having to mock this module.
136
+ * Production code reads `config.testHarness.defaultSkillsMode` only;
137
+ * the legacy global override is documented and exclusively read in
138
+ * src/config/test-harness.js (ESLint rule #557 blocks any reintroduction
139
+ * of `globalThis.__KJ_*` outside that one file). Legacy callers without
140
+ * a fully-loaded config still work because `config.testHarness` is
141
+ * undefined → falls through to `"auto"`.
137
142
  *
138
143
  * @param {Object} config
139
144
  * @param {Object} [flags]
140
145
  * @returns {"auto"|"regex"|"semantic"|"none"}
141
146
  */
142
147
  export function resolveSkillsMode(config, flags = {}) {
143
- const defaultMode = globalThis.__KJ_DEFAULT_SKILLS_MODE || "auto";
148
+ const defaultMode = config?.testHarness?.defaultSkillsMode || "auto";
144
149
  const candidate = flags.skillsMode ?? config?.skills?.mode ?? defaultMode;
145
150
  const valid = new Set(["auto", "regex", "semantic", "none"]);
146
151
  return valid.has(candidate) ? candidate : "auto";
@@ -226,7 +226,7 @@ async function collectPythonDeps(projectDir, needed) {
226
226
  for (const line of raw.split("\n")) {
227
227
  const trimmed = line.trim().split("#")[0].trim();
228
228
  if (!trimmed) continue;
229
- const name = trimmed.split(/[<>=!~\[\s]/)[0].toLowerCase();
229
+ const name = trimmed.split(/[<>=!~[\s]/)[0].toLowerCase();
230
230
  const skill = PYTHON_DEPS[name];
231
231
  if (skill) needed.add(skill);
232
232
  }
package/src/sonar/api.js CHANGED
@@ -3,7 +3,7 @@ import { withRetry } from "../utils/retry.js";
3
3
  import { resolveSonarProjectKey } from "./project-key.js";
4
4
  import { resolveSonarToken } from "./config-resolver.js";
5
5
 
6
- export class SonarApiError extends Error {
6
+ class SonarApiError extends Error {
7
7
  constructor(message, { url, httpStatus, hint } = {}) {
8
8
  super(message);
9
9
  this.name = "SonarApiError";
@@ -5,7 +5,6 @@ function buildCloudScannerArgs(projectKey, config) {
5
5
  const sc = config.sonarcloud || {};
6
6
  const scanner = sc.scanner || {};
7
7
  const host = sc.host || "https://sonarcloud.io";
8
- const token = process.env.KJ_SONARCLOUD_TOKEN || sc.token;
9
8
  const organization = process.env.KJ_SONARCLOUD_ORG || sc.organization;
10
9
 
11
10
  const args = [
@@ -1,13 +1,20 @@
1
1
  /**
2
- * Load SonarQube admin credentials from ~/.karajan/sonar-credentials.json.
2
+ * Load SonarQube credentials from ~/.karajan/sonar-credentials.json.
3
3
  *
4
- * File format:
4
+ * File format (any subset):
5
5
  * {
6
6
  * "user": "admin",
7
- * "password": "your-password"
7
+ * "password": "your-password",
8
+ * "token": "squ_..."
8
9
  * }
9
10
  *
10
- * Returns { user, password } or { user: null, password: null } if file missing.
11
+ * Returns { user, password, token } with each field null when missing.
12
+ *
13
+ * The `token` field is consumed by resolveSonarTokenAsync as the
14
+ * lowest-priority fallback. Until v2.7.5 it was not extracted here, which
15
+ * meant a token persisted to the credentials file by `kj init` was never
16
+ * actually used at scan time — Karajan would silently fall back to the
17
+ * stale token in kj.config.yml or fail with a 401.
11
18
  */
12
19
 
13
20
  import fs from "node:fs/promises";
@@ -23,10 +30,11 @@ export async function loadSonarCredentials() {
23
30
  const data = JSON.parse(raw);
24
31
  return {
25
32
  user: data.user || null,
26
- password: data.password || null
33
+ password: data.password || null,
34
+ token: data.token || null
27
35
  };
28
36
  } catch { /* credentials file may not exist */
29
- return { user: null, password: null };
37
+ return { user: null, password: null, token: null };
30
38
  }
31
39
  }
32
40