karajan-code 1.57.2 → 1.58.2
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 +18 -5
- package/docs/README.es.md +18 -5
- package/package.json +1 -1
- package/src/cli.js +7 -0
- package/src/domains/domain-loader.js +164 -0
- package/src/domains/domain-registry.js +140 -0
- package/src/domains/domain-synthesizer.js +129 -0
- package/src/mcp/shared-helpers.js +132 -4
- package/src/orchestrator/pipeline-context.js +3 -0
- package/src/orchestrator/stages/domain-curator-stage.js +82 -0
- package/src/orchestrator/stages/triage-stage.js +4 -1
- package/src/orchestrator.js +18 -0
- package/src/prompts/architect.js +5 -1
- package/src/prompts/coder.js +5 -1
- package/src/prompts/hu-reviewer.js +5 -1
- package/src/prompts/planner.js +5 -1
- package/src/prompts/reviewer.js +5 -1
- package/src/roles/architect-role.js +1 -1
- package/src/roles/coder-role.js +2 -1
- package/src/roles/domain-curator-role.js +141 -0
- package/src/roles/hu-reviewer-role.js +1 -1
- package/src/roles/planner-role.js +6 -2
- package/src/roles/reviewer-role.js +7 -2
- package/src/roles/triage-role.js +10 -0
- package/src/skills/skill-loader.js +35 -4
- package/src/utils/welcome.js +52 -0
- package/templates/domains/example-dental.md +57 -0
- package/templates/roles/domain-curator.md +52 -0
- package/templates/roles/triage.md +15 -1
|
@@ -104,11 +104,125 @@ export async function buildConfig(options, commandName = "run") {
|
|
|
104
104
|
return merged;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Parse a structured response from free-text input.
|
|
109
|
+
* Exported for testing.
|
|
110
|
+
* @param {string} raw — user's raw text answer
|
|
111
|
+
* @param {string} type — question type: "multi-select" | "select" | "confirm" | "text"
|
|
112
|
+
* @param {Array<{id: string, label: string}>} [options] — available options (for select types)
|
|
113
|
+
* @returns {string[]|string|boolean|null} parsed response
|
|
114
|
+
*/
|
|
115
|
+
export function parseStructuredResponse(raw, type, options = []) {
|
|
116
|
+
const trimmed = (raw || "").trim();
|
|
117
|
+
const lower = trimmed.toLowerCase();
|
|
118
|
+
|
|
119
|
+
if (type === "confirm") {
|
|
120
|
+
return ["yes", "y", "si", "sí", "1", "true"].includes(lower) ? true : false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (type === "text") {
|
|
124
|
+
return trimmed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (type === "select") {
|
|
128
|
+
const num = parseInt(trimmed, 10);
|
|
129
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) {
|
|
130
|
+
return options[num - 1].id;
|
|
131
|
+
}
|
|
132
|
+
// Try matching by id directly
|
|
133
|
+
const match = options.find(o => o.id.toLowerCase() === lower);
|
|
134
|
+
return match ? match.id : (options[0]?.id || null);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (type === "multi-select") {
|
|
138
|
+
if (lower === "all") return options.map(o => o.id);
|
|
139
|
+
if (lower === "none" || lower === "0") return [];
|
|
140
|
+
|
|
141
|
+
// Parse comma-separated numbers or ids
|
|
142
|
+
const parts = trimmed.split(/[,;\s]+/).map(p => p.trim()).filter(Boolean);
|
|
143
|
+
const selected = [];
|
|
144
|
+
|
|
145
|
+
for (const part of parts) {
|
|
146
|
+
const num = parseInt(part, 10);
|
|
147
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) {
|
|
148
|
+
selected.push(options[num - 1].id);
|
|
149
|
+
} else {
|
|
150
|
+
const match = options.find(o => o.id.toLowerCase() === part.toLowerCase());
|
|
151
|
+
if (match) selected.push(match.id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return selected;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return trimmed || null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format a structured question into a text message for elicitInput.
|
|
163
|
+
* @param {Object} question
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
function formatStructuredQuestion(question) {
|
|
167
|
+
const lines = [question.message, ""];
|
|
168
|
+
|
|
169
|
+
if (question.options?.length) {
|
|
170
|
+
for (let i = 0; i < question.options.length; i++) {
|
|
171
|
+
const opt = question.options[i];
|
|
172
|
+
const marker = opt.default ? "*" : " ";
|
|
173
|
+
lines.push(` ${i + 1}.${marker} ${opt.label}`);
|
|
174
|
+
}
|
|
175
|
+
lines.push("");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (question.type === "multi-select") {
|
|
179
|
+
lines.push("(Enter numbers separated by commas, 'all', or 'none')");
|
|
180
|
+
} else if (question.type === "select") {
|
|
181
|
+
lines.push("(Enter a number)");
|
|
182
|
+
} else if (question.type === "confirm") {
|
|
183
|
+
lines.push("(yes/no)");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return lines.join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build an askQuestion function from an MCP server.
|
|
191
|
+
* Detects host capabilities and adapts behavior accordingly.
|
|
192
|
+
*
|
|
193
|
+
* The returned function has an `.interactive` boolean property indicating
|
|
194
|
+
* whether the host supports elicitation.
|
|
195
|
+
*
|
|
196
|
+
* Accepts both plain string questions (backward compatible) and structured
|
|
197
|
+
* question objects ({message, type, options, defaults}).
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} server — MCP server instance
|
|
200
|
+
* @returns {Function & {interactive: boolean}}
|
|
201
|
+
*/
|
|
107
202
|
export function buildAskQuestion(server) {
|
|
108
|
-
|
|
203
|
+
const canElicit = Boolean(server.getClientCapabilities?.()?.elicitation);
|
|
204
|
+
|
|
205
|
+
const askQuestion = async (question) => {
|
|
206
|
+
const isStructured = question && typeof question === "object" && question.message;
|
|
207
|
+
|
|
208
|
+
// Non-interactive: return defaults or null
|
|
209
|
+
if (!canElicit) {
|
|
210
|
+
if (!isStructured) return null;
|
|
211
|
+
if (question.defaults) return question.defaults;
|
|
212
|
+
if (question.type === "confirm") return false;
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build the message text
|
|
217
|
+
const message = isStructured
|
|
218
|
+
? formatStructuredQuestion(question)
|
|
219
|
+
: question;
|
|
220
|
+
|
|
221
|
+
// Call elicitInput
|
|
222
|
+
let rawAnswer;
|
|
109
223
|
try {
|
|
110
224
|
const result = await server.elicitInput({
|
|
111
|
-
message
|
|
225
|
+
message,
|
|
112
226
|
requestedSchema: {
|
|
113
227
|
type: "object",
|
|
114
228
|
properties: {
|
|
@@ -117,11 +231,25 @@ export function buildAskQuestion(server) {
|
|
|
117
231
|
required: ["answer"]
|
|
118
232
|
}
|
|
119
233
|
});
|
|
120
|
-
|
|
234
|
+
rawAnswer = result.action === "accept" ? result.content?.answer || null : null;
|
|
121
235
|
} catch {
|
|
122
|
-
return null;
|
|
236
|
+
return isStructured && question.defaults ? question.defaults : null;
|
|
123
237
|
}
|
|
238
|
+
|
|
239
|
+
if (rawAnswer === null) {
|
|
240
|
+
return isStructured && question.defaults ? question.defaults : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Parse structured response
|
|
244
|
+
if (isStructured && question.type) {
|
|
245
|
+
return parseStructuredResponse(rawAnswer, question.type, question.options);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return rawAnswer;
|
|
124
249
|
};
|
|
250
|
+
|
|
251
|
+
askQuestion.interactive = canElicit;
|
|
252
|
+
return askQuestion;
|
|
125
253
|
}
|
|
126
254
|
|
|
127
255
|
const EVENT_LOG_LEVELS = {
|
|
@@ -40,6 +40,9 @@ export class PipelineContext {
|
|
|
40
40
|
// Product context (loaded from .karajan/context.md or product-vision.md)
|
|
41
41
|
this.productContext = null;
|
|
42
42
|
|
|
43
|
+
// Domain context (synthesized by Domain Curator from ~/.karajan/domains/ and .karajan/domains/)
|
|
44
|
+
this.domainContext = null;
|
|
45
|
+
|
|
43
46
|
// Planned task (may differ from original task after planner)
|
|
44
47
|
this.plannedTask = null;
|
|
45
48
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Curator stage logic.
|
|
3
|
+
* Runs the Domain Curator role to discover and synthesize domain knowledge.
|
|
4
|
+
* Slots after Triage and before Researcher/Architect/Planner.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DomainCuratorRole } from "../../roles/domain-curator-role.js";
|
|
8
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} params
|
|
12
|
+
* @param {Object} params.config
|
|
13
|
+
* @param {Object} params.logger
|
|
14
|
+
* @param {Object} params.emitter
|
|
15
|
+
* @param {Object} params.eventBase
|
|
16
|
+
* @param {Object} params.session
|
|
17
|
+
* @param {Function} params.trackBudget
|
|
18
|
+
* @param {string[]} params.domainHints — from triage
|
|
19
|
+
* @param {Function & {interactive: boolean}} [params.askQuestion]
|
|
20
|
+
* @returns {Promise<{domainContext: string|null, stageResult: Object}>}
|
|
21
|
+
*/
|
|
22
|
+
export async function runDomainCuratorStage({ config, logger, emitter, eventBase, session, trackBudget, domainHints = [], askQuestion = null }) {
|
|
23
|
+
logger.setContext?.({ iteration: 0, stage: "domain-curator" });
|
|
24
|
+
|
|
25
|
+
emitProgress(
|
|
26
|
+
emitter,
|
|
27
|
+
makeEvent("domain-curator:start", { ...eventBase, stage: "domain-curator" }, {
|
|
28
|
+
message: "Domain Curator discovering domain knowledge",
|
|
29
|
+
detail: { domainHints, interactive: Boolean(askQuestion?.interactive) }
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
const curator = new DomainCuratorRole({ config, logger });
|
|
35
|
+
|
|
36
|
+
let curatorOutput;
|
|
37
|
+
try {
|
|
38
|
+
curatorOutput = await curator.execute({
|
|
39
|
+
task: session.task,
|
|
40
|
+
domainHints,
|
|
41
|
+
askQuestion,
|
|
42
|
+
projectDir: config.projectDir || null
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logger.warn(`Domain Curator threw: ${err.message}`);
|
|
46
|
+
curatorOutput = {
|
|
47
|
+
ok: true,
|
|
48
|
+
result: { selectedDomains: [], domainContext: null, domainsFound: 0, domainsUsed: 0, source: "error" },
|
|
49
|
+
summary: `Domain Curator error: ${err.message}`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
trackBudget({
|
|
54
|
+
role: "domain-curator",
|
|
55
|
+
provider: "local",
|
|
56
|
+
model: null,
|
|
57
|
+
result: curatorOutput,
|
|
58
|
+
duration_ms: Date.now() - start
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const domainContext = curatorOutput.result?.domainContext || null;
|
|
62
|
+
|
|
63
|
+
const stageResult = {
|
|
64
|
+
ok: curatorOutput.ok,
|
|
65
|
+
domainsFound: curatorOutput.result?.domainsFound || 0,
|
|
66
|
+
domainsUsed: curatorOutput.result?.domainsUsed || 0,
|
|
67
|
+
selectedDomains: curatorOutput.result?.selectedDomains || [],
|
|
68
|
+
source: curatorOutput.result?.source || "none",
|
|
69
|
+
hasDomainContext: Boolean(domainContext)
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
emitProgress(
|
|
73
|
+
emitter,
|
|
74
|
+
makeEvent("domain-curator:done", { ...eventBase, stage: "domain-curator" }, {
|
|
75
|
+
status: "ok",
|
|
76
|
+
message: curatorOutput.summary,
|
|
77
|
+
detail: stageResult
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return { domainContext, stageResult };
|
|
82
|
+
}
|
|
@@ -99,6 +99,8 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
99
99
|
const shouldDecompose = triageOutput.result?.shouldDecompose || false;
|
|
100
100
|
const subtasks = triageOutput.result?.subtasks || [];
|
|
101
101
|
|
|
102
|
+
const domainHints = triageOutput.result?.domainHints || [];
|
|
103
|
+
|
|
102
104
|
const stageResult = {
|
|
103
105
|
ok: triageOutput.ok,
|
|
104
106
|
level: triageOutput.result?.level || null,
|
|
@@ -106,7 +108,8 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
106
108
|
reasoning: triageOutput.result?.reasoning || null,
|
|
107
109
|
taskType: triageOutput.result?.taskType || "sw",
|
|
108
110
|
shouldDecompose,
|
|
109
|
-
subtasks
|
|
111
|
+
subtasks,
|
|
112
|
+
domainHints
|
|
110
113
|
};
|
|
111
114
|
|
|
112
115
|
const modelSelection = applyModelSelection(triageOutput, config, emitter, eventBase);
|
package/src/orchestrator.js
CHANGED
|
@@ -34,6 +34,7 @@ import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
|
34
34
|
import { msg, getLang } from "./utils/messages.js";
|
|
35
35
|
import { PipelineContext } from "./orchestrator/pipeline-context.js";
|
|
36
36
|
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
|
|
37
|
+
import { runDomainCuratorStage } from "./orchestrator/stages/domain-curator-stage.js";
|
|
37
38
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
38
39
|
import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
|
|
39
40
|
import { needsSubPipeline, runHuSubPipeline } from "./orchestrator/hu-sub-pipeline.js";
|
|
@@ -919,6 +920,23 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
919
920
|
logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
|
|
920
921
|
}
|
|
921
922
|
|
|
923
|
+
// --- Domain Curator (after triage + skill auto-install, before planning phases) ---
|
|
924
|
+
const domainHints = triageResult.stageResult?.domainHints || [];
|
|
925
|
+
if (domainHints.length > 0 || config.projectDir) {
|
|
926
|
+
try {
|
|
927
|
+
const { domainContext, stageResult: dcStageResult } = await runDomainCuratorStage({
|
|
928
|
+
config, logger, emitter, eventBase, session, trackBudget,
|
|
929
|
+
domainHints, askQuestion
|
|
930
|
+
});
|
|
931
|
+
stageResults.domainCurator = dcStageResult;
|
|
932
|
+
if (domainContext) {
|
|
933
|
+
config = { ...config, domainContext };
|
|
934
|
+
}
|
|
935
|
+
} catch (err) {
|
|
936
|
+
logger.warn(`Domain Curator failed (non-blocking): ${err.message}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
922
940
|
// --- HU Reviewer auto-activation from triage (post-triage, no huFile needed) ---
|
|
923
941
|
const triageRoles = new Set(triageResult.stageResult?.roles || []);
|
|
924
942
|
if (triageRoles.has("hu-reviewer") && !stageResults.huReviewer) {
|
package/src/prompts/architect.js
CHANGED
|
@@ -9,7 +9,7 @@ const SUBAGENT_PREAMBLE = [
|
|
|
9
9
|
|
|
10
10
|
export const VALID_VERDICTS = new Set(["ready", "needs_clarification"]);
|
|
11
11
|
|
|
12
|
-
export async function buildArchitectPrompt({ task, instructions, researchContext = null, productContext = null, projectDir = null, language = "en" }) {
|
|
12
|
+
export async function buildArchitectPrompt({ task, instructions, researchContext = null, productContext = null, domainContext = null, projectDir = null, language = "en" }) {
|
|
13
13
|
const langInstruction = getLanguageInstruction(language);
|
|
14
14
|
const sections = [SUBAGENT_PREAMBLE, ...(langInstruction ? [langInstruction] : [])];
|
|
15
15
|
|
|
@@ -39,6 +39,10 @@ export async function buildArchitectPrompt({ task, instructions, researchContext
|
|
|
39
39
|
sections.push(`## Product Context\n${productContext}`);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
if (domainContext) {
|
|
43
|
+
sections.push(`## Domain Context\n${domainContext}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
if (researchContext) {
|
|
43
47
|
sections.push(`## Research Context\n${researchContext}`);
|
|
44
48
|
}
|
package/src/prompts/coder.js
CHANGED
|
@@ -33,7 +33,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
33
33
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
34
34
|
].join("\n");
|
|
35
35
|
|
|
36
|
-
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, plan = null, projectDir = null, language = "en" }) {
|
|
36
|
+
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en" }) {
|
|
37
37
|
const langInstruction = getLanguageInstruction(language);
|
|
38
38
|
const sections = [
|
|
39
39
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
@@ -56,6 +56,10 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
56
56
|
sections.push(`## Product Context\n${productContext}`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
if (domainContext) {
|
|
60
|
+
sections.push(`## Domain Context\n${domainContext}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
if (plan) {
|
|
60
64
|
sections.push(`## Implementation Plan (from planner)\nFollow these steps:\n${plan}`);
|
|
61
65
|
}
|
|
@@ -22,7 +22,7 @@ const DIMENSION_KEYS = [
|
|
|
22
22
|
* @param {{stories: Array<{id: string, text: string}>, instructions: string|null, context?: string|null}} params
|
|
23
23
|
* @returns {string} The assembled prompt.
|
|
24
24
|
*/
|
|
25
|
-
export function buildHuReviewerPrompt({ stories, instructions, context = null, productContext = null, hu_language = "en" }) {
|
|
25
|
+
export function buildHuReviewerPrompt({ stories, instructions, context = null, productContext = null, domainContext = null, hu_language = "en" }) {
|
|
26
26
|
const langInstruction = getLanguageInstruction(hu_language);
|
|
27
27
|
const sections = [SUBAGENT_PREAMBLE, ...(langInstruction ? [langInstruction] : [])];
|
|
28
28
|
|
|
@@ -45,6 +45,10 @@ export function buildHuReviewerPrompt({ stories, instructions, context = null, p
|
|
|
45
45
|
sections.push(`## Product Context\n${productContext}`);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
if (domainContext) {
|
|
49
|
+
sections.push(`## Domain Context\n${domainContext}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
if (context) {
|
|
49
53
|
sections.push(`## Additional Context\n${context}`);
|
|
50
54
|
}
|
package/src/prompts/planner.js
CHANGED
|
@@ -64,7 +64,7 @@ function formatArchitectContext(architectContext) {
|
|
|
64
64
|
return lines.length > 1 ? lines.join("\n") : null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export function buildPlannerPrompt({ task, context, architectContext, productContext = null }) {
|
|
67
|
+
export function buildPlannerPrompt({ task, context, architectContext, productContext = null, domainContext = null }) {
|
|
68
68
|
const parts = [
|
|
69
69
|
"You are an expert software architect. Create an implementation plan for the following task.",
|
|
70
70
|
"",
|
|
@@ -77,6 +77,10 @@ export function buildPlannerPrompt({ task, context, architectContext, productCon
|
|
|
77
77
|
parts.push("## Product Context", productContext, "");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
if (domainContext) {
|
|
81
|
+
parts.push("## Domain Context", domainContext, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
if (context) {
|
|
81
85
|
parts.push("## Context", context, "");
|
|
82
86
|
}
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -24,7 +24,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
24
24
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
25
25
|
].join("\n");
|
|
26
26
|
|
|
27
|
-
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, projectDir = null, language = "en" }) {
|
|
27
|
+
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en" }) {
|
|
28
28
|
const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
|
|
29
29
|
|
|
30
30
|
const langInstruction = getLanguageInstruction(language);
|
|
@@ -51,6 +51,10 @@ export async function buildReviewerPrompt({ task, diff, reviewRules, mode, seren
|
|
|
51
51
|
sections.push(`## Product Context\n${productContext}`);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
if (domainContext) {
|
|
55
|
+
sections.push(`## Domain Context\n${domainContext}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
54
58
|
sections.push(
|
|
55
59
|
`Task context:\n${task}`,
|
|
56
60
|
`Review rules:\n${reviewRules}`,
|
|
@@ -59,7 +59,7 @@ export class ArchitectRole extends BaseRole {
|
|
|
59
59
|
const provider = resolveProvider(this.config);
|
|
60
60
|
const agent = this._createAgent(provider, this.config, this.logger);
|
|
61
61
|
|
|
62
|
-
const prompt = await buildArchitectPrompt({ task, instructions: this.instructions, researchContext, productContext: this.config?.productContext || null });
|
|
62
|
+
const prompt = await buildArchitectPrompt({ task, instructions: this.instructions, researchContext, productContext: this.config?.productContext || null, domainContext: this.config?.domainContext || null });
|
|
63
63
|
const runArgs = { prompt, role: "architect" };
|
|
64
64
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
65
65
|
const result = await agent.runTask(runArgs);
|
package/src/roles/coder-role.js
CHANGED
|
@@ -43,7 +43,8 @@ export class CoderRole extends BaseRole {
|
|
|
43
43
|
methodology: this.config?.development?.methodology || "tdd",
|
|
44
44
|
serenaEnabled: Boolean(this.config?.serena?.enabled),
|
|
45
45
|
rtkAvailable: Boolean(this.config?.rtk?.available),
|
|
46
|
-
productContext: this.config?.productContext || null
|
|
46
|
+
productContext: this.config?.productContext || null,
|
|
47
|
+
domainContext: this.config?.domainContext || null
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
const coderArgs = { prompt, role: "coder" };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Curator role.
|
|
3
|
+
* Discovers, proposes and synthesizes domain knowledge for the pipeline.
|
|
4
|
+
* Operates deterministically (loader + registry + synthesizer) — no LLM call needed
|
|
5
|
+
* unless the user wants to generate domain knowledge from scratch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { loadDomains } from "../domains/domain-loader.js";
|
|
9
|
+
import { DomainRegistry } from "../domains/domain-registry.js";
|
|
10
|
+
import { synthesizeDomainContext } from "../domains/domain-synthesizer.js";
|
|
11
|
+
|
|
12
|
+
export class DomainCuratorRole {
|
|
13
|
+
constructor({ config, logger }) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute domain curation.
|
|
20
|
+
* @param {Object} input
|
|
21
|
+
* @param {string} input.task — the task description
|
|
22
|
+
* @param {string[]} input.domainHints — keywords from triage
|
|
23
|
+
* @param {Function & {interactive: boolean}} [input.askQuestion] — interactive question function
|
|
24
|
+
* @param {string} [input.projectDir] — project root
|
|
25
|
+
* @returns {Promise<{ok: boolean, result: Object, summary: string}>}
|
|
26
|
+
*/
|
|
27
|
+
async execute({ task, domainHints = [], askQuestion = null, projectDir = null }) {
|
|
28
|
+
// 1. Load domains from filesystem
|
|
29
|
+
const fileDomains = await loadDomains(projectDir);
|
|
30
|
+
|
|
31
|
+
// 2. Search registry for additional matches
|
|
32
|
+
let registryDomains = [];
|
|
33
|
+
if (domainHints.length > 0) {
|
|
34
|
+
try {
|
|
35
|
+
const registry = await DomainRegistry.load();
|
|
36
|
+
registryDomains = registry.search(domainHints);
|
|
37
|
+
} catch {
|
|
38
|
+
this.logger.warn("Domain Curator: failed to load registry, continuing with file domains only");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Deduplicate (file domains take precedence over registry entries)
|
|
43
|
+
const allDomains = deduplicateDomains(fileDomains, registryDomains);
|
|
44
|
+
|
|
45
|
+
// 4. No domains found
|
|
46
|
+
if (allDomains.length === 0) {
|
|
47
|
+
if (askQuestion?.interactive) {
|
|
48
|
+
const answer = await askQuestion({
|
|
49
|
+
message: "No domain knowledge found for this task.\nDo you have domain documents to provide, or should the pipeline continue without domain context?",
|
|
50
|
+
type: "confirm"
|
|
51
|
+
});
|
|
52
|
+
// For now, just log. Future: support document ingestion.
|
|
53
|
+
if (answer === true) {
|
|
54
|
+
this.logger.info("Domain Curator: user indicated domain documents available — future feature");
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
this.logger.warn("Domain Curator: no domain knowledge found, continuing without domain context");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
result: {
|
|
63
|
+
selectedDomains: [],
|
|
64
|
+
domainContext: null,
|
|
65
|
+
domainsFound: 0,
|
|
66
|
+
domainsUsed: 0,
|
|
67
|
+
source: "none"
|
|
68
|
+
},
|
|
69
|
+
summary: "Domain Curator: no domains found"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 5. Propose selection to user (if interactive) or use all
|
|
74
|
+
let selectedDomains;
|
|
75
|
+
|
|
76
|
+
if (askQuestion?.interactive && allDomains.length > 1) {
|
|
77
|
+
const options = allDomains.map(d => ({
|
|
78
|
+
id: d.name,
|
|
79
|
+
label: `${d.name} (v${d.version || "?"}) — ${d.description || "no description"}`,
|
|
80
|
+
default: true
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const selected = await askQuestion({
|
|
84
|
+
message: `Found ${allDomains.length} domain knowledge package(s):`,
|
|
85
|
+
type: "multi-select",
|
|
86
|
+
options,
|
|
87
|
+
defaults: allDomains.map(d => d.name)
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(selected) && selected.length > 0) {
|
|
91
|
+
const selectedSet = new Set(selected);
|
|
92
|
+
selectedDomains = allDomains.filter(d => selectedSet.has(d.name));
|
|
93
|
+
} else {
|
|
94
|
+
selectedDomains = allDomains; // fallback to all
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// Single domain or non-interactive: use all
|
|
98
|
+
selectedDomains = allDomains;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 6. Synthesize context
|
|
102
|
+
const domainContext = synthesizeDomainContext({
|
|
103
|
+
task,
|
|
104
|
+
domainHints,
|
|
105
|
+
selectedDomains
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const source = selectedDomains.some(d => d.origin === "project") ? "project+user" : "user";
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
result: {
|
|
113
|
+
selectedDomains: selectedDomains.map(d => d.name),
|
|
114
|
+
domainContext: domainContext || null,
|
|
115
|
+
domainsFound: allDomains.length,
|
|
116
|
+
domainsUsed: selectedDomains.length,
|
|
117
|
+
source
|
|
118
|
+
},
|
|
119
|
+
summary: `Domain Curator: ${selectedDomains.length}/${allDomains.length} domain(s) selected`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Deduplicate domains — file domains override registry entries by name.
|
|
126
|
+
*/
|
|
127
|
+
function deduplicateDomains(fileDomains, registryDomains) {
|
|
128
|
+
const byName = new Map();
|
|
129
|
+
|
|
130
|
+
// Registry entries first (lower priority)
|
|
131
|
+
for (const rd of registryDomains) {
|
|
132
|
+
if (rd.name) byName.set(rd.name, rd);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// File domains override
|
|
136
|
+
for (const fd of fileDomains) {
|
|
137
|
+
if (fd.name) byName.set(fd.name, fd);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return Array.from(byName.values());
|
|
141
|
+
}
|
|
@@ -52,7 +52,7 @@ export class HuReviewerRole extends BaseRole {
|
|
|
52
52
|
const provider = resolveProvider(this.config);
|
|
53
53
|
const agent = this._createAgent(provider, this.config, this.logger);
|
|
54
54
|
|
|
55
|
-
const prompt = buildHuReviewerPrompt({ stories, instructions: this.instructions, context, productContext: this.config?.productContext || null });
|
|
55
|
+
const prompt = buildHuReviewerPrompt({ stories, instructions: this.instructions, context, productContext: this.config?.productContext || null, domainContext: this.config?.domainContext || null });
|
|
56
56
|
const runArgs = { prompt, role: "hu-reviewer" };
|
|
57
57
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
58
58
|
const result = await agent.runTask(runArgs);
|
|
@@ -63,7 +63,7 @@ function appendArchitectSection(sections, architectContext) {
|
|
|
63
63
|
sections.push("");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function buildPrompt({ task, instructions, research, triageDecomposition, architectContext, productContext = null }) {
|
|
66
|
+
function buildPrompt({ task, instructions, research, triageDecomposition, architectContext, productContext = null, domainContext = null }) {
|
|
67
67
|
const sections = [];
|
|
68
68
|
|
|
69
69
|
if (instructions) {
|
|
@@ -80,6 +80,10 @@ function buildPrompt({ task, instructions, research, triageDecomposition, archit
|
|
|
80
80
|
sections.push("## Product Context", productContext, "");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
if (domainContext) {
|
|
84
|
+
sections.push("## Domain Context", domainContext, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
83
87
|
appendDecompositionSection(sections, triageDecomposition);
|
|
84
88
|
appendArchitectSection(sections, architectContext);
|
|
85
89
|
appendResearchSection(sections, research);
|
|
@@ -106,7 +110,7 @@ export class PlannerRole extends BaseRole {
|
|
|
106
110
|
const provider = resolveProvider(this.config);
|
|
107
111
|
|
|
108
112
|
const agent = this._createAgent(provider, this.config, this.logger);
|
|
109
|
-
const prompt = buildPrompt({ task: taskStr, instructions: this.instructions, research, triageDecomposition, architectContext, productContext: this.config?.productContext || null });
|
|
113
|
+
const prompt = buildPrompt({ task: taskStr, instructions: this.instructions, research, triageDecomposition, architectContext, productContext: this.config?.productContext || null, domainContext: this.config?.domainContext || null });
|
|
110
114
|
|
|
111
115
|
const runArgs = { prompt, role: "planner" };
|
|
112
116
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
@@ -25,7 +25,7 @@ function truncateDiff(diff) {
|
|
|
25
25
|
: diff;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAvailable = false, productContext = null }) {
|
|
28
|
+
function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAvailable = false, productContext = null, domainContext = null }) {
|
|
29
29
|
const sections = [];
|
|
30
30
|
|
|
31
31
|
sections.push(SUBAGENT_PREAMBLE);
|
|
@@ -46,6 +46,10 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAva
|
|
|
46
46
|
sections.push(`## Product Context\n${productContext}`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
if (domainContext) {
|
|
50
|
+
sections.push(`## Domain Context\n${domainContext}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
if (rtkAvailable) {
|
|
50
54
|
sections.push(RTK_INSTRUCTIONS);
|
|
51
55
|
}
|
|
@@ -89,7 +93,8 @@ export class ReviewerRole extends BaseRole {
|
|
|
89
93
|
reviewMode: this.config?.review_mode || "standard",
|
|
90
94
|
instructions: this.instructions,
|
|
91
95
|
rtkAvailable: Boolean(this.config?.rtk?.available),
|
|
92
|
-
productContext: this.config?.productContext || null
|
|
96
|
+
productContext: this.config?.productContext || null,
|
|
97
|
+
domainContext: this.config?.domainContext || null
|
|
93
98
|
});
|
|
94
99
|
|
|
95
100
|
const reviewArgs = { prompt, role: "reviewer" };
|
package/src/roles/triage-role.js
CHANGED
|
@@ -33,6 +33,13 @@ function normalizeSubtasks(subtasks) {
|
|
|
33
33
|
.slice(0, 5);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function normalizeDomainHints(hints) {
|
|
37
|
+
if (!Array.isArray(hints)) return [];
|
|
38
|
+
return hints
|
|
39
|
+
.map((h) => (typeof h === "string" ? h.trim().toLowerCase() : ""))
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
export class TriageRole extends BaseRole {
|
|
37
44
|
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
38
45
|
super({ name: "triage", config, logger, emitter });
|
|
@@ -90,11 +97,14 @@ export class TriageRole extends BaseRole {
|
|
|
90
97
|
const subtasks = normalizeSubtasks(parsed.subtasks);
|
|
91
98
|
const taskType = VALID_TASK_TYPES.has(parsed.taskType) ? parsed.taskType : FALLBACK_TASK_TYPE;
|
|
92
99
|
|
|
100
|
+
const domainHints = normalizeDomainHints(parsed.domainHints);
|
|
101
|
+
|
|
93
102
|
const triageResult = {
|
|
94
103
|
level,
|
|
95
104
|
roles,
|
|
96
105
|
reasoning,
|
|
97
106
|
taskType,
|
|
107
|
+
domainHints,
|
|
98
108
|
provider
|
|
99
109
|
};
|
|
100
110
|
|