karajan-code 2.5.0 → 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 +29 -3
- package/src/commands/board.js +23 -0
- package/src/commands/doctor.js +130 -362
- 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 -1923
- 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/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, domainContext = null, projectDir = null, language = "en" }) {
|
|
12
|
+
export async function buildArchitectPrompt({ task, instructions, researchContext = null, productContext = null, domainContext = null, projectDir = null, language = "en", provider = null }) {
|
|
13
13
|
const langInstruction = getLanguageInstruction(language);
|
|
14
14
|
const sections = [SUBAGENT_PREAMBLE, ...(langInstruction ? [langInstruction] : [])];
|
|
15
15
|
|
|
@@ -51,7 +51,7 @@ export async function buildArchitectPrompt({ task, instructions, researchContext
|
|
|
51
51
|
|
|
52
52
|
if (projectDir) {
|
|
53
53
|
const skills = await loadAvailableSkills(projectDir);
|
|
54
|
-
const skillSection = buildSkillSection(skills);
|
|
54
|
+
const skillSection = buildSkillSection(skills, { provider, role: "architect" });
|
|
55
55
|
if (skillSection) {
|
|
56
56
|
sections.push(skillSection);
|
|
57
57
|
}
|
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, domainContext = 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", provider = null }) {
|
|
37
37
|
const langInstruction = getLanguageInstruction(language);
|
|
38
38
|
const sections = [
|
|
39
39
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
@@ -108,7 +108,7 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
108
108
|
|
|
109
109
|
if (projectDir) {
|
|
110
110
|
const skills = await loadAvailableSkills(projectDir);
|
|
111
|
-
const skillSection = buildSkillSection(skills);
|
|
111
|
+
const skillSection = buildSkillSection(skills, { provider });
|
|
112
112
|
if (skillSection) {
|
|
113
113
|
sections.push(skillSection);
|
|
114
114
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-aware role template resolver.
|
|
3
|
+
*
|
|
4
|
+
* Extends the legacy `resolveRoleMdPath()` contract from base-role.js to
|
|
5
|
+
* support per-provider variants without breaking existing consumers.
|
|
6
|
+
*
|
|
7
|
+
* Lookup order (first match wins):
|
|
8
|
+
*
|
|
9
|
+
* 1. <projectDir>/.karajan/roles/{role}/{provider}.md project override, per provider
|
|
10
|
+
* 2. <projectDir>/.karajan/roles/{role}/default.md project override, default
|
|
11
|
+
* 3. <projectDir>/.karajan/roles/{role}.md project override, legacy flat
|
|
12
|
+
* 4. <karajanHome>/roles/{role}/{provider}.md user home, per provider
|
|
13
|
+
* 5. <karajanHome>/roles/{role}/default.md user home, default
|
|
14
|
+
* 6. <karajanHome>/roles/{role}.md user home, legacy flat
|
|
15
|
+
* 7. <built-in>/templates/roles/{role}/{provider}.md built-in, per provider
|
|
16
|
+
* 8. <built-in>/templates/roles/{role}/default.md built-in, default
|
|
17
|
+
* 9. <built-in>/templates/roles/{role}.md built-in, legacy flat (back-compat)
|
|
18
|
+
*
|
|
19
|
+
* When `provider` is undefined/null the per-provider entries are skipped and
|
|
20
|
+
* only the default + legacy tiers are tried.
|
|
21
|
+
*
|
|
22
|
+
* If a provider is supplied but no per-provider file exists, we emit a
|
|
23
|
+
* one-time `prompt-resolver:fallback` event/log entry so the user knows their
|
|
24
|
+
* config asked for a variant that isn't present.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from "node:fs/promises";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
31
|
+
import { normalizeProvider } from "../utils/provider-env.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compute the full list of candidate file paths for a role template.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} role - role slug (e.g. "coder")
|
|
37
|
+
* @param {string|null|undefined} provider - provider slug or CLI name;
|
|
38
|
+
* normalized internally
|
|
39
|
+
* @param {string|null|undefined} projectDir
|
|
40
|
+
* @returns {{ perProvider: string[], default: string[], legacy: string[], all: string[], provider: string|null }}
|
|
41
|
+
*/
|
|
42
|
+
export function resolveRolePromptPaths(role, provider, projectDir) {
|
|
43
|
+
if (!role || typeof role !== "string") {
|
|
44
|
+
throw new Error("resolveRolePromptPaths: role name is required");
|
|
45
|
+
}
|
|
46
|
+
const normalized = provider ? normalizeProvider(provider) : null;
|
|
47
|
+
const canonical = normalized && normalized.length > 0 ? normalized : null;
|
|
48
|
+
|
|
49
|
+
const homes = [];
|
|
50
|
+
if (projectDir) homes.push(path.join(projectDir, ".karajan", "roles"));
|
|
51
|
+
homes.push(path.join(getKarajanHome(), "roles"));
|
|
52
|
+
|
|
53
|
+
const builtInRoot = path.resolve(
|
|
54
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
55
|
+
"..",
|
|
56
|
+
"..",
|
|
57
|
+
"templates",
|
|
58
|
+
"roles"
|
|
59
|
+
);
|
|
60
|
+
homes.push(builtInRoot);
|
|
61
|
+
|
|
62
|
+
const perProvider = [];
|
|
63
|
+
const defaults = [];
|
|
64
|
+
const legacy = [];
|
|
65
|
+
|
|
66
|
+
for (const home of homes) {
|
|
67
|
+
if (canonical) {
|
|
68
|
+
perProvider.push(path.join(home, role, `${canonical}.md`));
|
|
69
|
+
}
|
|
70
|
+
defaults.push(path.join(home, role, "default.md"));
|
|
71
|
+
legacy.push(path.join(home, `${role}.md`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Interleave: for each home, try per-provider first, then default, then
|
|
75
|
+
// legacy. This gives the user's project override precedence over the
|
|
76
|
+
// built-in default even when they only supplied a legacy flat file.
|
|
77
|
+
const all = [];
|
|
78
|
+
for (const home of homes) {
|
|
79
|
+
if (canonical) all.push(path.join(home, role, `${canonical}.md`));
|
|
80
|
+
all.push(path.join(home, role, "default.md"));
|
|
81
|
+
all.push(path.join(home, `${role}.md`));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { perProvider, default: defaults, legacy, all, provider: canonical };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Return the contents of the first existing candidate path. Returns null if
|
|
89
|
+
* no template is found.
|
|
90
|
+
*
|
|
91
|
+
* @param {string[]} paths
|
|
92
|
+
* @returns {Promise<{content: string, path: string}|null>}
|
|
93
|
+
*/
|
|
94
|
+
export async function readFirstExisting(paths) {
|
|
95
|
+
for (const p of paths) {
|
|
96
|
+
try {
|
|
97
|
+
const content = await fs.readFile(p, "utf8");
|
|
98
|
+
return { content, path: p };
|
|
99
|
+
} catch { /* file not found, keep trying */ }
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load role instructions resolved for a specific provider.
|
|
106
|
+
*
|
|
107
|
+
* @param {Object} args
|
|
108
|
+
* @param {string} args.role
|
|
109
|
+
* @param {string|null|undefined} args.provider
|
|
110
|
+
* @param {string|null|undefined} args.projectDir
|
|
111
|
+
* @param {{ warn?: Function, debug?: Function }} [args.logger]
|
|
112
|
+
* @returns {Promise<{ instructions: string|null, path: string|null, resolvedProvider: string|null, fellBack: boolean }>}
|
|
113
|
+
*/
|
|
114
|
+
export async function loadRoleInstructions({ role, provider, projectDir, logger } = {}) {
|
|
115
|
+
const { all, provider: canonical } = resolveRolePromptPaths(role, provider, projectDir);
|
|
116
|
+
const hit = await readFirstExisting(all);
|
|
117
|
+
if (!hit) {
|
|
118
|
+
return { instructions: null, path: null, resolvedProvider: canonical, fellBack: !!canonical };
|
|
119
|
+
}
|
|
120
|
+
// Detect fallback: if the user asked for a provider but we matched a path
|
|
121
|
+
// that is NOT a per-provider file, we consider it a fallback and warn.
|
|
122
|
+
const fellBack = !!canonical && !hit.path.endsWith(`${canonical}.md`);
|
|
123
|
+
if (fellBack && logger?.debug) {
|
|
124
|
+
logger.debug(`prompt-resolver: role "${role}" has no "${canonical}" variant, using ${path.basename(hit.path)}`);
|
|
125
|
+
}
|
|
126
|
+
return { instructions: hit.content, path: hit.path, resolvedProvider: canonical, fellBack };
|
|
127
|
+
}
|
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
|
+
}
|