karajan-code 1.15.0 → 1.17.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 (69) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +6 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +157 -89
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/policy-resolver.js +3 -3
  18. package/src/mcp/orphan-guard.js +1 -2
  19. package/src/mcp/progress.js +4 -3
  20. package/src/mcp/run-kj.js +2 -0
  21. package/src/mcp/server-handlers.js +294 -241
  22. package/src/mcp/server.js +4 -3
  23. package/src/mcp/tools.js +19 -0
  24. package/src/orchestrator/agent-fallback.js +1 -3
  25. package/src/orchestrator/iteration-stages.js +206 -170
  26. package/src/orchestrator/pre-loop-stages.js +266 -34
  27. package/src/orchestrator/solomon-rules.js +2 -2
  28. package/src/orchestrator.js +820 -739
  29. package/src/planning-game/adapter.js +23 -20
  30. package/src/planning-game/architect-adrs.js +45 -0
  31. package/src/planning-game/client.js +15 -1
  32. package/src/planning-game/decomposition.js +7 -5
  33. package/src/prompts/architect.js +88 -0
  34. package/src/prompts/discover.js +228 -0
  35. package/src/prompts/planner.js +53 -33
  36. package/src/prompts/triage.js +8 -16
  37. package/src/review/parser.js +18 -19
  38. package/src/review/profiles.js +2 -2
  39. package/src/review/schema.js +3 -3
  40. package/src/review/scope-filter.js +3 -4
  41. package/src/roles/architect-role.js +122 -0
  42. package/src/roles/commiter-role.js +2 -2
  43. package/src/roles/discover-role.js +122 -0
  44. package/src/roles/index.js +2 -0
  45. package/src/roles/planner-role.js +54 -38
  46. package/src/roles/refactorer-role.js +8 -7
  47. package/src/roles/researcher-role.js +6 -7
  48. package/src/roles/reviewer-role.js +4 -5
  49. package/src/roles/security-role.js +3 -4
  50. package/src/roles/solomon-role.js +6 -18
  51. package/src/roles/sonar-role.js +5 -1
  52. package/src/roles/tester-role.js +8 -5
  53. package/src/roles/triage-role.js +2 -2
  54. package/src/session-cleanup.js +29 -24
  55. package/src/session-store.js +1 -1
  56. package/src/sonar/api.js +1 -1
  57. package/src/sonar/manager.js +1 -1
  58. package/src/sonar/project-key.js +5 -5
  59. package/src/sonar/scanner.js +34 -65
  60. package/src/utils/display.js +312 -272
  61. package/src/utils/git.js +3 -3
  62. package/src/utils/logger.js +6 -1
  63. package/src/utils/model-selector.js +5 -5
  64. package/src/utils/process.js +80 -102
  65. package/src/utils/rate-limit-detector.js +13 -13
  66. package/src/utils/run-log.js +55 -52
  67. package/templates/roles/architect.md +62 -0
  68. package/templates/roles/discover.md +167 -0
  69. package/templates/roles/planner.md +1 -0
@@ -1,13 +1,48 @@
1
1
  import { TriageRole } from "../roles/triage-role.js";
2
2
  import { ResearcherRole } from "../roles/researcher-role.js";
3
3
  import { PlannerRole } from "../roles/planner-role.js";
4
+ import { DiscoverRole } from "../roles/discover-role.js";
5
+ import { ArchitectRole } from "../roles/architect-role.js";
4
6
  import { createAgent } from "../agents/index.js";
7
+ import { createArchitectADRs } from "../planning-game/architect-adrs.js";
5
8
  import { addCheckpoint, markSessionStatus } from "../session-store.js";
6
9
  import { emitProgress, makeEvent } from "../utils/events.js";
7
10
  import { parsePlannerOutput } from "../prompts/planner.js";
8
11
  import { selectModelsForRoles } from "../utils/model-selector.js";
9
12
  import { createStallDetector } from "../utils/stall-detector.js";
10
13
 
14
+ const ROLE_NAMES = ["planner", "researcher", "architect", "refactorer", "reviewer", "tester", "security"];
15
+
16
+ function buildRoleOverrides(recommendedRoles, pipelineConfig) {
17
+ const overrides = {};
18
+ for (const role of ROLE_NAMES) {
19
+ overrides[`${role}Enabled`] = recommendedRoles.has(role) || Boolean(pipelineConfig[role]?.enabled);
20
+ }
21
+ return overrides;
22
+ }
23
+
24
+ function applyModelSelection(triageOutput, config, emitter, eventBase) {
25
+ if (!triageOutput.ok || !config?.model_selection?.enabled) return null;
26
+ const level = triageOutput.result?.level;
27
+ if (!level) return null;
28
+
29
+ const { modelOverrides, reasoning } = selectModelsForRoles({ level, config });
30
+ for (const [role, model] of Object.entries(modelOverrides)) {
31
+ if (config.roles?.[role] && !config.roles[role].model) {
32
+ config.roles[role].model = model;
33
+ }
34
+ }
35
+ const modelSelection = { modelOverrides, reasoning };
36
+ emitProgress(
37
+ emitter,
38
+ makeEvent("model-selection:applied", { ...eventBase, stage: "triage" }, {
39
+ message: "Smart model selection applied",
40
+ detail: modelSelection
41
+ })
42
+ );
43
+ return modelSelection;
44
+ }
45
+
11
46
  export async function runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
