karajan-code 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +5 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +174 -93
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/intent-guard.js +123 -0
  18. package/src/guards/output-guard.js +158 -0
  19. package/src/guards/perf-guard.js +126 -0
  20. package/src/guards/policy-resolver.js +3 -3
  21. package/src/mcp/orphan-guard.js +1 -2
  22. package/src/mcp/progress.js +4 -3
  23. package/src/mcp/run-kj.js +1 -0
  24. package/src/mcp/server-handlers.js +242 -253
  25. package/src/mcp/server.js +4 -3
  26. package/src/mcp/tools.js +2 -0
  27. package/src/orchestrator/agent-fallback.js +1 -3
  28. package/src/orchestrator/iteration-stages.js +206 -170
  29. package/src/orchestrator/pre-loop-stages.js +200 -34
  30. package/src/orchestrator/solomon-rules.js +2 -2
  31. package/src/orchestrator.js +902 -746
  32. package/src/planning-game/adapter.js +23 -20
  33. package/src/planning-game/architect-adrs.js +45 -0
  34. package/src/planning-game/client.js +15 -1
  35. package/src/planning-game/decomposition.js +7 -5
  36. package/src/prompts/architect.js +88 -0
  37. package/src/prompts/discover.js +54 -53
  38. package/src/prompts/planner.js +53 -33
  39. package/src/prompts/triage.js +8 -16
  40. package/src/review/parser.js +18 -19
  41. package/src/review/profiles.js +2 -2
  42. package/src/review/schema.js +3 -3
  43. package/src/review/scope-filter.js +3 -4
  44. package/src/roles/architect-role.js +122 -0
  45. package/src/roles/commiter-role.js +2 -2
  46. package/src/roles/discover-role.js +59 -67
  47. package/src/roles/index.js +1 -0
  48. package/src/roles/planner-role.js +54 -38
  49. package/src/roles/refactorer-role.js +8 -7
  50. package/src/roles/researcher-role.js +6 -7
  51. package/src/roles/reviewer-role.js +4 -5
  52. package/src/roles/security-role.js +3 -4
  53. package/src/roles/solomon-role.js +6 -18
  54. package/src/roles/sonar-role.js +5 -1
  55. package/src/roles/tester-role.js +8 -5
  56. package/src/roles/triage-role.js +2 -2
  57. package/src/session-cleanup.js +29 -24
  58. package/src/session-store.js +1 -1
  59. package/src/sonar/api.js +1 -1
  60. package/src/sonar/manager.js +1 -1
  61. package/src/sonar/project-key.js +5 -5
  62. package/src/sonar/scanner.js +34 -65
  63. package/src/utils/display.js +312 -272
  64. package/src/utils/git.js +3 -3
  65. package/src/utils/logger.js +6 -1
  66. package/src/utils/model-selector.js +5 -5
  67. package/src/utils/process.js +80 -102
  68. package/src/utils/rate-limit-detector.js +13 -13
  69. package/src/utils/run-log.js +55 -52
  70. package/templates/kj.config.yml +33 -0
  71. package/templates/roles/architect.md +62 -0
  72. package/templates/roles/planner.md +1 -0
@@ -7,31 +7,27 @@ const CARD_ID_PATTERN = /[A-Z0-9]{2,5}-(?:TSK|BUG|PCS|PRP|SPR|QA)-\d{4}/;
7
7
 
8
8
  export function parseCardId(text) {
9
9
  if (!text) return null;
10
- const match = String(text).match(CARD_ID_PATTERN);
10
+ const match = CARD_ID_PATTERN.exec(String(text));
11
11
  return match ? match[0] : null;
12
12
  }
13
13
 
