karajan-code 1.57.0 → 1.58.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.
- package/README.md +19 -7
- package/docs/README.es.md +19 -7
- package/package.json +1 -1
- package/scripts/ai-attribution-guard.yml +77 -0
- package/scripts/esbuild-sea.config.mjs +83 -15
- package/scripts/install-guard-all-repos.sh +85 -0
- package/scripts/protect-all-repos.sh +88 -0
- package/src/bootstrap.js +8 -4
- package/src/cli.js +7 -0
- package/src/commands/init.js +30 -0
- package/src/config.js +19 -6
- 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/preflight-checks.js +16 -5
- 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/process.js +4 -0
- 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
|
@@ -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
|
|
|
@@ -3,14 +3,37 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
const SKILL_DIRS = [".agent/skills", ".claude/skills"];
|
|
5
5
|
const SKILL_FILE = "SKILL.md";
|
|
6
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract the `type` field from SKILL.md frontmatter.
|
|
10
|
+
* Returns "technical" (default) or "domain".
|
|
11
|
+
* @param {string} content — raw SKILL.md content
|
|
12
|
+
* @returns {{type: string, body: string}}
|
|
13
|
+
*/
|
|
14
|
+
function parseSkillType(content) {
|
|
15
|
+
const match = content.match(FRONTMATTER_RE);
|
|
16
|
+
if (!match) return { type: "technical", body: content };
|
|
17
|
+
|
|
18
|
+
const frontmatter = match[1];
|
|
19
|
+
const body = match[2];
|
|
20
|
+
|
|
21
|
+
// Simple regex extraction — avoid full YAML parser for a single field
|
|
22
|
+
const typeMatch = frontmatter.match(/^type:\s*(.+)$/m);
|
|
23
|
+
const type = typeMatch ? typeMatch[1].trim().toLowerCase() : "technical";
|
|
24
|
+
|
|
25
|
+
return { type, body };
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
29
|
* Scans known skill directories for SKILL.md files.
|
|
9
30
|
* @param {string} projectDir — absolute path to the project root
|
|
10
|
-
* @
|
|
31
|
+
* @param {{type?: string}} [options] — filter by type: "technical" (default), "domain", or undefined (all)
|
|
32
|
+
* @returns {Promise<Array<{name: string, content: string, type: string}>>}
|
|
11
33
|
*/
|
|
12
|
-
export async function loadAvailableSkills(projectDir) {
|
|
34
|
+
export async function loadAvailableSkills(projectDir, options = {}) {
|
|
13
35
|
const skills = [];
|
|
36
|
+
const filterType = options.type || null;
|
|
14
37
|
|
|
15
38
|
for (const rel of SKILL_DIRS) {
|
|
16
39
|
const dir = join(projectDir, rel);
|
|
@@ -25,8 +48,16 @@ export async function loadAvailableSkills(projectDir) {
|
|
|
25
48
|
if (!entry.isDirectory()) continue;
|
|
26
49
|
const skillPath = join(dir, entry.name, SKILL_FILE);
|
|
27
50
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
51
|
+
const raw = await readFile(skillPath, "utf-8");
|
|
52
|
+
const { type, body } = parseSkillType(raw);
|
|
53
|
+
|
|
54
|
+
// Filter by type if specified
|
|
55
|
+
if (filterType && type !== filterType) continue;
|
|
56
|
+
|
|
57
|
+
// For backward compat: existing callers get content without frontmatter for domain skills,
|
|
58
|
+
// but full content for technical skills (they never had frontmatter before)
|
|
59
|
+
const content = type === "domain" ? body.trim() : raw;
|
|
60
|
+
skills.push({ name: entry.name, content, type });
|
|
30
61
|
} catch {
|
|
31
62
|
// no SKILL.md in this subdirectory — skip
|
|
32
63
|
}
|
package/src/utils/process.js
CHANGED
|
@@ -76,6 +76,10 @@ async function awaitWithTimeout(subprocess, timeout, getStdout) {
|
|
|
76
76
|
|
|
77
77
|
export async function runCommand(command, args = [], options = {}) {
|
|
78
78
|
const { timeout, onOutput, silenceTimeoutMs, partialOutputFlushMs, ...rest } = options;
|
|
79
|
+
// Always detach stdin: KJ subprocesses never need interactive input.
|
|
80
|
+
// Without this, a subprocess prompting for input (sonar credentials,
|
|
81
|
+
// agent approval, npm prompts) hangs forever since there is no TTY.
|
|
82
|
+
if (!rest.stdin) rest.stdin = "ignore";
|
|
79
83
|
const subprocess = execa(command, args, { reject: false, ...rest });
|
|
80
84
|
|
|
81
85
|
let stdoutAccum = "";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const A = {
|
|
2
|
+
reset: "\x1b[0m",
|
|
3
|
+
bold: "\x1b[1m",
|
|
4
|
+
dim: "\x1b[2m",
|
|
5
|
+
cyan: "\x1b[36m",
|
|
6
|
+
green: "\x1b[32m",
|
|
7
|
+
yellow: "\x1b[33m",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const BAR = `${A.dim}${"─".repeat(44)}${A.reset}`;
|
|
11
|
+
|
|
12
|
+
const QUICK_START = [
|
|
13
|
+
["kj run <task>", "Run the full coder+reviewer pipeline"],
|
|
14
|
+
["kj init", "Initialize config and setup"],
|
|
15
|
+
["kj doctor", "Check environment and agent availability"],
|
|
16
|
+
["kj status", "Show pipeline dashboard"],
|
|
17
|
+
["kj report", "Show latest session report"],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Print the CLI welcome screen.
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
* @param {string} opts.version - Version string (required)
|
|
24
|
+
* @param {object} [opts.config] - Loaded KJ config (optional, shows configured agents)
|
|
25
|
+
*/
|
|
26
|
+
export function printWelcomeScreen({ version, config = null }) {
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(BAR);
|
|
29
|
+
console.log(` ${A.bold}${A.cyan}▶ Karajan Code${A.reset} ${A.dim}v${version}${A.reset}`);
|
|
30
|
+
console.log(` ${A.dim}AI-powered coding pipeline${A.reset}`);
|
|
31
|
+
console.log(BAR);
|
|
32
|
+
console.log();
|
|
33
|
+
|
|
34
|
+
if (config) {
|
|
35
|
+
const coder = config.roles?.coder?.provider || config.coder || "claude";
|
|
36
|
+
const reviewer = config.roles?.reviewer?.provider || config.reviewer || "codex";
|
|
37
|
+
console.log(` ${A.bold}Agents${A.reset}`);
|
|
38
|
+
console.log(` ${A.dim}coder:${A.reset} ${coder}`);
|
|
39
|
+
console.log(` ${A.dim}reviewer:${A.reset} ${reviewer}`);
|
|
40
|
+
console.log();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(` ${A.bold}Quick start${A.reset}`);
|
|
44
|
+
for (const [cmd, desc] of QUICK_START) {
|
|
45
|
+
const padded = cmd.padEnd(20);
|
|
46
|
+
console.log(` ${A.green}${padded}${A.reset} ${A.dim}${desc}${A.reset}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(` ${A.dim}Run ${A.reset}kj --help${A.dim} to see all commands.${A.reset}`);
|
|
51
|
+
console.log();
|
|
52
|
+
}
|