12
47
  logger.setContext({ iteration: 0, stage: "triage" });
13
48
  emitProgress(
@@ -54,17 +89,9 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
54
89
  });
55
90
 
56
91
  const recommendedRoles = new Set(triageOutput.result?.roles || []);
57
- const roleOverrides = {};
58
- if (triageOutput.ok) {
59
- // Triage can activate roles, but cannot deactivate roles explicitly enabled in pipeline config
60
- const p = config.pipeline || {};
61
- roleOverrides.plannerEnabled = recommendedRoles.has("planner") || Boolean(p.planner?.enabled);
62
- roleOverrides.researcherEnabled = recommendedRoles.has("researcher") || Boolean(p.researcher?.enabled);
63
- roleOverrides.refactorerEnabled = recommendedRoles.has("refactorer") || Boolean(p.refactorer?.enabled);
64
- roleOverrides.reviewerEnabled = recommendedRoles.has("reviewer") || Boolean(p.reviewer?.enabled);
65
- roleOverrides.testerEnabled = recommendedRoles.has("tester") || Boolean(p.tester?.enabled);
66
- roleOverrides.securityEnabled = recommendedRoles.has("security") || Boolean(p.security?.enabled);
67
- }
92
+ const roleOverrides = triageOutput.ok
93
+ ? buildRoleOverrides(recommendedRoles, config.pipeline || {})
94
+ : {};
68
95
 
69
96
  const shouldDecompose = triageOutput.result?.shouldDecompose || false;
70
97
  const subtasks = triageOutput.result?.subtasks || [];
@@ -79,27 +106,7 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
79
106
  subtasks
80
107
  };
81
108
 
82
- let modelSelection = null;
83
- if (triageOutput.ok && config?.model_selection?.enabled) {
84
- const level = triageOutput.result?.level;
85
- if (level) {
86
- const { modelOverrides, reasoning } = selectModelsForRoles({ level, config });
87
- for (const [role, model] of Object.entries(modelOverrides)) {
88
- if (config.roles?.[role] && !config.roles[role].model) {
89
- config.roles[role].model = model;
90
- }
91
- }
92
- modelSelection = { modelOverrides, reasoning };
93
- emitProgress(
94
- emitter,
95
- makeEvent("model-selection:applied", { ...eventBase, stage: "triage" }, {
96
- message: "Smart model selection applied",
97
- detail: modelSelection
98
- })
99
- );
100
- }
101
- }
102
-
109
+ const modelSelection = applyModelSelection(triageOutput, config, emitter, eventBase);
103
110
  if (modelSelection) {
104
111
  stageResult.modelSelection = modelSelection;
105
112
  }
@@ -185,7 +192,167 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
185
192
  return { researchContext, stageResult };
186
193
  }
187
194
 
