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/commands/plan.js
CHANGED
|
@@ -4,13 +4,11 @@ import { resolveRole } from "../config.js";
|
|
|
4
4
|
import { buildPlannerPrompt } from "../prompts/planner.js";
|
|
5
5
|
import { parseMaybeJsonString } from "../review/parser.js";
|
|
6
6
|
|
|
7
|
+
// ---- Formatting helpers ----
|
|
8
|
+
|
|
7
9
|
function formatPlan(plan) {
|
|
8
10
|
const lines = [];
|
|
9
|
-
|
|
10
|
-
if (plan.approach) {
|
|
11
|
-
lines.push("## Approach", plan.approach, "");
|
|
12
|
-
}
|
|
13
|
-
|
|
11
|
+
if (plan.approach) lines.push("## Approach", plan.approach, "");
|
|
14
12
|
if (plan.steps?.length) {
|
|
15
13
|
lines.push("## Steps");
|
|
16
14
|
for (let i = 0; i < plan.steps.length; i++) {
|
|
@@ -20,27 +18,39 @@ function formatPlan(plan) {
|
|
|
20
18
|
}
|
|
21
19
|
lines.push("");
|
|
22
20
|
}
|
|
23
|
-
|
|
24
21
|
if (plan.risks?.length) {
|
|
25
22
|
lines.push("## Risks");
|
|
26
|
-
for (const risk of plan.risks) {
|
|
27
|
-
lines.push(`- ${risk}`);
|
|
28
|
-
}
|
|
23
|
+
for (const risk of plan.risks) lines.push(`- ${risk}`);
|
|
29
24
|
lines.push("");
|
|
30
25
|
}
|
|
31
|
-
|
|
32
26
|
if (plan.outOfScope?.length) {
|
|
33
27
|
lines.push("## Out of scope");
|
|
34
|
-
for (const item of plan.outOfScope) {
|
|
35
|
-
lines.push(`- ${item}`);
|
|
36
|
-
}
|
|
28
|
+
for (const item of plan.outOfScope) lines.push(`- ${item}`);
|
|
37
29
|
lines.push("");
|
|
38
30
|
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
39
33
|
|
|
34
|
+
function formatHuTable(hus) {
|
|
35
|
+
if (!hus?.length) return " (no HUs)";
|
|
36
|
+
const lines = [];
|
|
37
|
+
const maxTitle = Math.min(50, Math.max(...hus.map(h => h.title?.length || 0)));
|
|
38
|
+
lines.push(` ${"ID".padEnd(30)} ${"Title".padEnd(maxTitle)} ${"Type".padEnd(10)} ${"Status".padEnd(10)} Deps`);
|
|
39
|
+
lines.push(` ${"─".repeat(30)} ${"─".repeat(maxTitle)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(15)}`);
|
|
40
|
+
for (const hu of hus) {
|
|
41
|
+
const title = (hu.title || "").slice(0, maxTitle).padEnd(maxTitle);
|
|
42
|
+
const deps = (hu.blocked_by || []).join(", ") || "—";
|
|
43
|
+
lines.push(` ${hu.id.padEnd(30)} ${title} ${(hu.task_type || "sw").padEnd(10)} ${(hu.status || "?").padEnd(10)} ${deps}`);
|
|
44
|
+
}
|
|
40
45
|
return lines.join("\n");
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
// ---- Subcommand handlers ----
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* kj plan "task" — generate plan + HUs
|
|
52
|
+
*/
|
|
53
|
+
export async function planGenerateCommand({ task, config, logger, json, context }) {
|
|
44
54
|
const plannerRole = resolveRole(config, "planner");
|
|
45
55
|
await assertAgentsAvailable([plannerRole.provider]);
|
|
46
56
|
|
|
@@ -54,43 +64,217 @@ export async function planCommand({ task, config, logger, json, context }) {
|
|
|
54
64
|
: undefined;
|
|
55
65
|
const result = await planner.runTask({ prompt, role: "planner", silenceTimeoutMs, timeoutMs });
|
|
56
66
|
|
|
57
|
-
if (!result.ok)
|
|
58
|
-
throw new Error(result.error || result.output || "Planner failed");
|
|
59
|
-
}
|
|
67
|
+
if (!result.ok) throw new Error(result.error || result.output || "Planner failed");
|
|
60
68
|
|
|
61
69
|
const parsed = parseMaybeJsonString(result.output);
|
|
62
70
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
// Create v2 plan with HUs from planner steps
|
|
72
|
+
const { createPlanV2 } = await import("../plan/plan-schema.js");
|
|
73
|
+
const { addHu } = await import("../plan/plan-hu-ops.js");
|
|
74
|
+
const { savePlan } = await import("../plan/plan-store.js");
|
|
75
|
+
const { deriveProjectName, classifyTaskType } = await import("../hu/auto-generator.js");
|
|
76
|
+
|
|
77
|
+
const projectDir = config.projectDir || process.cwd();
|
|
78
|
+
const plan = createPlanV2(task);
|
|
79
|
+
plan.name = deriveProjectName(task);
|
|
80
|
+
plan.approach = parsed?.approach || (typeof parsed === "string" ? parsed : (typeof result.output === "string" ? result.output : null));
|
|
81
|
+
plan.risks = parsed?.risks || [];
|
|
82
|
+
plan.outOfScope = parsed?.outOfScope || [];
|
|
83
|
+
|
|
84
|
+
// Convert planner steps → HUs
|
|
85
|
+
const steps = parsed?.steps || [];
|
|
86
|
+
let prevId = null;
|
|
87
|
+
for (const step of steps) {
|
|
88
|
+
const desc = typeof step === "string" ? step : step.description || step.title || JSON.stringify(step);
|
|
89
|
+
const hu = addHu(plan, {
|
|
90
|
+
title: desc.slice(0, 80),
|
|
91
|
+
task_type: classifyTaskType(desc),
|
|
92
|
+
scope: desc,
|
|
93
|
+
blocked_by: prevId ? [prevId] : [],
|
|
94
|
+
acceptance_tests: [
|
|
95
|
+
"npx vitest run 2>&1; test $? -eq 0 && echo PASS || echo FAIL"
|
|
96
|
+
]
|
|
74
97
|
});
|
|
75
|
-
|
|
76
|
-
logger.warn(`Plan persistence failed: ${err.message}`);
|
|
98
|
+
prevId = hu.id;
|
|
77
99
|
}
|
|
78
100
|
|
|
101
|
+
const planId = await savePlan(projectDir, plan);
|
|
102
|
+
|
|
79
103
|
if (json) {
|
|
80
|
-
|
|
81
|
-
const outputObj = typeof jsonOutput === "object" ? { ...jsonOutput, planId } : { plan: jsonOutput, planId };
|
|
82
|
-
console.log(JSON.stringify(outputObj, null, 2));
|
|
104
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
83
105
|
return;
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
if (parsed?.approach) {
|
|
87
109
|
console.log(formatPlan(parsed));
|
|
110
|
+
} else if (plan.approach) {
|
|
111
|
+
console.log(plan.approach);
|
|
112
|
+
}
|
|
113
|
+
console.log(`\n## HUs (${plan.hus.length})`);
|
|
114
|
+
console.log(formatHuTable(plan.hus));
|
|
115
|
+
console.log(`\nPlan saved: ${planId} (${plan.hus.length} HUs, status: ${plan.status})`);
|
|
116
|
+
console.log(`Review: kj plan show ${planId}`);
|
|
117
|
+
console.log(`Approve: kj plan ready ${planId}`);
|
|
118
|
+
console.log(`Execute: kj run --plan ${planId} "${task.slice(0, 40)}..."`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* kj plan list
|
|
123
|
+
*/
|
|
124
|
+
export async function planListCommand({ config }) {
|
|
125
|
+
const { listPlans } = await import("../plan/plan-store.js");
|
|
126
|
+
const projectDir = config.projectDir || process.cwd();
|
|
127
|
+
const plans = await listPlans(projectDir);
|
|
128
|
+
|
|
129
|
+
if (plans.length === 0) {
|
|
130
|
+
console.log("No plans found. Create one with: kj plan \"your task\"");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(`Plans for ${projectDir}:\n`);
|
|
135
|
+
for (const p of plans) {
|
|
136
|
+
const husInfo = p.huCount > 0 ? ` (${p.huCount} HUs)` : "";
|
|
137
|
+
const status = p.status !== "draft" ? ` [${p.status}]` : "";
|
|
138
|
+
console.log(` ${p.planId} ${(p.name || p.task || "").slice(0, 50)}${husInfo}${status}`);
|
|
139
|
+
console.log(` Created: ${p.createdAt}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* kj plan show <planId>
|
|
145
|
+
*/
|
|
146
|
+
export async function planShowCommand({ config, planId }) {
|
|
147
|
+
const { loadPlan } = await import("../plan/plan-store.js");
|
|
148
|
+
const projectDir = config.projectDir || process.cwd();
|
|
149
|
+
const plan = await loadPlan(projectDir, planId);
|
|
150
|
+
|
|
151
|
+
if (!plan) {
|
|
152
|
+
console.error(`Plan not found: ${planId}`);
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`Plan: ${plan.planId}`);
|
|
158
|
+
console.log(`Task: ${plan.task}`);
|
|
159
|
+
console.log(`Name: ${plan.name || "—"}`);
|
|
160
|
+
console.log(`Status: ${plan.status}`);
|
|
161
|
+
console.log(`Version: ${plan.version}`);
|
|
162
|
+
console.log(`Created: ${plan.createdAt}`);
|
|
163
|
+
|
|
164
|
+
if (plan.approach) {
|
|
165
|
+
console.log(`\n## Approach\n${plan.approach}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (plan.hus?.length > 0) {
|
|
169
|
+
console.log(`\n## HUs (${plan.hus.length})`);
|
|
170
|
+
console.log(formatHuTable(plan.hus));
|
|
88
171
|
} else {
|
|
89
|
-
console.log(
|
|
172
|
+
console.log("\nNo HUs defined. Add with: kj plan add-hu " + planId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* kj plan ready <planId>
|
|
178
|
+
*/
|
|
179
|
+
export async function planReadyCommand({ config, planId }) {
|
|
180
|
+
const { loadPlan, savePlan } = await import("../plan/plan-store.js");
|
|
181
|
+
const { certifyAllHus } = await import("../plan/plan-hu-ops.js");
|
|
182
|
+
const { validatePlan } = await import("../plan/plan-schema.js");
|
|
183
|
+
const projectDir = config.projectDir || process.cwd();
|
|
184
|
+
|
|
185
|
+
const plan = await loadPlan(projectDir, planId);
|
|
186
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
187
|
+
|
|
188
|
+
const validation = validatePlan(plan);
|
|
189
|
+
if (!validation.valid) {
|
|
190
|
+
console.error("Plan validation failed:");
|
|
191
|
+
for (const err of validation.errors) console.error(` - ${err}`);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
return;
|
|
90
194
|
}
|
|
91
195
|
|
|
92
|
-
if (
|
|
93
|
-
console.
|
|
94
|
-
|
|
196
|
+
if (plan.hus.length === 0) {
|
|
197
|
+
console.error("Cannot approve a plan with 0 HUs.");
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
return;
|
|
95
200
|
}
|
|
201
|
+
|
|
202
|
+
const count = certifyAllHus(plan);
|
|
203
|
+
await savePlan(projectDir, plan);
|
|
204
|
+
console.log(`✅ ${count} HUs certified. Plan status: ready`);
|
|
205
|
+
console.log(`Execute: kj run --plan ${planId} "${(plan.task || "").slice(0, 40)}..."`);
|
|
96
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* kj plan validate <planId>
|
|
210
|
+
*/
|
|
211
|
+
export async function planValidateCommand({ config, planId }) {
|
|
212
|
+
const { loadPlan } = await import("../plan/plan-store.js");
|
|
213
|
+
const { validatePlan } = await import("../plan/plan-schema.js");
|
|
214
|
+
const projectDir = config.projectDir || process.cwd();
|
|
215
|
+
|
|
216
|
+
const plan = await loadPlan(projectDir, planId);
|
|
217
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
218
|
+
|
|
219
|
+
const result = validatePlan(plan);
|
|
220
|
+
if (result.valid) {
|
|
221
|
+
console.log(`✅ Plan ${planId} is valid (${plan.hus.length} HUs, status: ${plan.status})`);
|
|
222
|
+
} else {
|
|
223
|
+
console.error(`❌ Plan ${planId} has ${result.errors.length} error(s):`);
|
|
224
|
+
for (const err of result.errors) console.error(` - ${err}`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* kj plan delete <planId>
|
|
231
|
+
*/
|
|
232
|
+
export async function planDeleteCommand({ config, planId }) {
|
|
233
|
+
const { deletePlan } = await import("../plan/plan-store.js");
|
|
234
|
+
const projectDir = config.projectDir || process.cwd();
|
|
235
|
+
const ok = await deletePlan(projectDir, planId);
|
|
236
|
+
console.log(ok ? `Deleted: ${planId}` : `Not found: ${planId}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* kj plan add-hu <planId> --title "..." --type sw --deps hu_xxx
|
|
241
|
+
*/
|
|
242
|
+
export async function planAddHuCommand({ config, planId, title, type, deps, scope }) {
|
|
243
|
+
const { loadPlan, savePlan } = await import("../plan/plan-store.js");
|
|
244
|
+
const { addHu } = await import("../plan/plan-hu-ops.js");
|
|
245
|
+
const projectDir = config.projectDir || process.cwd();
|
|
246
|
+
|
|
247
|
+
const plan = await loadPlan(projectDir, planId);
|
|
248
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
249
|
+
|
|
250
|
+
const hu = addHu(plan, {
|
|
251
|
+
title: title || "New HU",
|
|
252
|
+
task_type: type || "sw",
|
|
253
|
+
scope: scope || null,
|
|
254
|
+
blocked_by: deps ? deps.split(",").map(d => d.trim()) : []
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await savePlan(projectDir, plan);
|
|
258
|
+
console.log(`Added: ${hu.id} — ${hu.title}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* kj plan remove-hu <planId> <huId>
|
|
263
|
+
*/
|
|
264
|
+
export async function planRemoveHuCommand({ config, planId, huId }) {
|
|
265
|
+
const { loadPlan, savePlan } = await import("../plan/plan-store.js");
|
|
266
|
+
const { removeHu } = await import("../plan/plan-hu-ops.js");
|
|
267
|
+
const projectDir = config.projectDir || process.cwd();
|
|
268
|
+
|
|
269
|
+
const plan = await loadPlan(projectDir, planId);
|
|
270
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
271
|
+
|
|
272
|
+
const ok = removeHu(plan, huId);
|
|
273
|
+
if (!ok) { console.error(`HU not found: ${huId}`); process.exitCode = 1; return; }
|
|
274
|
+
|
|
275
|
+
await savePlan(projectDir, plan);
|
|
276
|
+
console.log(`Removed: ${huId}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Keep backward compat — old callers import planCommand
|
|
280
|
+
export const planCommand = planGenerateCommand;
|
package/src/commands/resume.js
CHANGED
|
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import { resumeFlow } from "../orchestrator.js";
|
|
4
4
|
import { createActivityLog } from "../activity-log.js";
|
|
5
|
-
import { printEvent } from "../utils/display.js";
|
|
5
|
+
import { printEvent } from "../utils/display/event-handlers.js";
|
|
6
6
|
|
|
7
7
|
function createCliAskQuestion() {
|
|
8
8
|
return async (question, context) => {
|
package/src/commands/run.js
CHANGED
|
@@ -3,7 +3,8 @@ import readline from "node:readline";
|
|
|
3
3
|
import { runFlow } from "../orchestrator.js";
|
|
4
4
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
5
5
|
import { createActivityLog } from "../activity-log.js";
|
|
6
|
-
import { printHeader
|
|
6
|
+
import { printHeader } from "../utils/display/header.js";
|
|
7
|
+
import { printEvent } from "../utils/display/event-handlers.js";
|
|
7
8
|
import { resolveRole } from "../config.js";
|
|
8
9
|
import { parseCardId } from "../planning-game/adapter.js";
|
|
9
10
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj skills` — manage the local OpenSkills cache.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* - list : print cached skills with their age and freshness.
|
|
6
|
+
* - clear-cache : delete all cached metadata (forces re-install on next session).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { listCached, clearCache, getCacheRoot, DEFAULT_TTL_MS } from "../skills/skills-cache.js";
|
|
10
|
+
|
|
11
|
+
function humanAge(cachedAt) {
|
|
12
|
+
const ms = Date.now() - Date.parse(cachedAt);
|
|
13
|
+
if (!Number.isFinite(ms) || ms < 0) return "unknown";
|
|
14
|
+
const days = ms / (24 * 60 * 60 * 1000);
|
|
15
|
+
if (days < 1) return `${Math.max(1, Math.round(ms / (60 * 60 * 1000)))}h ago`;
|
|
16
|
+
return `${days.toFixed(1)}d ago`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function skillsCommand({ action = "list" } = {}) {
|
|
20
|
+
switch (action) {
|
|
21
|
+
case "list":
|
|
22
|
+
return listAction();
|
|
23
|
+
case "clear-cache":
|
|
24
|
+
return clearAction();
|
|
25
|
+
default:
|
|
26
|
+
console.error(`Unknown action: ${action}. Use: list | clear-cache`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function listAction() {
|
|
32
|
+
const cache = await listCached();
|
|
33
|
+
if (cache.length === 0) {
|
|
34
|
+
console.log(`No cached skills at ${getCacheRoot()}`);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
console.log(`Cached skills (TTL ${Math.round(DEFAULT_TTL_MS / 86400000)}d) at ${getCacheRoot()}:\n`);
|
|
38
|
+
for (const entry of cache) {
|
|
39
|
+
const flag = entry.fresh ? "FRESH" : "STALE";
|
|
40
|
+
console.log(` ${flag} ${entry.name} (${humanAge(entry.cachedAt)})`);
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function clearAction() {
|
|
46
|
+
const { removed } = await clearCache();
|
|
47
|
+
console.log(`Cleared ${removed} cached skill${removed === 1 ? "" : "s"} from ${getCacheRoot()}`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Valibot schema for Karajan's configuration surface (KJC-TSK-0318).
|
|
3
|
+
*
|
|
4
|
+
* Origin: this schema is based on Jorge del Casar's original work in PR #379
|
|
5
|
+
* (closed). The full 650-line mirror-of-DEFAULTS approach he proposed drifted
|
|
6
|
+
* heavily against `main` by the time it was reviewed, so this file takes the
|
|
7
|
+
* same intent — validating Karajan config at load time with Valibot — but
|
|
8
|
+
* narrows the scope to the handful of fields where validation actually
|
|
9
|
+
* catches real bugs (review_mode, methodology, max_iterations, percentage
|
|
10
|
+
* budgets, port numbers). Everything else is defined with `v.looseObject` /
|
|
11
|
+
* `v.unknown()` so existing consumers who touch undeclared keys keep working.
|
|
12
|
+
*
|
|
13
|
+
* The runtime DEFAULTS object still lives in src/config.js — it is the source
|
|
14
|
+
* of truth for default values. This schema exists to reject invalid input
|
|
15
|
+
* early (e.g. `max_iterations: 0`, `review_mode: "yolo"`) with a clear error,
|
|
16
|
+
* and to give downstream consumers a typed handle via `KarajanConfig`.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {import("valibot").InferOutput<typeof ConfigSchema>} KarajanConfig
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as v from "valibot";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Positive integer (>= 1). Used where 0 / negative would be a clear bug
|
|
25
|
+
* (max_iterations, port, most timeout knobs when they gate the loop itself).
|
|
26
|
+
*/
|
|
27
|
+
const PositiveInt = v.pipe(
|
|
28
|
+
v.number("must be a number"),
|
|
29
|
+
v.integer("must be an integer"),
|
|
30
|
+
v.minValue(1, "must be >= 1")
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Non-negative integer (>= 0). Used for retry counts where 0 is a valid
|
|
35
|
+
* "don't retry" signal.
|
|
36
|
+
*/
|
|
37
|
+
const NonNegativeInt = v.pipe(
|
|
38
|
+
v.number("must be a number"),
|
|
39
|
+
v.integer("must be an integer"),
|
|
40
|
+
v.minValue(0, "must be >= 0")
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* review_mode picklist — anything outside this set is a typo.
|
|
45
|
+
*/
|
|
46
|
+
const ReviewMode = v.picklist(
|
|
47
|
+
["paranoid", "strict", "standard", "relaxed", "custom"],
|
|
48
|
+
"review_mode must be one of: paranoid | strict | standard | relaxed | custom"
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const Methodology = v.picklist(
|
|
52
|
+
["tdd", "standard"],
|
|
53
|
+
"development.methodology must be one of: tdd | standard"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Role entry — provider + model, both nullable. Extra keys allowed.
|
|
58
|
+
*/
|
|
59
|
+
const RoleEntry = v.looseObject({
|
|
60
|
+
provider: v.optional(v.nullable(v.string())),
|
|
61
|
+
model: v.optional(v.nullable(v.string())),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const RolesMap = v.optional(v.looseObject({
|
|
65
|
+
planner: v.optional(RoleEntry),
|
|
66
|
+
coder: v.optional(RoleEntry),
|
|
67
|
+
reviewer: v.optional(RoleEntry),
|
|
68
|
+
refactorer: v.optional(RoleEntry),
|
|
69
|
+
solomon: v.optional(RoleEntry),
|
|
70
|
+
researcher: v.optional(RoleEntry),
|
|
71
|
+
tester: v.optional(RoleEntry),
|
|
72
|
+
security: v.optional(RoleEntry),
|
|
73
|
+
impeccable: v.optional(RoleEntry),
|
|
74
|
+
triage: v.optional(RoleEntry),
|
|
75
|
+
discover: v.optional(RoleEntry),
|
|
76
|
+
architect: v.optional(RoleEntry),
|
|
77
|
+
hu_reviewer: v.optional(RoleEntry),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const PipelineEntry = v.looseObject({
|
|
81
|
+
enabled: v.optional(v.boolean()),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const PipelineMap = v.optional(v.looseObject({
|
|
85
|
+
planner: v.optional(PipelineEntry),
|
|
86
|
+
refactorer: v.optional(PipelineEntry),
|
|
87
|
+
solomon: v.optional(PipelineEntry),
|
|
88
|
+
researcher: v.optional(PipelineEntry),
|
|
89
|
+
tester: v.optional(PipelineEntry),
|
|
90
|
+
security: v.optional(PipelineEntry),
|
|
91
|
+
impeccable: v.optional(PipelineEntry),
|
|
92
|
+
triage: v.optional(PipelineEntry),
|
|
93
|
+
discover: v.optional(PipelineEntry),
|
|
94
|
+
architect: v.optional(PipelineEntry),
|
|
95
|
+
hu_reviewer: v.optional(PipelineEntry),
|
|
96
|
+
auto_simplify: v.optional(v.boolean()),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const Development = v.optional(v.looseObject({
|
|
100
|
+
methodology: v.optional(Methodology),
|
|
101
|
+
require_test_changes: v.optional(v.boolean()),
|
|
102
|
+
test_file_patterns: v.optional(v.array(v.string())),
|
|
103
|
+
source_file_extensions: v.optional(v.array(v.string())),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const Git = v.optional(v.looseObject({
|
|
107
|
+
auto_commit: v.optional(v.boolean()),
|
|
108
|
+
auto_push: v.optional(v.boolean()),
|
|
109
|
+
auto_pr: v.optional(v.boolean()),
|
|
110
|
+
auto_rebase: v.optional(v.boolean()),
|
|
111
|
+
branch_prefix: v.optional(v.string()),
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
const ReviewerOptions = v.optional(v.looseObject({
|
|
115
|
+
output_format: v.optional(v.string()),
|
|
116
|
+
require_schema: v.optional(v.boolean()),
|
|
117
|
+
model: v.optional(v.nullable(v.string())),
|
|
118
|
+
deterministic: v.optional(v.boolean()),
|
|
119
|
+
retries: v.optional(NonNegativeInt, 1),
|
|
120
|
+
fallback_reviewer: v.optional(v.nullable(v.string())),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const CoderOptions = v.optional(v.looseObject({
|
|
124
|
+
model: v.optional(v.nullable(v.string())),
|
|
125
|
+
auto_approve: v.optional(v.boolean()),
|
|
126
|
+
fallback_coder: v.optional(v.nullable(v.string())),
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
const HuBoard = v.optional(v.looseObject({
|
|
130
|
+
enabled: v.optional(v.boolean()),
|
|
131
|
+
port: v.optional(v.pipe(
|
|
132
|
+
v.number("hu_board.port must be a number"),
|
|
133
|
+
v.integer(),
|
|
134
|
+
v.minValue(1, "hu_board.port must be >= 1"),
|
|
135
|
+
v.maxValue(65535, "hu_board.port must be <= 65535")
|
|
136
|
+
)),
|
|
137
|
+
auto_start: v.optional(v.boolean()),
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Budget warn threshold: 0-100 percent. Anything outside is nonsense.
|
|
142
|
+
*/
|
|
143
|
+
const Budget = v.optional(v.looseObject({
|
|
144
|
+
warn_threshold_pct: v.optional(v.pipe(
|
|
145
|
+
v.number(),
|
|
146
|
+
v.minValue(0, "budget.warn_threshold_pct must be 0-100"),
|
|
147
|
+
v.maxValue(100, "budget.warn_threshold_pct must be 0-100")
|
|
148
|
+
)),
|
|
149
|
+
currency: v.optional(v.string()),
|
|
150
|
+
exchange_rate_eur: v.optional(v.number()),
|
|
151
|
+
pricing: v.optional(v.unknown()),
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Main config schema. `looseObject` lets every consumer that reads an
|
|
156
|
+
* undeclared key keep working while the schema catches the bugs we care
|
|
157
|
+
* about.
|
|
158
|
+
*/
|
|
159
|
+
export const ConfigSchema = v.looseObject({
|
|
160
|
+
coder: v.optional(v.string()),
|
|
161
|
+
reviewer: v.optional(v.string()),
|
|
162
|
+
roles: RolesMap,
|
|
163
|
+
pipeline: PipelineMap,
|
|
164
|
+
review_mode: v.optional(ReviewMode),
|
|
165
|
+
max_iterations: v.optional(PositiveInt, 5),
|
|
166
|
+
hu_max_iterations: v.optional(PositiveInt, 3),
|
|
167
|
+
max_budget_usd: v.optional(v.nullable(v.pipe(
|
|
168
|
+
v.number(),
|
|
169
|
+
v.minValue(0, "max_budget_usd must be >= 0")
|
|
170
|
+
))),
|
|
171
|
+
review_rules: v.optional(v.string()),
|
|
172
|
+
coder_rules: v.optional(v.string()),
|
|
173
|
+
base_branch: v.optional(v.string()),
|
|
174
|
+
coder_options: CoderOptions,
|
|
175
|
+
reviewer_options: ReviewerOptions,
|
|
176
|
+
development: Development,
|
|
177
|
+
git: Git,
|
|
178
|
+
hu_board: HuBoard,
|
|
179
|
+
budget: Budget,
|
|
180
|
+
language: v.optional(v.string()),
|
|
181
|
+
hu_language: v.optional(v.string()),
|
|
182
|
+
policies: v.optional(v.record(v.string(), v.unknown())),
|
|
183
|
+
telemetry: v.optional(v.boolean()),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parse a raw merged config against the schema. Returns the normalized
|
|
188
|
+
* config or throws a Valibot ValiError. Call sites should wrap this in a
|
|
189
|
+
* try/catch that re-throws with a friendlier prefix.
|
|
190
|
+
*
|
|
191
|
+
* @param {unknown} input
|
|
192
|
+
* @returns {KarajanConfig}
|
|
193
|
+
*/
|
|
194
|
+
export function parseConfig(input) {
|
|
195
|
+
return v.parse(ConfigSchema, input);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Safe variant — returns { success, data } | { success: false, issues } so
|
|
200
|
+
* callers can decide how to report errors without catching ValiError.
|
|
201
|
+
* @param {unknown} input
|
|
202
|
+
*/
|
|
203
|
+
export function safeParseConfig(input) {
|
|
204
|
+
const result = v.safeParse(ConfigSchema, input);
|
|
205
|
+
if (result.success) return { success: true, data: result.output };
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
issues: result.issues.map(i => ({
|
|
209
|
+
path: (i.path || []).map(p => p.key).join("."),
|
|
210
|
+
message: i.message,
|
|
211
|
+
})),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Aggregate Valibot issues into a single readable string. Used by loadConfig
|
|
217
|
+
* to produce actionable error messages instead of the default ValiError dump.
|
|
218
|
+
* @param {Array<{path: string, message: string}>} issues
|
|
219
|
+
*/
|
|
220
|
+
export function formatConfigIssues(issues) {
|
|
221
|
+
return issues.map(i => (i.path ? `${i.path}: ${i.message}` : i.message)).join("\n");
|
|
222
|
+
}
|
package/src/config.js
CHANGED
|
@@ -3,6 +3,11 @@ import path from "node:path";
|
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
4
|
import { ensureDir, exists } from "./utils/fs.js";
|
|
5
5
|
import { getKarajanHome } from "./utils/paths.js";
|
|
6
|
+
import { safeParseConfig, formatConfigIssues } from "./config/schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import("./config/schema.js").KarajanConfig} KarajanConfig
|
|
10
|
+
*/
|
|
6
11
|
|
|
7
12
|
const DEFAULTS = {
|
|
8
13
|
coder: "claude",
|
|
@@ -266,6 +271,16 @@ export async function loadConfig(projectDir) {
|
|
|
266
271
|
merged.budget = mergeDeep(merged.budget || {}, { pricing: projectPricing });
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
// KJC-TSK-0318 — schema-validate the merged config so invalid values
|
|
275
|
+
// (review_mode typos, max_iterations=0, out-of-range ports) surface at
|
|
276
|
+
// load time with a clear error instead of crashing deep inside the
|
|
277
|
+
// orchestrator. safeParse + explicit error prefix keeps the message
|
|
278
|
+
// actionable.
|
|
279
|
+
const validation = safeParseConfig(merged);
|
|
280
|
+
if (!validation.success) {
|
|
281
|
+
throw new Error(`Invalid Karajan config:\n${formatConfigIssues(validation.issues)}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
269
284
|
return { config: merged, path: configPath, exists: globalExists, hasProjectConfig: !!projectConfig };
|
|
270
285
|
}
|
|
271
286
|
|
|
@@ -303,10 +318,13 @@ const PIPELINE_ENABLE_FLAGS = [
|
|
|
303
318
|
|
|
304
319
|
const AUTO_SIMPLIFY_FLAG = "autoSimplify";
|
|
305
320
|
|
|
306
|
-
// Scalar flags: [flagName, setter] — truthy check
|
|
321
|
+
// Scalar flags: [flagName, setter] — truthy check.
|
|
322
|
+
// Note: max_iterations was previously here but `0` was silently ignored
|
|
323
|
+
// (truthy check skipped it). It now lives in UNDEF_CHECK_FLAGS so
|
|
324
|
+
// `--max-iterations 0` reaches the schema validator and errors cleanly
|
|
325
|
+
// instead of falling through to the default (KJC-TSK-0318).
|
|
307
326
|
const SCALAR_FLAGS = [
|
|
308
327
|
["mode", (out, v) => { out.review_mode = v; }],
|
|
309
|
-
["maxIterations", (out, v) => { out.max_iterations = Number(v); }],
|
|
310
328
|
["maxIterationMinutes", (out, v) => { out.session.max_iteration_minutes = Number(v); }],
|
|
311
329
|
["maxTotalMinutes", (out, v) => { out.session.max_total_minutes = Number(v); }],
|
|
312
330
|
["checkpointInterval", (out, v) => { out.session.checkpoint_interval_minutes = Number(v); }],
|
|
@@ -317,8 +335,11 @@ const SCALAR_FLAGS = [
|
|
|
317
335
|
["branchPrefix", (out, v) => { out.git.branch_prefix = String(v); }]
|
|
318
336
|
];
|
|
319
337
|
|
|
320
|
-
// Boolean/undefined-check flags: [flagName, setter] — !== undefined check
|
|
338
|
+
// Boolean/undefined-check flags: [flagName, setter] — !== undefined check.
|
|
339
|
+
// These preserve falsy values (false, 0) so --no-rebase correctly sets
|
|
340
|
+
// auto_rebase=false and --reviewer-retries 0 correctly disables retries.
|
|
321
341
|
const UNDEF_CHECK_FLAGS = [
|
|
342
|
+
["maxIterations", (out, v) => { out.max_iterations = Number(v); }],
|
|
322
343
|
["reviewerRetries", (out, v) => { out.reviewer_options.retries = Number(v); }],
|
|
323
344
|
["autoCommit", (out, v) => { out.git.auto_commit = Boolean(v); }],
|
|
324
345
|
["autoPush", (out, v) => { out.git.auto_push = Boolean(v); }],
|
|
@@ -431,6 +452,14 @@ export function applyRunOverrides(config, flags) {
|
|
|
431
452
|
applyMiscOverrides(out, flags);
|
|
432
453
|
applyOutputModeOverrides(out, flags);
|
|
433
454
|
|
|
455
|
+
// KJC-TSK-0318 — re-run schema validation after overrides so CLI flags
|
|
456
|
+
// that would produce an invalid config (e.g. --max-iterations 0) fail
|
|
457
|
+
// fast with the same clear message loadConfig would have produced.
|
|
458
|
+
const validation = safeParseConfig(out);
|
|
459
|
+
if (!validation.success) {
|
|
460
|
+
throw new Error(`Invalid Karajan config after CLI overrides:\n${formatConfigIssues(validation.issues)}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
434
463
|
return out;
|
|
435
464
|
}
|
|
436
465
|
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy resolution: map task type + config toggles to concrete pipeline
|
|
3
|
+
* flags (TDD, Sonar, reviewer retries, tests required, coder required, ...).
|
|
4
|
+
*
|
|
5
|
+
* @typedef {import("../types/policy.js").ResolvedPolicies} ResolvedPolicies
|
|
6
|
+
* @typedef {import("../types/config.js").KarajanConfig} KarajanConfig
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
export const VALID_TASK_TYPES = new Set(["sw", "infra", "doc", "add-tests", "refactor", "audit", "analysis", "no-code"]);
|
|
2
10
|
|
|
3
11
|
export const DEFAULT_POLICIES = {
|