karajan-code 2.4.1 → 2.6.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.
- package/README.md +20 -4
- package/package.json +5 -2
- package/src/agents/aider-agent.js +1 -2
- package/src/agents/base-agent.js +63 -1
- package/src/agents/claude-agent.js +3 -4
- package/src/agents/codex-agent.js +1 -2
- package/src/agents/gemini-agent.js +1 -2
- package/src/agents/index.js +11 -2
- package/src/agents/opencode-agent.js +1 -2
- package/src/brain/decision-tracker.js +123 -0
- package/src/brain/decisor.js +243 -0
- package/src/brain/solomon-consult.js +163 -0
- package/src/budget/comparison.js +96 -0
- package/src/checks/binaries.js +123 -0
- package/src/checks/ci.js +105 -0
- package/src/checks/config-files.js +217 -0
- package/src/checks/dir-setup.js +63 -0
- package/src/checks/mcp-health.js +157 -0
- package/src/checks/node.js +61 -0
- package/src/checks/ports.js +110 -0
- package/src/checks/rtk.js +36 -0
- package/src/checks/runner.js +267 -0
- package/src/checks/skills.js +49 -0
- package/src/checks/sonar.js +67 -0
- package/src/checks/system.js +53 -0
- package/src/checks/tokens.js +96 -0
- package/src/checks/types.js +105 -0
- package/src/cli.js +90 -7
- package/src/commands/board.js +23 -0
- package/src/commands/doctor.js +130 -362
- package/src/commands/plan.js +221 -37
- package/src/commands/resume.js +1 -1
- package/src/commands/run.js +2 -1
- package/src/commands/skills.js +49 -0
- package/src/config/schema.js +222 -0
- package/src/config.js +32 -3
- package/src/guards/policy-resolver.js +8 -0
- package/src/hu/store.js +9 -0
- package/src/infrastructure/command-runner.js +44 -0
- package/src/infrastructure/environment.js +38 -0
- package/src/infrastructure/file-system-service.js +63 -0
- package/src/infrastructure/mocks.js +182 -0
- package/src/mcp/handlers/management-handlers.js +7 -2
- package/src/mcp/tools.js +10 -3
- package/src/orchestrator/config-init.js +16 -1
- package/src/orchestrator/flow-runner.js +2085 -0
- package/src/orchestrator/pipeline-context.js +17 -0
- package/src/orchestrator/preflight-checks.js +78 -4
- package/src/orchestrator/solomon-escalation.js +21 -0
- package/src/orchestrator/stages/stage-executor.js +155 -0
- package/src/orchestrator.js +19 -1884
- package/src/plan/plan-executor.js +69 -0
- package/src/plan/plan-hu-ops.js +134 -0
- package/src/plan/plan-id.js +25 -0
- package/src/plan/plan-schema.js +139 -0
- package/src/plan/plan-store.js +58 -25
- package/src/prompts/architect.js +2 -2
- package/src/prompts/coder.js +2 -2
- package/src/prompts/prompt-resolver.js +127 -0
- package/src/prompts/reviewer.js +2 -2
- package/src/roles/architect-role.js +3 -1
- package/src/roles/base-role.js +23 -2
- package/src/roles/coder-role.js +3 -1
- package/src/roles/security-role.js +12 -0
- package/src/roles/tester-role.js +11 -0
- package/src/session/journal/decisions-writer.js +200 -0
- package/src/session/journal/iteration-logger.js +194 -0
- package/src/session/journal/summary-writer.js +176 -0
- package/src/session/journal/tree-writer.js +164 -0
- package/src/skills/semantic-detector.js +147 -0
- package/src/skills/skill-detector.js +186 -8
- package/src/skills/skill-loader.js +131 -1
- package/src/skills/skills-cache.js +128 -0
- package/src/types/agent.js +55 -0
- package/src/types/config.js +114 -0
- package/src/types/hu.js +47 -0
- package/src/types/index.js +34 -0
- package/src/types/policy.js +43 -0
- package/src/types/session.js +74 -0
- package/src/types/stage.js +68 -0
- package/src/utils/display/event-handlers.js +389 -0
- package/src/utils/display/formatters.js +132 -0
- package/src/utils/display/header.js +48 -0
- package/src/utils/display/session-summary.js +108 -0
- package/src/utils/display/solomon.js +42 -0
- package/src/utils/port-check.js +45 -0
- package/src/utils/port-occupant.js +95 -0
- package/src/utils/provider-env.js +116 -0
- package/src/utils/with-timeout.js +42 -0
- package/templates/roles/architect/anthropic.md +65 -0
- package/templates/roles/architect/default.md +63 -0
- package/templates/roles/architect/google.md +52 -0
- package/templates/roles/architect/openai.md +51 -0
- package/templates/roles/architect/opencode.md +50 -0
- package/templates/roles/coder/anthropic.md +72 -0
- package/templates/roles/coder/default.md +69 -0
- package/templates/roles/coder/google.md +78 -0
- package/templates/roles/coder/openai.md +59 -0
- package/templates/roles/coder/opencode.md +49 -0
- package/templates/roles/planner/anthropic.md +50 -0
- package/templates/roles/planner/default.md +48 -0
- package/templates/roles/planner/google.md +66 -0
- package/templates/roles/planner/openai.md +46 -0
- package/templates/roles/planner/opencode.md +53 -0
- package/templates/roles/reviewer/anthropic.md +61 -0
- package/templates/roles/reviewer/default.md +62 -0
- package/templates/roles/reviewer/google.md +63 -0
- package/templates/roles/reviewer/openai.md +57 -0
- package/templates/roles/reviewer/opencode.md +42 -0
- package/templates/roles/solomon/anthropic.md +71 -0
- package/templates/roles/solomon/default.md +83 -0
- package/templates/roles/solomon/google.md +61 -0
- package/templates/roles/solomon/openai.md +53 -0
- package/templates/roles/solomon/opencode.md +55 -0
- package/src/utils/display.js +0 -669
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, domainContext = 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", provider = null }) {
|
|
28
28
|
const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
|
|
29
29
|
|
|
30
30
|
const langInstruction = getLanguageInstruction(language);
|
|
@@ -64,7 +64,7 @@ export async function buildReviewerPrompt({ task, diff, reviewRules, mode, seren
|
|
|
64
64
|
|
|
65
65
|
if (projectDir) {
|
|
66
66
|
const skills = await loadAvailableSkills(projectDir);
|
|
67
|
-
const skillSection = buildSkillSection(skills);
|
|
67
|
+
const skillSection = buildSkillSection(skills, { provider, role: "reviewer" });
|
|
68
68
|
if (skillSection) {
|
|
69
69
|
sections.push(skillSection);
|
|
70
70
|
}
|
|
@@ -24,7 +24,9 @@ export class ArchitectRole extends AgentRole {
|
|
|
24
24
|
const prompt = await buildArchitectPrompt({
|
|
25
25
|
task, instructions: this.instructions, researchContext,
|
|
26
26
|
productContext: this.config?.productContext || null,
|
|
27
|
-
domainContext: this.config?.domainContext || null
|
|
27
|
+
domainContext: this.config?.domainContext || null,
|
|
28
|
+
projectDir: this.config?.projectDir || null,
|
|
29
|
+
provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null)
|
|
28
30
|
});
|
|
29
31
|
return { prompt };
|
|
30
32
|
}
|
package/src/roles/base-role.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getKarajanHome } from "../utils/paths.js";
|
|
4
|
+
import { loadRoleInstructions } from "../prompts/prompt-resolver.js";
|
|
4
5
|
|
|
5
6
|
const ROLE_EVENTS = {
|
|
6
7
|
START: "role:start",
|
|
@@ -8,6 +9,12 @@ const ROLE_EVENTS = {
|
|
|
8
9
|
ERROR: "role:error"
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Legacy flat resolver. Kept for back-compat with external callers and
|
|
14
|
+
* existing tests that assert the 3-entry candidate list. New code should use
|
|
15
|
+
* `loadRoleInstructions()` from src/prompts/prompt-resolver.js, which
|
|
16
|
+
* additionally tries per-provider and per-role subdirectories.
|
|
17
|
+
*/
|
|
11
18
|
function resolveRoleMdPath(roleName, projectDir) {
|
|
12
19
|
const fileName = `${roleName}.md`;
|
|
13
20
|
const candidates = [];
|
|
@@ -58,8 +65,22 @@ export class BaseRole {
|
|
|
58
65
|
async init(context = {}) {
|
|
59
66
|
this.context = context;
|
|
60
67
|
const projectDir = this.config.projectDir || process.cwd();
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
// Subclasses (AgentRole) can override resolveProvider to expose the
|
|
69
|
+
// provider slug for the active role, which lets the prompt resolver
|
|
70
|
+
// pick a per-provider template variant when available.
|
|
71
|
+
const provider = typeof this.resolveProvider === "function"
|
|
72
|
+
? this.resolveProvider()
|
|
73
|
+
: null;
|
|
74
|
+
const { instructions, path: templatePath, resolvedProvider, fellBack } = await loadRoleInstructions({
|
|
75
|
+
role: this.name,
|
|
76
|
+
provider,
|
|
77
|
+
projectDir,
|
|
78
|
+
logger: this.logger,
|
|
79
|
+
});
|
|
80
|
+
this.instructions = instructions;
|
|
81
|
+
this._templatePath = templatePath;
|
|
82
|
+
this._resolvedProvider = resolvedProvider;
|
|
83
|
+
this._promptFellBack = fellBack;
|
|
63
84
|
this._initialized = true;
|
|
64
85
|
}
|
|
65
86
|
|
package/src/roles/coder-role.js
CHANGED
|
@@ -40,7 +40,9 @@ export class CoderRole extends AgentRole {
|
|
|
40
40
|
serenaEnabled: Boolean(this.config?.serena?.enabled),
|
|
41
41
|
rtkAvailable: Boolean(this.config?.rtk?.available),
|
|
42
42
|
productContext: this.config?.productContext || null,
|
|
43
|
-
domainContext: this.config?.domainContext || null
|
|
43
|
+
domainContext: this.config?.domainContext || null,
|
|
44
|
+
projectDir: this.config?.projectDir || null,
|
|
45
|
+
provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null)
|
|
44
46
|
});
|
|
45
47
|
return { prompt };
|
|
46
48
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentRole } from "./agent-role.js";
|
|
2
2
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
3
|
+
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
3
4
|
|
|
4
5
|
const SUBAGENT_PREAMBLE = [
|
|
5
6
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -23,6 +24,17 @@ export class SecurityRole extends AgentRole {
|
|
|
23
24
|
`## Task\n${task}`
|
|
24
25
|
);
|
|
25
26
|
if (diff) sections.push(`## Git diff to audit\n${diff}`);
|
|
27
|
+
|
|
28
|
+
// Inject security-relevant skills (owasp-*, security-*, sast-*) filtered
|
|
29
|
+
// by role=security.
|
|
30
|
+
const projectDir = this.config?.projectDir || process.cwd();
|
|
31
|
+
const skills = await loadAvailableSkills(projectDir);
|
|
32
|
+
const skillSection = buildSkillSection(skills, {
|
|
33
|
+
provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null),
|
|
34
|
+
role: "security",
|
|
35
|
+
});
|
|
36
|
+
if (skillSection) sections.push(skillSection);
|
|
37
|
+
|
|
26
38
|
return { prompt: sections.join("\n\n") };
|
|
27
39
|
}
|
|
28
40
|
|
package/src/roles/tester-role.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentRole } from "./agent-role.js";
|
|
2
2
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
3
3
|
import { detectTestFramework } from "../utils/project-detect.js";
|
|
4
|
+
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
4
5
|
|
|
5
6
|
const SUBAGENT_PREAMBLE = [
|
|
6
7
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -108,6 +109,16 @@ export class TesterRole extends AgentRole {
|
|
|
108
109
|
);
|
|
109
110
|
if (diff) sections.push(`## Git diff\n${diff}`);
|
|
110
111
|
if (sonarIssues) sections.push(`## Sonar test issues\n${sonarIssues}`);
|
|
112
|
+
|
|
113
|
+
// Inject test-relevant skills (filtered by role=tester to only include
|
|
114
|
+
// testing patterns like pytest-patterns, vitest-patterns, etc.)
|
|
115
|
+
const skills = await loadAvailableSkills(projectDir);
|
|
116
|
+
const skillSection = buildSkillSection(skills, {
|
|
117
|
+
provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null),
|
|
118
|
+
role: "tester",
|
|
119
|
+
});
|
|
120
|
+
if (skillSection) sections.push(skillSection);
|
|
121
|
+
|
|
111
122
|
return { prompt: sections.join("\n\n") };
|
|
112
123
|
}
|
|
113
124
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a human-readable `decisions.md` journal file summarizing every
|
|
3
|
+
* Solomon ruling and Brain decision made during a session.
|
|
4
|
+
*
|
|
5
|
+
* Acceptance criterion (KJC-TSK-0287):
|
|
6
|
+
* - Given Solomon was invoked at least once → decisions.md is written with
|
|
7
|
+
* timestamp, trigger, conflict context, ruling (action + reasoning),
|
|
8
|
+
* and resulting actions for every invocation.
|
|
9
|
+
* - Given Solomon was NOT invoked → decisions.md is NOT created.
|
|
10
|
+
*
|
|
11
|
+
* Brain routing decisions (from KJC-TSK-0322) are appended as a secondary
|
|
12
|
+
* section when present, since they share the same "decisions" semantics.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { getSessionRoot } from "../../utils/paths.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} SolomonRuling
|
|
21
|
+
* @property {string} at - ISO timestamp
|
|
22
|
+
* @property {string} stage - conflict stage (reviewer, brain-routing, ...)
|
|
23
|
+
* @property {string} trigger - short reason the invocation was triggered
|
|
24
|
+
* @property {Object} conflict - conflict context (task, reason, summary)
|
|
25
|
+
* @property {string} ruling - approve / continue / escalate_human / create_subtask / ...
|
|
26
|
+
* @property {string} [reasoning]
|
|
27
|
+
* @property {string[]} [conditions]
|
|
28
|
+
* @property {string} [alternativeAgent]
|
|
29
|
+
* @property {string} [result] - what the orchestrator did with the ruling
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} BrainDecisionRecord
|
|
34
|
+
* @property {number} seq
|
|
35
|
+
* @property {string} at
|
|
36
|
+
* @property {string} intent
|
|
37
|
+
* @property {string[]} rolesOn
|
|
38
|
+
* @property {string} confidence
|
|
39
|
+
* @property {string} rationale
|
|
40
|
+
* @property {string[]} appliedOverrides
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
function esc(text) {
|
|
44
|
+
return String(text ?? "").replace(/\|/g, "\\|");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatConditions(conditions) {
|
|
48
|
+
if (!Array.isArray(conditions) || conditions.length === 0) return "—";
|
|
49
|
+
return conditions.map((c) => `- ${esc(c)}`).join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatConflict(conflict) {
|
|
53
|
+
if (!conflict) return "—";
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (conflict.task) parts.push(`**Task**: ${esc(conflict.task.slice(0, 200))}${conflict.task.length > 200 ? "…" : ""}`);
|
|
56
|
+
if (conflict.reason) parts.push(`**Reason**: ${esc(conflict.reason)}`);
|
|
57
|
+
if (conflict.summary) parts.push(`**Summary**: ${esc(conflict.summary)}`);
|
|
58
|
+
return parts.length > 0 ? parts.join("\n") : "—";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render a Solomon ruling as a Markdown subsection.
|
|
63
|
+
* @param {SolomonRuling} ruling
|
|
64
|
+
* @param {number} index
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
function renderRuling(ruling, index) {
|
|
68
|
+
const at = ruling.at || "(unknown timestamp)";
|
|
69
|
+
const header = `### ${index + 1}. Solomon @ ${at} — ${esc(ruling.ruling || "unknown")}`;
|
|
70
|
+
const lines = [
|
|
71
|
+
header,
|
|
72
|
+
"",
|
|
73
|
+
`- **Stage**: ${esc(ruling.stage || "?")}`,
|
|
74
|
+
`- **Trigger**: ${esc(ruling.trigger || "?")}`,
|
|
75
|
+
`- **Ruling**: \`${esc(ruling.ruling || "?")}\``,
|
|
76
|
+
];
|
|
77
|
+
if (ruling.alternativeAgent) lines.push(`- **Alternative agent**: ${esc(ruling.alternativeAgent)}`);
|
|
78
|
+
if (ruling.result) lines.push(`- **Applied result**: ${esc(ruling.result)}`);
|
|
79
|
+
lines.push("");
|
|
80
|
+
if (ruling.reasoning) {
|
|
81
|
+
lines.push("**Reasoning:**", "");
|
|
82
|
+
lines.push(ruling.reasoning.trim(), "");
|
|
83
|
+
}
|
|
84
|
+
if (ruling.conditions?.length) {
|
|
85
|
+
lines.push("**Conditions:**", "");
|
|
86
|
+
lines.push(formatConditions(ruling.conditions), "");
|
|
87
|
+
}
|
|
88
|
+
lines.push("**Conflict context:**", "");
|
|
89
|
+
lines.push(formatConflict(ruling.conflict), "");
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderBrainDecision(decision, index) {
|
|
94
|
+
return [
|
|
95
|
+
`### ${index + 1}. Brain @ ${decision.at} — ${esc(decision.intent)}`,
|
|
96
|
+
"",
|
|
97
|
+
`- **Confidence**: ${esc(decision.confidence)}`,
|
|
98
|
+
`- **Roles on**: ${(decision.rolesOn || []).map(esc).join(", ") || "—"}`,
|
|
99
|
+
`- **Overrides applied**: ${(decision.appliedOverrides || []).map(esc).join(", ") || "—"}`,
|
|
100
|
+
"",
|
|
101
|
+
`> ${esc(decision.rationale || "")}`,
|
|
102
|
+
"",
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the Markdown content for a session. Returns `null` if no Solomon
|
|
108
|
+
* rulings are present (per acceptance criteria — no Solomon, no file).
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} session
|
|
111
|
+
* @returns {string|null}
|
|
112
|
+
*/
|
|
113
|
+
export function buildDecisionsMarkdown(session) {
|
|
114
|
+
const rulings = Array.isArray(session?.solomonRulings) ? session.solomonRulings : [];
|
|
115
|
+
const brainDecisions = Array.isArray(session?.brainDecisions) ? session.brainDecisions : [];
|
|
116
|
+
|
|
117
|
+
// AC: no Solomon invocation → do not generate the file.
|
|
118
|
+
if (rulings.length === 0) return null;
|
|
119
|
+
|
|
120
|
+
const sessionId = session?.id || "(unknown)";
|
|
121
|
+
const header = [
|
|
122
|
+
`# Session decisions — ${sessionId}`,
|
|
123
|
+
"",
|
|
124
|
+
`_Generated at ${new Date().toISOString()}._`,
|
|
125
|
+
"",
|
|
126
|
+
`This journal records every decision the pipeline delegated to Solomon, plus Brain routing decisions for cross-reference.`,
|
|
127
|
+
"",
|
|
128
|
+
"## Summary",
|
|
129
|
+
"",
|
|
130
|
+
`- Solomon invocations: **${rulings.length}**`,
|
|
131
|
+
`- Brain routing decisions: **${brainDecisions.length}**`,
|
|
132
|
+
"",
|
|
133
|
+
"---",
|
|
134
|
+
"",
|
|
135
|
+
"## Solomon rulings",
|
|
136
|
+
"",
|
|
137
|
+
].join("\n");
|
|
138
|
+
|
|
139
|
+
const solomonSection = rulings.map((r, i) => renderRuling(r, i)).join("\n---\n\n");
|
|
140
|
+
|
|
141
|
+
const brainSection = brainDecisions.length > 0
|
|
142
|
+
? "\n---\n\n## Brain routing decisions\n\n" + brainDecisions.map((d, i) => renderBrainDecision(d, i)).join("\n")
|
|
143
|
+
: "";
|
|
144
|
+
|
|
145
|
+
return header + solomonSection + brainSection + "\n";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Persist decisions.md to the session's journal directory. No-op when there
|
|
150
|
+
* are no Solomon rulings (per acceptance criteria).
|
|
151
|
+
*
|
|
152
|
+
* Path resolution (first match wins):
|
|
153
|
+
* 1. options.journalDir — direct path to the session journal directory
|
|
154
|
+
* 2. options.sessionRoot + id — combines with session.id
|
|
155
|
+
* 3. getSessionRoot() + id — default Karajan session root
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} session
|
|
158
|
+
* @param {{ journalDir?: string, sessionRoot?: string, logger?: Object }} [options]
|
|
159
|
+
* @returns {Promise<{ written: boolean, path: string|null }>}
|
|
160
|
+
*/
|
|
161
|
+
export async function writeDecisionsJournal(session, options = {}) {
|
|
162
|
+
const content = buildDecisionsMarkdown(session);
|
|
163
|
+
if (content === null) return { written: false, path: null };
|
|
164
|
+
|
|
165
|
+
const sessionDir = options.journalDir
|
|
166
|
+
|| path.join(options.sessionRoot || getSessionRoot(), session.id);
|
|
167
|
+
const filePath = path.join(sessionDir, "decisions.md");
|
|
168
|
+
try {
|
|
169
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
170
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
171
|
+
options.logger?.info?.(`Wrote decisions journal: ${filePath}`);
|
|
172
|
+
return { written: true, path: filePath };
|
|
173
|
+
} catch (err) {
|
|
174
|
+
options.logger?.warn?.(`Failed to write decisions journal (non-blocking): ${err.message}`);
|
|
175
|
+
return { written: false, path: null };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Append a Solomon ruling to session.solomonRulings. Creates the array if
|
|
181
|
+
* absent. Callers (invokeSolomon) pass the structured record.
|
|
182
|
+
*
|
|
183
|
+
* @param {Object} session
|
|
184
|
+
* @param {SolomonRuling} record
|
|
185
|
+
*/
|
|
186
|
+
export function recordSolomonRuling(session, record) {
|
|
187
|
+
if (!session) return;
|
|
188
|
+
if (!Array.isArray(session.solomonRulings)) session.solomonRulings = [];
|
|
189
|
+
session.solomonRulings.push({
|
|
190
|
+
at: record.at || new Date().toISOString(),
|
|
191
|
+
stage: record.stage || "unknown",
|
|
192
|
+
trigger: record.trigger || "",
|
|
193
|
+
conflict: record.conflict || {},
|
|
194
|
+
ruling: record.ruling || "unknown",
|
|
195
|
+
reasoning: record.reasoning || "",
|
|
196
|
+
conditions: Array.isArray(record.conditions) ? [...record.conditions] : [],
|
|
197
|
+
alternativeAgent: record.alternativeAgent || null,
|
|
198
|
+
result: record.result || "",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-iteration journal entries for `.reviews/{sessionId}/iterations.md`.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator calls `recordIteration(session, data)` after every
|
|
5
|
+
* iteration of the coder/reviewer loop completes. Entries are appended to
|
|
6
|
+
* `session._journalIterations` and rendered as Markdown at session end.
|
|
7
|
+
*
|
|
8
|
+
* Acceptance criterion (KJC-TSK-0287):
|
|
9
|
+
* - Given a kj run with multiple iterations → iterations.md exists with
|
|
10
|
+
* one section per iteration containing: number, coder summary, reviewer
|
|
11
|
+
* blocking issues, sonar issues (if any), Solomon ruling (if any),
|
|
12
|
+
* duration.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} IterationData
|
|
17
|
+
* @property {number} iteration - 1-based index
|
|
18
|
+
* @property {number} durationMs
|
|
19
|
+
* @property {string} [coderSummary]
|
|
20
|
+
* @property {string} [reviewerSummary]
|
|
21
|
+
* @property {Array<{id?:string, severity?:string, file?:string, line?:number, description?:string}>} [blockingIssues]
|
|
22
|
+
* @property {string} [sonarSummary]
|
|
23
|
+
* @property {number} [sonarIssuesFound]
|
|
24
|
+
* @property {number} [sonarIssuesResolved]
|
|
25
|
+
* @property {string} [testerSummary]
|
|
26
|
+
* @property {string} [securitySummary]
|
|
27
|
+
* @property {{ ruling?:string, reasoning?:string }} [solomon] - only present when Solomon ran in this iteration
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
function esc(text) {
|
|
31
|
+
return String(text ?? "").replace(/\|/g, "\\|");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatDuration(ms) {
|
|
35
|
+
if (!Number.isFinite(ms) || ms < 0) return "—";
|
|
36
|
+
if (ms < 1000) return `${ms}ms`;
|
|
37
|
+
const s = Math.round(ms / 1000);
|
|
38
|
+
if (s < 60) return `${s}s`;
|
|
39
|
+
const m = Math.floor(s / 60);
|
|
40
|
+
const rs = s % 60;
|
|
41
|
+
return `${m}m ${rs}s`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatBlockingIssues(issues) {
|
|
45
|
+
if (!Array.isArray(issues) || issues.length === 0) return null;
|
|
46
|
+
const lines = ["**Blocking issues**:"];
|
|
47
|
+
for (const issue of issues) {
|
|
48
|
+
const sev = issue.severity ? `[${issue.severity}] ` : "";
|
|
49
|
+
const loc = issue.file ? ` — \`${esc(issue.file)}${issue.line ? `:${issue.line}` : ""}\`` : "";
|
|
50
|
+
const id = issue.id ? `${issue.id}: ` : "";
|
|
51
|
+
const desc = esc(issue.description || issue.message || "(no description)");
|
|
52
|
+
lines.push(`- ${sev}${id}${desc}${loc}`);
|
|
53
|
+
}
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatSonarBlock(data) {
|
|
58
|
+
const parts = [];
|
|
59
|
+
if (data.sonarSummary) parts.push(esc(data.sonarSummary));
|
|
60
|
+
if (Number.isFinite(data.sonarIssuesFound)) {
|
|
61
|
+
const resolved = Number.isFinite(data.sonarIssuesResolved) ? ` (${data.sonarIssuesResolved} resolved)` : "";
|
|
62
|
+
parts.push(`Issues: ${data.sonarIssuesFound}${resolved}`);
|
|
63
|
+
}
|
|
64
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatSolomonBlock(solomon) {
|
|
68
|
+
if (!solomon || !solomon.ruling) return null;
|
|
69
|
+
const lines = [`**Ruling**: \`${esc(solomon.ruling)}\``];
|
|
70
|
+
if (solomon.reasoning) lines.push(`**Reasoning**: ${esc(solomon.reasoning)}`);
|
|
71
|
+
return lines.join(" \n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render a single iteration as a Markdown section.
|
|
76
|
+
*
|
|
77
|
+
* @param {IterationData} data
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function renderIterationEntry(data) {
|
|
81
|
+
const lines = [`## Iteration ${data.iteration}`, ""];
|
|
82
|
+
lines.push(`**Duration**: ${formatDuration(data.durationMs)}`);
|
|
83
|
+
|
|
84
|
+
if (data.coderSummary) {
|
|
85
|
+
lines.push("", "### Coder", data.coderSummary);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (data.reviewerSummary || (data.blockingIssues && data.blockingIssues.length > 0)) {
|
|
89
|
+
lines.push("", "### Reviewer");
|
|
90
|
+
if (data.reviewerSummary) lines.push(data.reviewerSummary);
|
|
91
|
+
const blocks = formatBlockingIssues(data.blockingIssues);
|
|
92
|
+
if (blocks) {
|
|
93
|
+
lines.push("", blocks);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sonarLine = formatSonarBlock(data);
|
|
98
|
+
if (sonarLine) {
|
|
99
|
+
lines.push("", "### Sonar", sonarLine);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (data.testerSummary) lines.push("", "### Tester", data.testerSummary);
|
|
103
|
+
if (data.securitySummary) lines.push("", "### Security", data.securitySummary);
|
|
104
|
+
|
|
105
|
+
const solomonBlock = formatSolomonBlock(data.solomon);
|
|
106
|
+
if (solomonBlock) {
|
|
107
|
+
lines.push("", "### Solomon", solomonBlock);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
lines.push("");
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Append an iteration record to session._journalIterations.
|
|
116
|
+
* Creates the array if absent. Callers pass structured IterationData.
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} session
|
|
119
|
+
* @param {IterationData} data
|
|
120
|
+
*/
|
|
121
|
+
export function recordIteration(session, data) {
|
|
122
|
+
if (!session || !data) return;
|
|
123
|
+
if (!Array.isArray(session._journalIterations)) session._journalIterations = [];
|
|
124
|
+
session._journalIterations.push(renderIterationEntry(data));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build the full iterations.md content from recorded entries.
|
|
129
|
+
* Returns null when there are no iterations to render.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} session
|
|
132
|
+
* @returns {string|null}
|
|
133
|
+
*/
|
|
134
|
+
export function buildIterationsMarkdown(session) {
|
|
135
|
+
const entries = Array.isArray(session?._journalIterations) ? session._journalIterations : [];
|
|
136
|
+
if (entries.length === 0) return null;
|
|
137
|
+
const header = [
|
|
138
|
+
`# Session iterations — ${session.id || "(unknown)"}`,
|
|
139
|
+
"",
|
|
140
|
+
`_Generated at ${new Date().toISOString()}._`,
|
|
141
|
+
"",
|
|
142
|
+
`Total iterations: **${entries.length}**`,
|
|
143
|
+
"",
|
|
144
|
+
"---",
|
|
145
|
+
"",
|
|
146
|
+
].join("\n");
|
|
147
|
+
return header + entries.join("---\n\n") + "\n";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Extract structured iteration data from a stageResults bundle + session state.
|
|
152
|
+
* The orchestrator produces stage results per role; this helper pulls the
|
|
153
|
+
* relevant fields into a single object suitable for recordIteration().
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} args
|
|
156
|
+
* @param {number} args.iteration
|
|
157
|
+
* @param {number} args.durationMs
|
|
158
|
+
* @param {Object} args.stageResults
|
|
159
|
+
* @param {Object} [args.session]
|
|
160
|
+
* @returns {IterationData}
|
|
161
|
+
*/
|
|
162
|
+
export function extractIterationData({ iteration, durationMs, stageResults = {}, session = {} }) {
|
|
163
|
+
const coderSummary = stageResults.coder?.summary || stageResults.coder?.result?.summary;
|
|
164
|
+
const reviewerSummary = stageResults.reviewer?.summary;
|
|
165
|
+
const blockingIssues = stageResults.reviewer?.result?.blocking_issues
|
|
166
|
+
|| stageResults.reviewer?.blocking_issues
|
|
167
|
+
|| [];
|
|
168
|
+
const testerSummary = stageResults.tester?.summary;
|
|
169
|
+
const securitySummary = stageResults.security?.summary;
|
|
170
|
+
const sonarSummary = stageResults.sonar?.summary
|
|
171
|
+
|| (stageResults.sonar?.openIssues != null ? `${stageResults.sonar.openIssues} open issue(s)` : undefined);
|
|
172
|
+
const sonarIssuesFound = stageResults.sonar?.issuesInitial ?? stageResults.sonar?.issuesFound;
|
|
173
|
+
const sonarIssuesResolved = stageResults.sonar?.issuesResolved;
|
|
174
|
+
|
|
175
|
+
// Solomon ruling for THIS iteration: match by iteration if the session
|
|
176
|
+
// tracks it; otherwise just take the latest ruling.
|
|
177
|
+
const rulings = Array.isArray(session.solomonRulings) ? session.solomonRulings : [];
|
|
178
|
+
const solomonRecord = rulings.find((r) => r.iteration === iteration) || rulings.at(-1);
|
|
179
|
+
const solomon = solomonRecord ? { ruling: solomonRecord.ruling, reasoning: solomonRecord.reasoning } : undefined;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
iteration,
|
|
183
|
+
durationMs,
|
|
184
|
+
coderSummary,
|
|
185
|
+
reviewerSummary,
|
|
186
|
+
blockingIssues,
|
|
187
|
+
sonarSummary,
|
|
188
|
+
sonarIssuesFound,
|
|
189
|
+
sonarIssuesResolved,
|
|
190
|
+
testerSummary,
|
|
191
|
+
securitySummary,
|
|
192
|
+
solomon,
|
|
193
|
+
};
|
|
194
|
+
}
|