188
- export async function runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, triageDecomposition = null, trackBudget }) {
195
+ async function handleArchitectClarification({ architectOutput, askQuestion, config, logger, emitter, eventBase, session, architectOnOutput, architectProvider, coderRole, researchContext, discoverResult, triageLevel, trackBudget }) {
196
+ if (!architectOutput.ok
197
+ || architectOutput.result?.verdict !== "needs_clarification"
198
+ || !architectOutput.result?.questions?.length) {
199
+ return architectOutput;
200
+ }
201
+
202
+ const questions = architectOutput.result.questions;
203
+ if (!askQuestion) {
204
+ logger.warn("Architect returned needs_clarification but no interactive input available — continuing with best-effort decisions");
205
+ return architectOutput;
206
+ }
207
+
208
+ const formatted = "The architect needs clarification before proceeding:\n\n"
209
+ + questions.map((q, i) => `${i + 1}. ${q}`).join("\n")
210
+ + "\n\nPlease provide your answers:";
211
+
212
+ emitProgress(
213
+ emitter,
214
+ makeEvent("architect:clarification", { ...eventBase, stage: "architect" }, {
215
+ message: "Architect needs clarification — pausing for human input",
216
+ detail: { questions }
217
+ })
218
+ );
219
+
220
+ const answer = await askQuestion(formatted, { iteration: 0, stage: "architect" });
221
+ if (!answer) return architectOutput;
222
+
223
+ const architect2 = new ArchitectRole({ config, logger, emitter, createAgentFn: createAgent });
224
+ await architect2.init({ task: session.task, sessionId: session.id, iteration: 0 });
225
+ const rerunStart = Date.now();
226
+ const rerunStall = createStallDetector({
227
+ onOutput: architectOnOutput, emitter, eventBase, stage: "architect", provider: architectProvider
228
+ });
229
+ let result;
230
+ try {
231
+ result = await architect2.execute({
232
+ task: session.task,
233
+ onOutput: rerunStall.onOutput,
234
+ researchContext,
235
+ discoverResult,
236
+ triageLevel,
237
+ humanAnswers: answer
238
+ });
239
+ } finally {
240
+ rerunStall.stop();
241
+ }
242
+ trackBudget({
243
+ role: "architect",
244
+ provider: architectProvider,
245
+ model: config?.roles?.architect?.model || coderRole.model,
246
+ result,
247
+ duration_ms: Date.now() - rerunStart
248
+ });
249
+ return result;
250
+ }
251
+
252
+ export async function runArchitectStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, researchContext = null, discoverResult = null, triageLevel = null, askQuestion = null }) {
253
+ logger.setContext({ iteration: 0, stage: "architect" });
254
+ emitProgress(
255
+ emitter,
256
+ makeEvent("architect:start", { ...eventBase, stage: "architect" }, {
257
+ message: "Architect designing solution architecture"
258
+ })
259
+ );
260
+
261
+ const architectProvider = config?.roles?.architect?.provider || coderRole.provider;
262
+ const architectOnOutput = ({ stream, line }) => {
263
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "architect" }, {
264
+ message: line,
265
+ detail: { stream, agent: architectProvider }
266
+ }));
267
+ };
268
+ const architectStall = createStallDetector({
269
+ onOutput: architectOnOutput, emitter, eventBase, stage: "architect", provider: architectProvider
270
+ });
271
+
272
+ const architect = new ArchitectRole({ config, logger, emitter, createAgentFn: createAgent });
273
+ await architect.init({ task: session.task, sessionId: session.id, iteration: 0 });
274
+ const architectStart = Date.now();
275
+ let architectOutput;
276
+ try {
277
+ architectOutput = await architect.execute({
278
+ task: session.task,
279
+ onOutput: architectStall.onOutput,
280
+ researchContext,
281
+ discoverResult,
282
+ triageLevel
283
+ });
284
+ } finally {
285
+ architectStall.stop();
286
+ }
287
+ trackBudget({
288
+ role: "architect",
289
+ provider: architectProvider,
290
+ model: config?.roles?.architect?.model || coderRole.model,
291
+ result: architectOutput,
292
+ duration_ms: Date.now() - architectStart
293
+ });
294
+
295
+ await addCheckpoint(session, {
296
+ stage: "architect",
297
+ iteration: 0,
298
+ ok: architectOutput.ok,
299
+ provider: architectProvider,
300
+ model: config?.roles?.architect?.model || coderRole.model || null
301
+ });
302
+
303
+ // --- Interactive clarification loop ---
304
+ architectOutput = await handleArchitectClarification({
305
+ architectOutput, askQuestion, config, logger, emitter, eventBase, session,
306
+ architectOnOutput, architectProvider, coderRole, researchContext, discoverResult, triageLevel, trackBudget
307
+ });
308
+
309
+ const stageResult = {
310
+ ok: architectOutput.ok,
311
+ verdict: architectOutput.result?.verdict || null,
312
+ architecture: architectOutput.result?.architecture || null,
313
+ questions: architectOutput.result?.questions || []
314
+ };
315
+
316
+ emitProgress(
317
+ emitter,
318
+ makeEvent("architect:end", { ...eventBase, stage: "architect" }, {
319
+ status: architectOutput.ok ? "ok" : "fail",
320
+ message: architectOutput.ok ? "Architecture completed" : `Architecture failed: ${architectOutput.summary}`,
321
+ detail: stageResult
322
+ })
323
+ );
324
+
325
+ const architectContext = architectOutput.ok ? architectOutput.result : null;
326
+
327
+ // Generate ADRs from architect tradeoffs when PG is linked
328
+ const tradeoffs = architectOutput.result?.architecture?.tradeoffs;
329
+ if (architectOutput.ok
330
+ && architectOutput.result?.verdict === "ready"
331
+ && tradeoffs?.length > 0
332
+ && session.pg_task_id
333
+ && session.pg_project_id) {
334
+ try {
335
+ const pgClient = await import("../planning-game/client.js");
336
+ const adrResult = await createArchitectADRs({
337
+ tradeoffs,
338
+ pgTaskId: session.pg_task_id,
339
+ pgProject: session.pg_project_id,
340
+ taskTitle: session.task,
341
+ mcpClient: pgClient
342
+ });
343
+ stageResult.adrs = adrResult;
344
+ if (adrResult.created > 0) {
345
+ logger.info(`Architect: created ${adrResult.created} ADR(s) in Planning Game`);
346
+ }
347
+ } catch (err) {
348
+ logger.warn(`Architect: failed to create ADRs: ${err.message}`);
349
+ }
350
+ }
351
+
352
+ return { architectContext, stageResult };
353
+ }
354
+
355
+ export async function runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, architectContext = null, triageDecomposition = null, trackBudget }) {
189
356
  const task = session.task;
190
357
  logger.setContext({ iteration: 0, stage: "planner" });
191
358
  emitProgress(
@@ -207,7 +374,7 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
207
374
  });