14
- export function buildTaskFromCard(card) {
15
- const parts = [`## ${card.cardId}: ${card.title}`];
16
-
14
+ function appendDescriptionSection(parts, card) {
17
15
  if (card.descriptionStructured?.length) {
18
16
  parts.push("", "### User Story");
19
17
  for (const s of card.descriptionStructured) {
20
- parts.push(`- **Como** ${s.role}`);
21
- parts.push(` **Quiero** ${s.goal}`);
22
- parts.push(` **Para** ${s.benefit}`);
18
+ parts.push(`- **Como** ${s.role}`, ` **Quiero** ${s.goal}`, ` **Para** ${s.benefit}`);
23
19
  }
24
20
  } else if (card.description) {
25
21
  parts.push("", "### Description", card.description);
26
22
  }
23
+ }
27
24
 
25
+ function appendAcceptanceCriteriaSection(parts, card) {
28
26
  if (card.acceptanceCriteriaStructured?.length) {
29
27
  parts.push("", "### Acceptance Criteria");
30
28
  for (const ac of card.acceptanceCriteriaStructured) {
31
29
  if (ac.given && ac.when && ac.then) {
32
- parts.push(`- **Given** ${ac.given}`);
33
- parts.push(` **When** ${ac.when}`);
34
- parts.push(` **Then** ${ac.then}`);
30
+ parts.push(`- **Given** ${ac.given}`, ` **When** ${ac.when}`, ` **Then** ${ac.then}`);
35
31
  } else if (ac.raw) {
36
32
  parts.push(`- ${ac.raw}`);
37
33
  }
@@ -39,24 +35,31 @@ export function buildTaskFromCard(card) {
39
35
  } else if (card.acceptanceCriteria) {
40
36
  parts.push("", "### Acceptance Criteria", card.acceptanceCriteria);
41
37
  }
38
+ }
42
39
 
43
- if (card.implementationPlan) {
44
- const plan = card.implementationPlan;
45
- parts.push("", "### Implementation Plan");
46
- if (plan.approach) parts.push(`**Approach:** ${plan.approach}`);
47
- if (plan.steps?.length) {
48
- parts.push("**Steps:**");
49
- for (const step of plan.steps) {
50
- parts.push(`1. ${step.description}`);
51
- }
40
+ function appendImplementationPlanSection(parts, card) {
41
+ if (!card.implementationPlan) return;
42
+ const plan = card.implementationPlan;
43
+ parts.push("", "### Implementation Plan");
44
+ if (plan.approach) parts.push(`**Approach:** ${plan.approach}`);
45
+ if (plan.steps?.length) {
46
+ parts.push("**Steps:**");
47
+ for (const step of plan.steps) {
48
+ parts.push(`1. ${step.description}`);
52
49
  }
53
50
  }
51
+ }
54
52
 
53
+ export function buildTaskFromCard(card) {
54
+ const parts = [`## ${card.cardId}: ${card.title}`];
55
+ appendDescriptionSection(parts, card);
56
+ appendAcceptanceCriteriaSection(parts, card);
57
+ appendImplementationPlanSection(parts, card);
55
58
  return parts.join("\n");
56
59
  }
57
60
 
58
61
  export function buildCommitsPayload(gitLog) {
59
- if (!gitLog || !gitLog.length) return [];
62
+ if (!gitLog?.length) return [];
60
63
  return gitLog.map((entry) => ({
61
64
  hash: entry.hash,
62
65
  message: entry.message,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Automatic ADR generation from architect tradeoffs.
3
+ * Creates ADRs in Planning Game when PG is linked, or returns suggestions otherwise.
4
+ */
5
+
6
+ function buildAdr({ tradeoff, taskTitle }) {
7
+ return {
8
+ title: tradeoff,
9
+ status: "accepted",
10
+ context: `Architecture decision for task: ${taskTitle}`,
11
+ decision: tradeoff
12
+ };
13
+ }
14
+
15
+ export async function createArchitectADRs({ tradeoffs, pgTaskId, pgProject, taskTitle, mcpClient }) {
16
+ if (!tradeoffs?.length) {
17
+ return { created: 0, adrs: [] };
18
+ }
19
+
20
+ const hasPg = Boolean(pgTaskId && pgProject && mcpClient);
21
+
22
+ if (!hasPg) {
23
+ const adrs = tradeoffs.map(tradeoff => ({
24
+ ...buildAdr({ tradeoff, taskTitle }),
25
+ suggestion: true
26
+ }));
27
+ return { created: 0, adrs };
28
+ }
29
+
30
+ const adrs = [];
31
+ let created = 0;
32
+
33
+ for (const tradeoff of tradeoffs) {
34
+ const adr = buildAdr({ tradeoff, taskTitle });
35
+ try {
36
+ await mcpClient.createAdr({ projectId: pgProject, adr });
37
+ adrs.push(adr);
38
+ created++;
39
+ } catch {
40
+ // Log warning but don't block pipeline for a single ADR failure
41
+ }
42
+ }
43
+
44
+ return { created, adrs };
45
+ }
@@ -9,7 +9,7 @@
9
9
  * Requires planning_game.api_url in config or PG_API_URL env var.
10
10
  */
11
11
 
12
- import { withRetry, isTransientError } from "../utils/retry.js";
12
+ import { withRetry } from "../utils/retry.js";
13
13
 
14
14
  const DEFAULT_API_URL = "http://localhost:3000/api";
15
15
  const DEFAULT_TIMEOUT_MS = 10000;
@@ -104,6 +104,20 @@ export async function createCard({ projectId, card, timeoutMs = DEFAULT_TIMEOUT_
104
104
  return parseJsonResponse(response);
105
105
  }
106
106
 
107
+ export async function createAdr({ projectId, adr, timeoutMs = DEFAULT_TIMEOUT_MS }) {
108
+ const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/adrs`;
109
+ const response = await fetchWithRetry(
110
+ url,
111
+ {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify(adr)
115
+ },
116
+ timeoutMs
117
+ );
118
+ return parseJsonResponse(response);
119
+ }
120
+
107
121
  export async function relateCards({ projectId, sourceCardId, targetCardId, relationType, timeoutMs = DEFAULT_TIMEOUT_MS }) {
108
122
  const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards/relate`;
109
123
  const response = await fetchWithRetry(
@@ -74,10 +74,12 @@ export function buildDecompositionQuestion(subtasks, parentCardId) {
74
74
  for (let i = 0; i < subtasks.length; i++) {
75
75
  lines.push(`${i + 1}. ${subtasks[i]}`);
76
76
  }
77
- lines.push("");
78
- lines.push(`Create these as linked cards in Planning Game (parent: ${parentCardId})?`);
79
- lines.push("Each subtask will block the next one (sequential chain).");
80
- lines.push("");
81
- lines.push("Reply: yes / no");
77
+ lines.push(
78
+ "",
79
+ `Create these as linked cards in Planning Game (parent: ${parentCardId})?`,
80
+ "Each subtask will block the next one (sequential chain).",
81
+ "",
82
+ "Reply: yes / no"
83
+ );
82
84
  return lines.join("\n");
83
85
  }
@@ -0,0 +1,88 @@
1
+ const SUBAGENT_PREAMBLE = [
2
+ "IMPORTANT: You are running as a Karajan sub-agent.",
3
+ "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
4
+ "Do NOT use any MCP tools. Focus only on designing the architecture for the task."
5
+ ].join(" ");
6
+
7
+ export const VALID_VERDICTS = new Set(["ready", "needs_clarification"]);
8
+
9
+ export function buildArchitectPrompt({ task, instructions, researchContext = null }) {
10
+ const sections = [SUBAGENT_PREAMBLE];
11
+
12
+ if (instructions) {
13
+ sections.push(instructions);
14
+ }
15
+
16
+ sections.push(
17
+ "You are the architect in a multi-role AI pipeline.",
18
+ "Analyze the task and produce a concrete architecture design including layers, patterns, data model, API contracts, dependencies, and tradeoffs.",
19
+ "## Architecture Guidelines",
20
+ [
21
+ "- Identify the architecture type (layered, microservices, event-driven, etc.)",
22
+ "- Define the layers and their responsibilities",
23
+ "- Identify design patterns to apply",
24
+ "- Define the data model with entities",
25
+ "- Specify API contracts (endpoints, events, interfaces)",
26
+ "- List external and internal dependencies",
27
+ "- Document tradeoffs and their rationale",
28
+ "- If critical decisions cannot be made without more information, list clarifying questions"
29
+ ].join("\n"),
30
+ "Return a single valid JSON object and nothing else.",
31
+ 'JSON schema: {"verdict":"ready|needs_clarification","architecture":{"type":string,"layers":[string],"patterns":[string],"dataModel":{"entities":[string]},"apiContracts":[string],"dependencies":[string],"tradeoffs":[string]},"questions":[string],"summary":string}'
32
+ );
33
+
34
+ if (researchContext) {
35
+ sections.push(`## Research Context\n${researchContext}`);
36
+ }
37
+
38
+ sections.push(`## Task\n${task}`);
39
+
40
+ return sections.join("\n\n");
41
+ }
42
+
43
+ function filterStrings(arr) {
44
+ if (!Array.isArray(arr)) return [];
45
+ return arr.filter((item) => typeof item === "string");
46
+ }
47
+
48
+ export function parseArchitectOutput(raw) {
49
+ const text = raw?.trim() || "";
50
+ const jsonMatch = /\{[\s\S]*\}/.exec(text);
51
+ if (!jsonMatch) return null;
52
+
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(jsonMatch[0]);
56
+ } catch {
57
+ return null;
58
+ }
59
+
60
+ const verdict = VALID_VERDICTS.has(parsed.verdict)
61
+ ? parsed.verdict
62
+ : "needs_clarification";
63
+
64
+ const arch = parsed.architecture && typeof parsed.architecture === "object"
65
+ ? parsed.architecture
66
+ : {};
67
+
68
+ const dataModel = arch.dataModel && typeof arch.dataModel === "object"
69
+ ? arch.dataModel
70
+ : {};
71
+
72
+ return {
73
+ verdict,
74
+ architecture: {
75
+ type: typeof arch.type === "string" ? arch.type : "",
76
+ layers: filterStrings(arch.layers),
77
+ patterns: filterStrings(arch.patterns),
78
+ dataModel: {
79
+ entities: filterStrings(dataModel.entities)
80
+ },
81
+ apiContracts: filterStrings(arch.apiContracts),
82
+ dependencies: filterStrings(arch.dependencies),
83
+ tradeoffs: filterStrings(arch.tradeoffs)
84
+ },
85
+ questions: filterStrings(parsed.questions),
86
+ summary: parsed.summary || ""
87
+ };
88
+ }
@@ -6,11 +6,11 @@ const SUBAGENT_PREAMBLE = [
6
6
 
7
7
  export const DISCOVER_MODES = ["gaps", "momtest", "wendel", "classify", "jtbd"];
8
8
 
9
- const VALID_VERDICTS = ["ready", "needs_validation"];
10
- const VALID_SEVERITIES = ["critical", "major", "minor"];
11
- const VALID_WENDEL_STATUSES = ["pass", "fail", "unknown", "not_applicable"];
12
- const VALID_CLASSIFY_TYPES = ["START", "STOP", "DIFFERENT", "not_applicable"];
13
- const VALID_ADOPTION_RISKS = ["none", "low", "medium", "high"];
9
+ const VALID_VERDICTS = new Set(["ready", "needs_validation"]);
10
+ const VALID_SEVERITIES = new Set(["critical", "major", "minor"]);
11
+ const VALID_WENDEL_STATUSES = new Set(["pass", "fail", "unknown", "not_applicable"]);
12
+ const VALID_CLASSIFY_TYPES = new Set(["START", "STOP", "DIFFERENT", "not_applicable"]);
13
+ const VALID_ADOPTION_RISKS = new Set(["none", "low", "medium", "high"]);
14
14
 
15
15
  export function buildDiscoverPrompt({ task, instructions, mode = "gaps", context = null }) {
16
16
  const sections = [SUBAGENT_PREAMBLE];
@@ -21,10 +21,7 @@ export function buildDiscoverPrompt({ task, instructions, mode = "gaps", context
21
21
 
22
22
  sections.push(
23
23
  "You are a task discovery agent for Karajan Code, a multi-agent coding orchestrator.",
24
- "Analyze the following task and identify gaps, ambiguities, missing information, and implicit assumptions."
25
- );
26
-
27
- sections.push(
24
+ "Analyze the following task and identify gaps, ambiguities, missing information, and implicit assumptions.",
28
25
  "## Gap Detection Guidelines",
29
26
  [
30
27
  "- Look for missing acceptance criteria or requirements",
@@ -143,56 +140,60 @@ export function buildDiscoverPrompt({ task, instructions, mode = "gaps", context
143
140
  return sections.join("\n\n");
144
141
  }
145
142
 
146
- export function parseDiscoverOutput(raw) {
147
- const text = raw?.trim() || "";
148
- const jsonMatch = text.match(/\{[\s\S]*\}/);
149
- if (!jsonMatch) return null;
150
-
151
- let parsed;
152
- try {
153
- parsed = JSON.parse(jsonMatch[0]);
154
- } catch {
155
- return null;
156
- }
157
-
158
- const verdict = VALID_VERDICTS.includes(parsed.verdict) ? parsed.verdict : "ready";
143
+ function parseClassification(raw) {
144
+ if (!raw || typeof raw !== "object") return null;
145
+ const rawType = String(raw.type || "").toUpperCase();
146
+ let type;
147
+ if (rawType === "NOT_APPLICABLE") type = "not_applicable";
148
+ else if (VALID_CLASSIFY_TYPES.has(rawType)) type = rawType;
149
+ else type = "not_applicable";
150
+ const rawRisk = String(raw.adoptionRisk || "").toLowerCase();
151
+ return {
152
+ type,
153
+ adoptionRisk: VALID_ADOPTION_RISKS.has(rawRisk) ? rawRisk : "medium",
154
+ frictionEstimate: raw.frictionEstimate || ""
155
+ };
156
+ }
159
157
 
160
- const rawGaps = Array.isArray(parsed.gaps) ? parsed.gaps : [];
161
- const gaps = rawGaps
162
- .filter((g) => g && g.id && g.description && g.suggestedQuestion)
158
+ function parseGaps(rawGaps) {
159
+ return (Array.isArray(rawGaps) ? rawGaps : [])
160
+ .filter((g) => g?.id && g.description && g.suggestedQuestion)
163
161
  .map((g) => ({
164
162
  id: g.id,
165
163
  description: g.description,
166
- severity: VALID_SEVERITIES.includes(String(g.severity).toLowerCase())
164
+ severity: VALID_SEVERITIES.has(String(g.severity).toLowerCase())
167
165
  ? String(g.severity).toLowerCase()
168
166
  : "major",
169
167
  suggestedQuestion: g.suggestedQuestion
170
168
  }));
169
+ }
171
170
 
172
- const rawQuestions = Array.isArray(parsed.momTestQuestions) ? parsed.momTestQuestions : [];
173
- const momTestQuestions = rawQuestions
174
- .filter((q) => q && q.gapId && q.question && q.targetRole && q.rationale)
171
+ function parseMomTestQuestions(rawQuestions) {
172
+ return (Array.isArray(rawQuestions) ? rawQuestions : [])
173
+ .filter((q) => q?.gapId && q.question && q.targetRole && q.rationale)
175
174
  .map((q) => ({
176
175
  gapId: q.gapId,
177
176
  question: q.question,
178
177
  targetRole: q.targetRole,
179
178
  rationale: q.rationale
180
179
  }));
180
+ }
181
181
 
182
- const rawChecklist = Array.isArray(parsed.wendelChecklist) ? parsed.wendelChecklist : [];
183
- const wendelChecklist = rawChecklist
184
- .filter((c) => c && c.condition && c.justification && c.status)
182
+ function parseWendelChecklist(rawChecklist) {
183
+ return (Array.isArray(rawChecklist) ? rawChecklist : [])
184
+ .filter((c) => c?.condition && c.justification && c.status)
185
185
  .map((c) => ({
186
186
  condition: c.condition,
187
- status: VALID_WENDEL_STATUSES.includes(String(c.status).toLowerCase())
187
+ status: VALID_WENDEL_STATUSES.has(String(c.status).toLowerCase())
188
188
  ? String(c.status).toLowerCase()
189
189
  : "unknown",
190
190
  justification: c.justification
191
191
  }));
192
+ }
192
193
 
193
- const rawJtbds = Array.isArray(parsed.jtbds) ? parsed.jtbds : [];
194
- const jtbds = rawJtbds
195
- .filter((j) => j && j.id && j.functional && j.emotionalPersonal && j.emotionalSocial && j.behaviorChange && j.evidence)
194
+ function parseJtbds(rawJtbds) {
195
+ return (Array.isArray(rawJtbds) ? rawJtbds : [])
196
+ .filter((j) => j?.id && j.functional && j.emotionalPersonal && j.emotionalSocial && j.behaviorChange && j.evidence)
196
197
  .map((j) => ({
197
198
  id: j.id,
198
199
  functional: j.functional,
@@ -201,27 +202,27 @@ export function parseDiscoverOutput(raw) {
201
202
  behaviorChange: j.behaviorChange,
202
203
  evidence: j.evidence
203
204
  }));
205
+ }
204
206
 
205
- let classification = null;
206
- if (parsed.classification && typeof parsed.classification === "object") {
207
- const rawType = String(parsed.classification.type || "").toUpperCase();
208
- const type = rawType === "NOT_APPLICABLE" ? "not_applicable"
209
- : VALID_CLASSIFY_TYPES.includes(rawType) ? rawType : "not_applicable";
210
- const rawRisk = String(parsed.classification.adoptionRisk || "").toLowerCase();
211
- classification = {
212
- type,
213
- adoptionRisk: VALID_ADOPTION_RISKS.includes(rawRisk) ? rawRisk : "medium",
214
- frictionEstimate: parsed.classification.frictionEstimate || ""
215
- };
207
+ export function parseDiscoverOutput(raw) {
208
+ const text = raw?.trim() || "";
209
+ const jsonMatch = /\{[\s\S]*\}/.exec(text);
210
+ if (!jsonMatch) return null;
211
+
212
+ let parsed;
213
+ try {
214
+ parsed = JSON.parse(jsonMatch[0]);
215
+ } catch {
216
+ return null;
216
217
  }
217
218
 
218
219
  return {
219
- verdict,
220
- gaps,
221
- momTestQuestions,
222
- wendelChecklist,
223
- classification,
224
- jtbds,
220
+ verdict: VALID_VERDICTS.has(parsed.verdict) ? parsed.verdict : "ready",
221
+ gaps: parseGaps(parsed.gaps),
222
+ momTestQuestions: parseMomTestQuestions(parsed.momTestQuestions),
223
+ wendelChecklist: parseWendelChecklist(parsed.wendelChecklist),
224
+ classification: parseClassification(parsed.classification),
225
+ jtbds: parseJtbds(parsed.jtbds),
225
226
  summary: parsed.summary || ""
226
227
  };
227
228
  }
@@ -1,3 +1,25 @@
1
+ function extractStepText(line) {
2
+ const numberedStep = /^\d+[).:-]\s*(.+)$/.exec(line);
3
+ if (numberedStep) return numberedStep[1].trim();
4
+ const bulletStep = /^[-*]\s+(.+)$/.exec(line);
5
+ if (bulletStep) return bulletStep[1].trim();
6
+ return null;
7
+ }
8
+
9
+ function classifyLine(line, state) {
10
+ if (!state.title) {
11
+ const titleMatch = /^title\s*:\s*(.+)$/i.exec(line);
12
+ if (titleMatch) return { type: "title", value: titleMatch[1].trim() };
13
+ }
14
+ if (!state.approach) {
15
+ const approachMatch = /^(approach|strategy)\s*:\s*(.+)$/i.exec(line);
16
+ if (approachMatch) return { type: "approach", value: approachMatch[2].trim() };
17
+ }
18
+ const stepText = extractStepText(line);
19
+ if (stepText) return { type: "step", value: stepText };
20
+ return null;
21
+ }
22
+
1
23
  export function parsePlannerOutput(output) {
2
24
  const text = String(output || "").trim();
3
25
  if (!text) return null;
@@ -7,49 +29,42 @@ export function parsePlannerOutput(output) {
7
29
  .map((line) => line.trim())
8
30
  .filter(Boolean);
9
31
 
10
- let title = null;
11
- let approach = null;
32
+ const state = { title: null, approach: null };
12
33
  const steps = [];
13
34
 
14
35
  for (const line of lines) {
15
- if (!title) {
16
- const titleMatch = line.match(/^title\s*:\s*(.+)$/i);
17
- if (titleMatch) {
18
- title = titleMatch[1].trim();
19
- continue;
20
- }
21
- }
36
+ const classified = classifyLine(line, state);
37
+ if (!classified) continue;
38
+ if (classified.type === "title") state.title = classified.value;
39
+ else if (classified.type === "approach") state.approach = classified.value;
40
+ else if (classified.type === "step") steps.push(classified.value);
41
+ }
22
42
 
23
- if (!approach) {
24
- const approachMatch = line.match(/^(approach|strategy)\s*:\s*(.+)$/i);
25
- if (approachMatch) {
26
- approach = approachMatch[2].trim();
27
- continue;
28
- }
29
- }
43
+ if (!state.title) {
44
+ const firstFreeLine = lines.find((line) => !/^(approach|strategy)\s*:/i.test(line) && !/^\d+[).:-]\s*/.test(line));
45
+ state.title = firstFreeLine || null;
46
+ }
30
47
 
31
- const numberedStep = line.match(/^\d+[\).:-]\s*(.+)$/);
32
- if (numberedStep) {
33
- steps.push(numberedStep[1].trim());
34
- continue;
35
- }
48
+ return { title: state.title, approach: state.approach, steps };
49
+ }
36
50
 
37
- const bulletStep = line.match(/^[-*]\s+(.+)$/);
38
- if (bulletStep) {
39
- steps.push(bulletStep[1].trim());
40
- continue;
41
- }
42
- }
51
+ function formatArchitectContext(architectContext) {
52
+ if (!architectContext) return null;
53
+ const arch = architectContext.architecture || {};
54
+ const lines = ["## Architecture Context"];
43
55
 
44
- if (!title) {
45
- const firstFreeLine = lines.find((line) => !/^(approach|strategy)\s*:/i.test(line) && !/^\d+[\).:-]\s*/.test(line));
46
- title = firstFreeLine || null;
47
- }
56
+ if (arch.type) lines.push(`**Type:** ${arch.type}`);
57
+ if (arch.layers?.length) lines.push(`**Layers:** ${arch.layers.join(", ")}`);
58
+ if (arch.patterns?.length) lines.push(`**Patterns:** ${arch.patterns.join(", ")}`);
59
+ if (arch.dataModel?.entities?.length) lines.push(`**Data model entities:** ${arch.dataModel.entities.join(", ")}`);
60
+ if (arch.apiContracts?.length) lines.push(`**API contracts:** ${arch.apiContracts.join(", ")}`);
61
+ if (arch.tradeoffs?.length) lines.push(`**Tradeoffs:** ${arch.tradeoffs.join(", ")}`);
62
+ if (architectContext.summary) lines.push(`**Summary:** ${architectContext.summary}`);
48
63
 
49
- return { title, approach, steps };
64
+ return lines.length > 1 ? lines.join("\n") : null;
50
65
  }
51
66
 
52
- export function buildPlannerPrompt({ task, context }) {
67
+ export function buildPlannerPrompt({ task, context, architectContext }) {
53
68
  const parts = [
54
69
  "You are an expert software architect. Create an implementation plan for the following task.",
55
70
  "",
@@ -62,6 +77,11 @@ export function buildPlannerPrompt({ task, context }) {
62
77
  parts.push("## Context", context, "");
63
78
  }
64
79
 
80
+ const archSection = formatArchitectContext(architectContext);
81
+ if (archSection) {
82
+ parts.push(archSection, "");
83
+ }
84
+
65
85
  parts.push(
66
86
  "## Output format",
67
87
  "Respond with a JSON object containing:",
@@ -10,7 +10,8 @@ const ROLE_DESCRIPTIONS = [
10
10
  { role: "tester", description: "Runs dedicated testing pass after coding. Ensures tests exist and pass." },
11
11
  { role: "security", description: "Audits code for security vulnerabilities. Checks auth, input validation, injection risks." },
12
12
  { role: "refactorer", description: "Cleans up and refactors code after the main implementation." },
13
- { role: "reviewer", description: "Reviews the code diff for quality issues. Standard quality gate." }
13
+ { role: "reviewer", description: "Reviews the code diff for quality issues. Standard quality gate." },
14
+ { role: "architect", description: "Designs solution architecture — layers, patterns, data model, API contracts, tradeoffs. Activate when task creates new module/app, affects data model/APIs, complexity is medium/complex, or design is ambiguous." }
14
15
  ];
15
16
 
16
17
  export function buildTriagePrompt({ task, instructions, availableRoles }) {
@@ -24,15 +25,9 @@ export function buildTriagePrompt({ task, instructions, availableRoles }) {
24
25
 
25
26
  sections.push(
26
27
  "You are a task triage agent for Karajan Code, a multi-agent coding orchestrator.",
27
- "Analyze the following task and determine which pipeline roles should be activated."
28
- );
29
-
30
- sections.push(
28
+ "Analyze the following task and determine which pipeline roles should be activated.",
31
29
  "## Available Roles",
32
- roles.map((r) => `- **${r.role}**: ${r.description}`).join("\n")
33
- );
34
-
35
- sections.push(
30
+ roles.map((r) => `- **${r.role}**: ${r.description}`).join("\n"),
36
31
  "## Decision Guidelines",
37
32
  [
38
33
  "- **planner**: Enable for complex tasks (multi-file, architectural changes, data model changes). Disable for simple fixes.",
@@ -41,20 +36,17 @@ export function buildTriagePrompt({ task, instructions, availableRoles }) {
41
36
  "- **security**: Enable for authentication, APIs, user input handling, data access, external integrations. Disable for UI-only or doc changes.",
42
37
  "- **refactorer**: Enable only when explicitly requested or when the task is a refactoring task.",
43
38
  "- **reviewer**: Enable for most tasks as a quality gate. Disable only for trivial, single-line changes.",
39
+ "- **architect**: Enable when creating new modules/apps, changing data models or APIs, medium/complex tasks, or when design approach is ambiguous. Disable for simple fixes, doc-only, or CSS-only changes.",
44
40
  "",
45
41
  "Note: coder is ALWAYS active — you don't need to decide on it."
46
- ].join("\n")
47
- );
48
-
49
- sections.push(
42
+ ].join("\n"),
50
43
  "Classify the task complexity, determine its taskType, recommend only the necessary pipeline roles, and assess whether the task should be decomposed into smaller subtasks.",
51
44
  "Keep the reasoning short and practical.",
52
45
  "Return a single valid JSON object and nothing else.",
53
- 'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security"],"taskType":"sw|infra|doc|add-tests|refactor","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}'
46
+ 'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security|architect"],"taskType":"sw|infra|doc|add-tests|refactor","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}',
47
+ `## Task\n${task}`
54
48
  );
55
49
 
56
- sections.push(`## Task\n${task}`);
57
-
58
50
  return sections.join("\n\n");
59
51
  }
60
52
 
@@ -22,33 +22,32 @@ export function parseMaybeJsonString(value) {
22
22
  }
23
23
  }
24
24
 
25
- export function normalizeReviewPayload(payload) {
26
- if (!payload) return null;
27
-
28
- if (payload.approved !== undefined && payload.blocking_issues !== undefined) {
29
- return payload;
30
- }
25
+ function isReviewPayload(obj) {
26
+ return obj?.approved !== undefined && obj?.blocking_issues !== undefined;
27
+ }
31
28
 
32
- if (Array.isArray(payload)) {
33
- for (let i = payload.length - 1; i >= 0; i -= 1) {
34
- const item = payload[i];
35
- if (item?.approved !== undefined && item?.blocking_issues !== undefined) {
36
- return item;
37
- }
29
+ function findReviewInArray(arr) {
30
+ for (let i = arr.length - 1; i >= 0; i -= 1) {
31
+ const item = arr[i];
32
+ if (isReviewPayload(item)) return item;
38
33
 
39
- const nested = item?.result || item?.message?.content?.[0]?.text;
40
- if (typeof nested === "string") {
41
- const parsedNested = parseMaybeJsonString(nested);
42
- if (parsedNested?.approved !== undefined) return parsedNested;
43
- }
34
+ const nested = item?.result || item?.message?.content?.[0]?.text;
35
+ if (typeof nested === "string") {
36
+ const parsedNested = parseMaybeJsonString(nested);
37
+ if (parsedNested?.approved !== undefined) return parsedNested;
44
38
  }
45
- return null;
46
39
  }
40
+ return null;
41
+ }
42
+
43
+ export function normalizeReviewPayload(payload) {
44
+ if (!payload) return null;
45
+ if (isReviewPayload(payload)) return payload;
46
+ if (Array.isArray(payload)) return findReviewInArray(payload);
47
47
 
48
48
  if (typeof payload.result === "string") {
49
49
  const parsedResult = parseMaybeJsonString(payload.result);
50
50
  if (parsedResult?.approved !== undefined) return parsedResult;
51
- return null;
52
51
  }
53
52
 
54
53
  return null;