karajan-code 1.49.0 → 1.50.1

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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Triage stage logic.
3
+ * Extracted from pre-loop-stages.js for maintainability.
4
+ */
5
+
6
+ import { TriageRole } from "../../roles/triage-role.js";
7
+ import { addCheckpoint } from "../../session-store.js";
8
+ import { emitProgress, makeEvent } from "../../utils/events.js";
9
+ import { selectModelsForRoles } from "../../utils/model-selector.js";
10
+ import { createStallDetector } from "../../utils/stall-detector.js";
11
+
12
+ const ROLE_NAMES = ["planner", "researcher", "architect", "refactorer", "reviewer", "tester", "security", "impeccable"];
13
+
14
+ function buildRoleOverrides(recommendedRoles, pipelineConfig) {
15
+ const overrides = {};
16
+ for (const role of ROLE_NAMES) {
17
+ overrides[`${role}Enabled`] = recommendedRoles.has(role) || Boolean(pipelineConfig[role]?.enabled);
18
+ }
19
+ return overrides;
20
+ }
21
+
22
+ function applyModelSelection(triageOutput, config, emitter, eventBase) {
23
+ if (!triageOutput.ok || !config?.model_selection?.enabled) return null;
24
+ const level = triageOutput.result?.level;
25
+ if (!level) return null;
26
+
27
+ const { modelOverrides, reasoning } = selectModelsForRoles({ level, config });
28
+ for (const [role, model] of Object.entries(modelOverrides)) {
29
+ if (config.roles?.[role] && !config.roles[role].model) {
30
+ config.roles[role].model = model;
31
+ }
32
+ }
33
+ const modelSelection = { modelOverrides, reasoning };
34
+ emitProgress(
35
+ emitter,
36
+ makeEvent("model-selection:applied", { ...eventBase, stage: "triage" }, {
37
+ message: "Smart model selection applied",
38
+ detail: modelSelection
39
+ })
40
+ );
41
+ return modelSelection;
42
+ }
43
+
44
+ export async function runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
45
+ logger.setContext({ iteration: 0, stage: "triage" });
46
+
47
+ const triageProvider = config?.roles?.triage?.provider || coderRole.provider;
48
+ emitProgress(
49
+ emitter,
50
+ makeEvent("triage:start", { ...eventBase, stage: "triage" }, {
51
+ message: "Triage classifying task complexity",
52
+ detail: { provider: triageProvider, executorType: "agent" }
53
+ })
54
+ );
55
+
56
+ const triageOnOutput = ({ stream, line }) => {
57
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "triage" }, {
58
+ message: line,
59
+ detail: { stream, agent: triageProvider }
60
+ }));
61
+ };
62
+ const triageStall = createStallDetector({
63
+ onOutput: triageOnOutput, emitter, eventBase, stage: "triage", provider: triageProvider
64
+ });
65
+
66
+ const triage = new TriageRole({ config, logger, emitter });
67
+ await triage.init({ task: session.task, sessionId: session.id, iteration: 0 });
68
+ const triageStart = Date.now();
69
+ let triageOutput;
70
+ try {
71
+ triageOutput = await triage.run({ task: session.task, onOutput: triageStall.onOutput });
72
+ } catch (err) {
73
+ logger.warn(`Triage threw: ${err.message}`);
74
+ triageOutput = { ok: false, summary: `Triage error: ${err.message}`, result: { error: err.message } };
75
+ } finally {
76
+ triageStall.stop();
77
+ }
78
+ trackBudget({
79
+ role: "triage",
80
+ provider: config?.roles?.triage?.provider || coderRole.provider,
81
+ model: config?.roles?.triage?.model || coderRole.model,
82
+ result: triageOutput,
83
+ duration_ms: Date.now() - triageStart
84
+ });
85
+
86
+ await addCheckpoint(session, {
87
+ stage: "triage",
88
+ iteration: 0,
89
+ ok: triageOutput.ok,
90
+ provider: config?.roles?.triage?.provider || coderRole.provider,
91
+ model: config?.roles?.triage?.model || coderRole.model || null
92
+ });
93
+
94
+ const recommendedRoles = new Set(triageOutput.result?.roles || []);
95
+ const roleOverrides = triageOutput.ok
96
+ ? buildRoleOverrides(recommendedRoles, config.pipeline || {})
97
+ : {};
98
+
99
+ const shouldDecompose = triageOutput.result?.shouldDecompose || false;
100
+ const subtasks = triageOutput.result?.subtasks || [];
101
+
102
+ const stageResult = {
103
+ ok: triageOutput.ok,
104
+ level: triageOutput.result?.level || null,
105
+ roles: Array.from(recommendedRoles),
106
+ reasoning: triageOutput.result?.reasoning || null,
107
+ taskType: triageOutput.result?.taskType || "sw",
108
+ shouldDecompose,
109
+ subtasks
110
+ };
111
+
112
+ const modelSelection = applyModelSelection(triageOutput, config, emitter, eventBase);
113
+ if (modelSelection) {
114
+ stageResult.modelSelection = modelSelection;
115
+ }
116
+
117
+ if (shouldDecompose && subtasks.length > 0) {
118
+ emitProgress(
119
+ emitter,
120
+ makeEvent("triage:decompose", { ...eventBase, stage: "triage" }, {
121
+ message: `Task decomposition recommended: ${subtasks.length} subtask${subtasks.length === 1 ? "" : "s"}`,
122
+ detail: { shouldDecompose, subtasks }
123
+ })
124
+ );
125
+ }
126
+
127
+ emitProgress(
128
+ emitter,
129
+ makeEvent("triage:end", { ...eventBase, stage: "triage" }, {
130
+ status: triageOutput.ok ? "ok" : "fail",
131
+ message: triageOutput.ok ? "Triage completed" : `Triage failed: ${triageOutput.summary}`,
132
+ detail: { ...stageResult, provider: triageProvider, executorType: "agent" }
133
+ })
134
+ );
135
+
136
+ return { roleOverrides, stageResult };
137
+ }
@@ -31,6 +31,7 @@ import { classifyIntent } from "./guards/intent-guard.js";
31
31
  import { resolveReviewProfile } from "./review/profiles.js";