208
375
 
209
376
  const planRole = new PlannerRole({ config, logger, emitter, createAgentFn: createAgent });
210
- planRole.context = { task, research: researchContext, triageDecomposition };
377
+ planRole.context = { task, research: researchContext, architecture: architectContext, triageDecomposition };
211
378
  await planRole.init();
212
379
  const plannerStart = Date.now();
213
380
  let planResult;
@@ -258,3 +425,68 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
258
425
 
259
426
  return { plannedTask, stageResult };
260
427
  }
428
+
429
+ export async function runDiscoverStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
430
+ logger.setContext({ iteration: 0, stage: "discover" });
431
+ emitProgress(
432
+ emitter,
433
+ makeEvent("discover:start", { ...eventBase, stage: "discover" }, {
434
+ message: "Discover analyzing task for gaps"
435
+ })
436
+ );
437
+
438
+ const discoverProvider = config?.roles?.discover?.provider || coderRole.provider;
439
+ const discoverOnOutput = ({ stream, line }) => {
440
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "discover" }, {
441
+ message: line,
442
+ detail: { stream, agent: discoverProvider }
443
+ }));
444
+ };
445
+ const discoverStall = createStallDetector({
446
+ onOutput: discoverOnOutput, emitter, eventBase, stage: "discover", provider: discoverProvider
447
+ });
448
+
449
+ const mode = config?.pipeline?.discover?.mode || "gaps";
450
+ const discover = new DiscoverRole({ config, logger, emitter });
451
+ await discover.init({ task: session.task, sessionId: session.id, iteration: 0 });
452
+ const discoverStart = Date.now();
453
+ let discoverOutput;
454
+ try {
455
+ discoverOutput = await discover.run({ task: session.task, mode, onOutput: discoverStall.onOutput });
456
+ } finally {
457
+ discoverStall.stop();
458
+ }
459
+ trackBudget({
460
+ role: "discover",
461
+ provider: discoverProvider,
462
+ model: config?.roles?.discover?.model || coderRole.model,
463
+ result: discoverOutput,
464
+ duration_ms: Date.now() - discoverStart
465
+ });
466
+
467
+ await addCheckpoint(session, {
468
+ stage: "discover",
469
+ iteration: 0,
470
+ ok: discoverOutput.ok,
471
+ provider: discoverProvider,
472
+ model: config?.roles?.discover?.model || coderRole.model || null
473
+ });
474
+
475
+ const stageResult = {
476
+ ok: discoverOutput.ok,
477
+ verdict: discoverOutput.result?.verdict || null,
478
+ gaps: discoverOutput.result?.gaps || [],
479
+ mode
480
+ };
481
+
482
+ emitProgress(
483
+ emitter,
484
+ makeEvent("discover:end", { ...eventBase, stage: "discover" }, {
485
+ status: discoverOutput.ok ? "ok" : "fail",
486
+ message: discoverOutput.ok ? "Discovery completed" : `Discovery failed: ${discoverOutput.summary}`,
487
+ detail: stageResult
488
+ })
489
+ );
490
+
491
+ return { stageResult };
492
+ }
@@ -126,11 +126,11 @@ export async function buildRulesContext({ session, task, iteration }) {
126
126
  const addedDeps = (pkgDiff.stdout || "").split("\n")
127
127
  .filter(line => line.startsWith("+") && line.includes('"') && !line.startsWith("+++"))
128
128
  .map(line => {
129
- const match = line.match(/"([^"]+)":\s*"/);
129
+ const match = /"([^"]+)":\s*"/.exec(line);
130
130
  return match ? match[1] : null;
131
131
  })
132
132
  .filter(Boolean)
133
- .filter(name => !["name", "version", "description", "main", "scripts", "type", "license", "author"].includes(name));
133
+ .filter(name => !new Set(["name", "version", "description", "main", "scripts", "type", "license", "author"]).has(name));
134
134
  context.newDependencies = addedDeps;
135
135
  } catch { /* ignore */ }
136
136
  }