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.
Files changed (38) hide show
  1. package/README.md +19 -7
  2. package/docs/README.es.md +19 -7
  3. package/package.json +1 -1
  4. package/scripts/ai-attribution-guard.yml +77 -0
  5. package/scripts/esbuild-sea.config.mjs +83 -15
  6. package/scripts/install-guard-all-repos.sh +85 -0
  7. package/scripts/protect-all-repos.sh +88 -0
  8. package/src/bootstrap.js +8 -4
  9. package/src/cli.js +7 -0
  10. package/src/commands/init.js +30 -0
  11. package/src/config.js +19 -6
  12. package/src/domains/domain-loader.js +164 -0
  13. package/src/domains/domain-registry.js +140 -0
  14. package/src/domains/domain-synthesizer.js +129 -0
  15. package/src/mcp/shared-helpers.js +132 -4
  16. package/src/orchestrator/pipeline-context.js +3 -0
  17. package/src/orchestrator/preflight-checks.js +16 -5
  18. package/src/orchestrator/stages/domain-curator-stage.js +82 -0
  19. package/src/orchestrator/stages/triage-stage.js +4 -1
  20. package/src/orchestrator.js +18 -0
  21. package/src/prompts/architect.js +5 -1
  22. package/src/prompts/coder.js +5 -1
  23. package/src/prompts/hu-reviewer.js +5 -1
  24. package/src/prompts/planner.js +5 -1
  25. package/src/prompts/reviewer.js +5 -1
  26. package/src/roles/architect-role.js +1 -1
  27. package/src/roles/coder-role.js +2 -1
  28. package/src/roles/domain-curator-role.js +141 -0
  29. package/src/roles/hu-reviewer-role.js +1 -1
  30. package/src/roles/planner-role.js +6 -2
  31. package/src/roles/reviewer-role.js +7 -2
  32. package/src/roles/triage-role.js +10 -0
  33. package/src/skills/skill-loader.js +35 -4
  34. package/src/utils/process.js +4 -0
  35. package/src/utils/welcome.js +52 -0
  36. package/templates/domains/example-dental.md +57 -0
  37. package/templates/roles/domain-curator.md +52 -0
  38. 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);
@@ -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) {
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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);
@@ -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" };
@@ -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
- * @returns {Promise<Array<{name: string, content: string}>>}
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 content = await readFile(skillPath, "utf-8");
29
- skills.push({ name: entry.name, content });
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
  }
@@ -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
+ }