32
32
  import { CoderRole } from "./roles/coder-role.js";
33
33
  import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
34
+ import { msg, getLang } from "./utils/messages.js";
34
35
  import { PipelineContext } from "./orchestrator/pipeline-context.js";
35
36
  import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
36
37
  import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
@@ -402,8 +403,9 @@ async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpoin
402
403
  })
403
404
  );
404
405
 
406
+ const lang = getLang(config);
405
407
  const answer = await askQuestion(
406
- `${checkpointMsg}\n\nOptions:\n1. Continue 5 more minutes\n2. Continue until done (no more checkpoints)\n3. Continue for N minutes (reply with the number)\n4. Stop now`
408
+ `${checkpointMsg}\n\n${msg("checkpoint_options", lang)}`
407
409
  );
408
410
 
409
411
  await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
@@ -429,7 +431,7 @@ async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpoin
429
431
  return parsed;
430
432
  }
431
433
 
432
- function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
434
+ export function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
433
435
  if (!trimmedAnswer) {
434
436
  return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
435
437
  }
@@ -632,11 +634,23 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
632
634
  .filter(a => a.severity === "critical")
633
635
  .map(a => a.message)
634
636
  .join("\n");
635
- const answer = await askQuestion(
636
- `Solomon detected critical issues:\n${alertSummary}\n\nShould I continue, pause, or revert?`,
637
- { iteration: i, stage: "solomon" }
638
- );
639
- if (!answer || answer.toLowerCase().includes("pause") || answer.toLowerCase().includes("stop")) {
637
+ const question = [
638
+ "Solomon detected critical issues:",
639
+ alertSummary,
640
+ "",
641
+ "What should we do?",
642
+ "1 = Continue anyway",
643
+ "2 = Pause the session",
644
+ "3 = Stop the session",
645
+ "",
646
+ "Type 1, 2, or 3:"
647
+ ].join("\n");
648
+ const answer = await askQuestion(question, { iteration: i, stage: "solomon" });
649
+ const trimmed = (answer || "").trim().toLowerCase();
650
+ const shouldPause = !answer
651
+ || trimmed === "2" || trimmed === "3"
652
+ || trimmed.startsWith("pause") || trimmed.startsWith("stop");
653
+ if (shouldPause) {
640
654
  await pauseSession(session, {
641
655
  question: `Solomon supervisor paused: ${alertSummary}`,
642
656
  context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
@@ -1280,11 +1294,13 @@ async function runSingleIteration(ctx) {
1280
1294
 
1281
1295
  const reviewerRetryCount = session.reviewer_retry_count || 0;
1282
1296
  const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
1297
+ const iterLang = getLang(config);
1298
+ const iterMsg = msg("pipeline_iteration", iterLang, { current: i, max: config.max_iterations });
1283
1299
  emitProgress(emitter, makeEvent("iteration:start", { ...eventBase, stage: "iteration" }, {
1284
- message: `Iteration ${i}/${config.max_iterations}`,
1300
+ message: iterMsg,
1285
1301
  detail: { iteration: i, maxIterations: config.max_iterations, reviewerRetryCount, maxReviewerRetries }
1286
1302
  }));
1287
- logger.info(`Iteration ${i}/${config.max_iterations}`);
1303
+ logger.info(iterMsg);
1288
1304
 
1289
1305
  const crResult = await runCoderAndRefactorerStages({
1290
1306
  coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
@@ -4,6 +4,8 @@
4
4
  * recommends decomposition and user accepts.
5
5
  */
6
6
 
7
+ import { msg } from "../utils/messages.js";
8
+
7
9
  export async function createDecompositionSubtasks({
8
10
  client,
9
11
  projectId,
@@ -66,9 +68,9 @@ export async function createDecompositionSubtasks({
66
68
  return created;
67
69
  }
68
70
 
69
- export function buildDecompositionQuestion(subtasks, parentCardId) {
71
+ export function buildDecompositionQuestion(subtasks, parentCardId, lang = "en") {
70
72
  const lines = [
71
- `Triage recommends decomposing this task into ${subtasks.length} subtasks:`,
73
+ msg("triage_decompose", lang, { count: subtasks.length }),
72
74
  ""
73
75
  ];
74
76
  for (let i = 0; i < subtasks.length; i++) {
@@ -76,10 +78,10 @@ export function buildDecompositionQuestion(subtasks, parentCardId) {
76
78
  }
77
79
  lines.push(
78
80
  "",
79
- `Create these as linked cards in Planning Game (parent: ${parentCardId})?`,
80
- "Each subtask will block the next one (sequential chain).",
81
+ msg("triage_create_cards", lang, { cardId: parentCardId }),
82
+ msg("triage_sequential", lang),
81
83
  "",
82
- "Reply: yes / no"
84
+ msg("triage_reply", lang)
83
85
  );
84
86
  return lines.join("\n");
85
87
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { emitProgress, makeEvent } from "../utils/events.js";
15
15
  import { addCheckpoint, saveSession } from "../session-store.js";
16
+ import { getLang } from "../utils/messages.js";
16
17
 
17
18
  /**
18
19
  * Attach the PG adapter to a pipeline emitter.
@@ -54,10 +55,13 @@ export async function handlePgDecomposition({ triageResult, pgTaskId, pgProject,
54
55
  const { buildDecompositionQuestion, createDecompositionSubtasks } = await import("./decomposition.js");
55
56
  const { createCard, relateCards, fetchCard } = await import("./client.js");
56
57
 
57
- const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId);
58
+ const lang = getLang(config);
59
+ const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId, lang);
58
60
  const answer = await askQuestion(question);
59
61
 
60
- if (answer && (answer.trim().toLowerCase() === "yes" || answer.trim().toLowerCase() === "sí" || answer.trim().toLowerCase() === "si")) {
62
+ const normalizedAnswer = (answer || "").trim().toLowerCase();
63
+ const isAccept = normalizedAnswer === "1" || normalizedAnswer === "yes" || normalizedAnswer === "sí" || normalizedAnswer === "si";
64
+ if (isAccept) {
61
65
  const parentCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId }).catch(() => null);
62
66
  const createdSubtasks = await createDecompositionSubtasks({
63
67
  client: { createCard, relateCards },
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Centralized message catalog for pipeline UI strings.
3
+ * Supports i18n by resolving messages for the configured language.
4
+ * Falls back to English when a key or language is missing.
5
+ */
6
+
7
+ const MESSAGES = {
8
+ en: {
9
+ triage_decompose: "Triage recommends decomposing this task into {count} subtasks:",
10
+ triage_create_cards: "Create these as linked cards in Planning Game (parent: {cardId})?",
11
+ triage_sequential: "Each subtask will block the next one (sequential chain).",
12
+ triage_reply: "Reply: yes / no",
13
+ solomon_conflict: "--- Conflict: {stage} ---",
14
+ solomon_feedback: "Reviewer feedback:",
15
+ solomon_reason: "Solomon could not resolve the conflict:",
16
+ solomon_options: "Options:\n 1. Accept coder's work as-is\n 2. Retry with reviewer's feedback\n 3. Stop the session",
17
+ solomon_proceed: "How should we proceed?",
18
+ checkpoint_continue: "Continue for {minutes} more minutes?",
19
+ checkpoint_options: "1. Continue 5 more minutes\n2. Continue until done (no more checkpoints)\n3. Continue for N minutes (reply with the number)\n4. Stop now",
20
+ preflight_passed: "Preflight passed — all checks OK",
21
+ pipeline_iteration: "Iteration {current}/{max}",
22
+ },
23
+ es: {
24
+ triage_decompose: "Triage recomienda descomponer esta tarea en {count} subtareas:",
25
+ triage_create_cards: "¿Crear estas como cards vinculadas en Planning Game (padre: {cardId})?",
26
+ triage_sequential: "Cada subtarea bloqueará la siguiente (cadena secuencial).",
27
+ triage_reply: "Responde: sí / no",
28
+ solomon_conflict: "--- Conflicto: {stage} ---",
29
+ solomon_feedback: "Feedback del reviewer:",
30
+ solomon_reason: "Solomon no pudo resolver el conflicto:",
31
+ solomon_options: "Opciones:\n 1. Aceptar el trabajo del coder tal cual\n 2. Reintentar con el feedback del reviewer\n 3. Parar la sesión",
32
+ solomon_proceed: "¿Cómo procedemos?",
33
+ checkpoint_continue: "¿Continuar {minutes} minutos más?",
34
+ checkpoint_options: "1. Continuar (5 min)\n2. Continuar hasta terminar\n3. Tiempo personalizado\n4. Parar",
35
+ preflight_passed: "Preflight superado — todas las comprobaciones OK",
36
+ pipeline_iteration: "Iteración {current}/{max}",
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Resolve a message template by key and language, interpolating parameters.
42
+ * Falls back to English, then to the raw key if not found.
43
+ *
44
+ * @param {string} key - Message key (e.g. "solomon_conflict")
45
+ * @param {string} [lang="en"] - 2-letter language code
46
+ * @param {object} [params={}] - Interpolation values (e.g. {stage: "reviewer"})
47
+ * @returns {string}
48
+ */
49
+ export function msg(key, lang = "en", params = {}) {
50
+ const template = MESSAGES[lang]?.[key] || MESSAGES.en[key] || key;
51
+ return template.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`);
52
+ }
53
+
54
+ /**
55
+ * Extract the language code from a Karajan config object.
56
+ * @param {object} [config] - Karajan config
57
+ * @returns {string} 2-letter language code
58
+ */
59
+ export function getLang(config) {
60
+ return config?.language || "en";
61
+ }