karajan-code 1.52.0 → 1.53.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/package.json +1 -1
- package/src/cli.js +1 -0
- package/src/commands/plan.js +24 -1
- package/src/mcp/handlers/direct-handlers.js +89 -2
- package/src/mcp/tools.js +1 -0
- package/src/orchestrator.js +38 -0
- package/src/plan/plan-store.js +116 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -86,6 +86,7 @@ program
|
|
|
86
86
|
.option("--enable-triage")
|
|
87
87
|
.option("--enable-discover")
|
|
88
88
|
.option("--enable-architect")
|
|
89
|
+
.option("--plan <planId>", "Plan ID from kj plan. Loads persisted plan context and skips researcher/architect/planner.")
|
|
89
90
|
.option("--enable-hu-reviewer")
|
|
90
91
|
.option("--hu-file <path>", "YAML file with HU stories to certify before coding")
|
|
91
92
|
.option("--enable-serena")
|
package/src/commands/plan.js
CHANGED
|
@@ -60,8 +60,26 @@ export async function planCommand({ task, config, logger, json, context }) {
|
|
|
60
60
|
|
|
61
61
|
const parsed = parseMaybeJsonString(result.output);
|
|
62
62
|
|
|
63
|
+
// Persist plan
|
|
64
|
+
let planId = null;
|
|
65
|
+
try {
|
|
66
|
+
const { savePlan } = await import("../plan/plan-store.js");
|
|
67
|
+
const projectDir = config.projectDir || process.cwd();
|
|
68
|
+
planId = await savePlan(projectDir, {
|
|
69
|
+
task,
|
|
70
|
+
researchContext: null,
|
|
71
|
+
architectContext: null,
|
|
72
|
+
plan: parsed || result.output,
|
|
73
|
+
raw: result.output
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.warn(`Plan persistence failed: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
if (json) {
|
|
64
|
-
|
|
80
|
+
const jsonOutput = parsed || result.output;
|
|
81
|
+
const outputObj = typeof jsonOutput === "object" ? { ...jsonOutput, planId } : { plan: jsonOutput, planId };
|
|
82
|
+
console.log(JSON.stringify(outputObj, null, 2));
|
|
65
83
|
return;
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -70,4 +88,9 @@ export async function planCommand({ task, config, logger, json, context }) {
|
|
|
70
88
|
} else {
|
|
71
89
|
console.log(result.output);
|
|
72
90
|
}
|
|
91
|
+
|
|
92
|
+
if (planId) {
|
|
93
|
+
console.log(`\nPlan saved: ${planId}`);
|
|
94
|
+
console.log(`Use it with: kj run --plan ${planId} "<task>"`);
|
|
95
|
+
}
|
|
73
96
|
}
|
|
@@ -70,6 +70,76 @@ export async function handlePlanDirect(a, server, extra) {
|
|
|
70
70
|
);
|
|
71
71
|
const emitter = buildDirectEmitter(server, runLog, extra);
|
|
72
72
|
const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
|
|
73
|
+
|
|
74
|
+
// --- Phase 1: Researcher (codebase context) ---
|
|
75
|
+
let researchContext = null;
|
|
76
|
+
try {
|
|
77
|
+
const { ResearcherRole } = await import("../../roles/researcher-role.js");
|
|
78
|
+
const researcherRole = resolveRole(config, "researcher");
|
|
79
|
+
const researcherOnOutput = ({ stream, line }) => {
|
|
80
|
+
emitter.emit("progress", { type: "agent:output", stage: "researcher", message: line, detail: { stream, agent: researcherRole.provider } });
|
|
81
|
+
};
|
|
82
|
+
const researcherStall = createStallDetector({
|
|
83
|
+
onOutput: researcherOnOutput, emitter, eventBase, stage: "researcher", provider: researcherRole.provider
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
sendTrackerLog(server, "researcher", "running", researcherRole.provider);
|
|
87
|
+
runLog.logText(`[researcher] agent launched for plan context...`);
|
|
88
|
+
const researcher = new ResearcherRole({ config, logger, emitter });
|
|
89
|
+
await researcher.init({ task: a.task });
|
|
90
|
+
let researchOutput;
|
|
91
|
+
try {
|
|
92
|
+
researchOutput = await researcher.run({ task: a.task, onOutput: researcherStall.onOutput });
|
|
93
|
+
} finally {
|
|
94
|
+
researcherStall.stop();
|
|
95
|
+
}
|
|
96
|
+
if (researchOutput.ok) {
|
|
97
|
+
researchContext = researchOutput.result;
|
|
98
|
+
sendTrackerLog(server, "researcher", "done");
|
|
99
|
+
runLog.logText(`[researcher] completed — context available`);
|
|
100
|
+
} else {
|
|
101
|
+
sendTrackerLog(server, "researcher", "failed");
|
|
102
|
+
runLog.logText(`[researcher] failed: ${researchOutput.summary || "unknown"}`);
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logger.warn(`Researcher phase skipped: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Phase 2: Architect (solution design) ---
|
|
109
|
+
let architectContext = null;
|
|
110
|
+
try {
|
|
111
|
+
const { ArchitectRole } = await import("../../roles/architect-role.js");
|
|
112
|
+
const architectRole = resolveRole(config, "architect");
|
|
113
|
+
const architectOnOutput = ({ stream, line }) => {
|
|
114
|
+
emitter.emit("progress", { type: "agent:output", stage: "architect", message: line, detail: { stream, agent: architectRole.provider } });
|
|
115
|
+
};
|
|
116
|
+
const architectStall = createStallDetector({
|
|
117
|
+
onOutput: architectOnOutput, emitter, eventBase, stage: "architect", provider: architectRole.provider
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
sendTrackerLog(server, "architect", "running", architectRole.provider);
|
|
121
|
+
runLog.logText(`[architect] agent launched for plan context...`);
|
|
122
|
+
const architect = new ArchitectRole({ config, logger, emitter });
|
|
123
|
+
await architect.init({ task: a.task });
|
|
124
|
+
let architectOutput;
|
|
125
|
+
try {
|
|
126
|
+
architectOutput = await architect.run({ task: a.task, researchContext, onOutput: architectStall.onOutput });
|
|
127
|
+
} finally {
|
|
128
|
+
architectStall.stop();
|
|
129
|
+
}
|
|
130
|
+
if (architectOutput.ok) {
|
|
131
|
+
architectContext = architectOutput.result;
|
|
132
|
+
sendTrackerLog(server, "architect", "done");
|
|
133
|
+
runLog.logText(`[architect] completed — context available`);
|
|
134
|
+
} else {
|
|
135
|
+
sendTrackerLog(server, "architect", "failed");
|
|
136
|
+
runLog.logText(`[architect] failed: ${architectOutput.summary || "unknown"}`);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.warn(`Architect phase skipped: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Phase 3: Planner (with research + architecture context) ---
|
|
73
143
|
const onOutput = ({ stream, line }) => {
|
|
74
144
|
emitter.emit("progress", { type: "agent:output", stage: "planner", message: line, detail: { stream, agent: plannerRole.provider } });
|
|
75
145
|
};
|
|
@@ -78,7 +148,7 @@ export async function handlePlanDirect(a, server, extra) {
|
|
|
78
148
|
});
|
|
79
149
|
|
|
80
150
|
const planner = createAgent(plannerRole.provider, config, logger);
|
|
81
|
-
const prompt = buildPlannerPrompt({ task: a.task, context: a.context });
|
|
151
|
+
const prompt = buildPlannerPrompt({ task: a.task, context: a.context, architectContext });
|
|
82
152
|
sendTrackerLog(server, "planner", "running", plannerRole.provider);
|
|
83
153
|
runLog.logText(`[planner] agent launched, waiting for response...`);
|
|
84
154
|
let result;
|
|
@@ -111,7 +181,24 @@ export async function handlePlanDirect(a, server, extra) {
|
|
|
111
181
|
|
|
112
182
|
sendTrackerLog(server, "planner", "done");
|
|
113
183
|
const parsed = parseMaybeJsonString(result.output);
|
|
114
|
-
|
|
184
|
+
|
|
185
|
+
// --- Persist plan ---
|
|
186
|
+
let planId = null;
|
|
187
|
+
try {
|
|
188
|
+
const { savePlan } = await import("../../plan/plan-store.js");
|
|
189
|
+
planId = await savePlan(projectDir, {
|
|
190
|
+
task: a.task,
|
|
191
|
+
researchContext,
|
|
192
|
+
architectContext,
|
|
193
|
+
plan: parsed || result.output,
|
|
194
|
+
raw: result.output
|
|
195
|
+
});
|
|
196
|
+
runLog.logText(`[kj_plan] plan persisted — planId=${planId}`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
logger.warn(`Plan persistence failed (non-blocking): ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { ok: true, plan: parsed || result.output, raw: result.output, planId, researchContext, architectContext };
|
|
115
202
|
}
|
|
116
203
|
|
|
117
204
|
export async function handleCodeDirect(a, server, extra) {
|
package/src/mcp/tools.js
CHANGED
|
@@ -53,6 +53,7 @@ export const tools = [
|
|
|
53
53
|
required: ["task"],
|
|
54
54
|
properties: {
|
|
55
55
|
task: { type: "string", description: "Task description for the coder (can include a Planning Game card ID like KJC-TSK-0042)" },
|
|
56
|
+
plan: { type: "string", description: "Plan ID from kj_plan. Loads persisted plan context and skips researcher/architect/planner stages." },
|
|
56
57
|
projectDir: { type: "string", description: "Absolute path to the project directory. Required when KJ MCP server runs from a different directory than the target project." },
|
|
57
58
|
pgTask: { type: "string", description: "Planning Game card ID (e.g., KJC-TSK-0042). If provided, fetches full card details as task context and updates card status on completion." },
|
|
58
59
|
pgProject: { type: "string", description: "Planning Game project ID (e.g., 'Karajan Code'). Required when pgTask is used." },
|
package/src/orchestrator.js
CHANGED
|
@@ -984,6 +984,44 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
984
984
|
pipelineFlags.securityEnabled = false;
|
|
985
985
|
}
|
|
986
986
|
|
|
987
|
+
// --- Plan injection: skip researcher/architect/planner if a persisted plan is loaded ---
|
|
988
|
+
if (flags.plan) {
|
|
989
|
+
try {
|
|
990
|
+
const { loadPlan } = await import("./plan/plan-store.js");
|
|
991
|
+
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
992
|
+
const loadedPlan = await loadPlan(projectDir, flags.plan);
|
|
993
|
+
if (loadedPlan) {
|
|
994
|
+
logger.info(`Loaded persisted plan: ${flags.plan}`);
|
|
995
|
+
emitProgress(emitter, makeEvent("plan:loaded", { ...eventBase, stage: "plan" }, {
|
|
996
|
+
message: `Plan loaded from kj_plan: ${flags.plan}`,
|
|
997
|
+
detail: { planId: flags.plan, task: loadedPlan.task, createdAt: loadedPlan.createdAt }
|
|
998
|
+
}));
|
|
999
|
+
stageResults.researcher = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
|
|
1000
|
+
stageResults.architect = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
|
|
1001
|
+
stageResults.planner = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
|
|
1002
|
+
// Inject contexts into session for downstream stages
|
|
1003
|
+
session.research_context = loadedPlan.researchContext || null;
|
|
1004
|
+
session.architect_context = loadedPlan.architectContext || null;
|
|
1005
|
+
session.loaded_plan = loadedPlan.plan || null;
|
|
1006
|
+
await saveSession(session);
|
|
1007
|
+
|
|
1008
|
+
// Build the planned task from the plan steps
|
|
1009
|
+
const plan = loadedPlan.plan;
|
|
1010
|
+
let plannedTask = task;
|
|
1011
|
+
if (plan && typeof plan === "object" && plan.steps) {
|
|
1012
|
+
const stepList = plan.steps.map((s, idx) => `${idx + 1}. ${s.description || s}`).join("\n");
|
|
1013
|
+
plannedTask = `${task}\n\n## Implementation Plan\n${plan.approach || ""}\n\n## Steps\n${stepList}`;
|
|
1014
|
+
} else if (typeof plan === "string") {
|
|
1015
|
+
plannedTask = `${task}\n\n## Implementation Plan\n${plan}`;
|
|
1016
|
+
}
|
|
1017
|
+
return { plannedTask, updatedConfig };
|
|
1018
|
+
}
|
|
1019
|
+
logger.warn(`Plan ${flags.plan} not found — falling back to normal pipeline`);
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
logger.warn(`Plan loading failed: ${err.message} — falling back to normal pipeline`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
987
1025
|
// --- Researcher → Planner ---
|
|
988
1026
|
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
|
|
989
1027
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan persistence — save, load, list plans generated by kj_plan.
|
|
3
|
+
* Plans are stored at ~/.kj/plans/<projectSlug>/<planId>.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
|
|
10
|
+
function getKjHome() {
|
|
11
|
+
return process.env.KJ_HOME || path.join(os.homedir(), ".kj");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function projectSlug(projectDir) {
|
|
15
|
+
return projectDir
|
|
16
|
+
.replace(/^\//, "")
|
|
17
|
+
.replace(/[/\\]/g, "_")
|
|
18
|
+
.replace(/[^a-zA-Z0-9_.-]/g, "")
|
|
19
|
+
.slice(0, 120);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function plansDir(projectDir) {
|
|
23
|
+
return path.join(getKjHome(), "plans", projectSlug(projectDir));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generatePlanId() {
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const ts = now.toISOString().replace(/[-:T]/g, "").replace(/\.\d+Z$/, "");
|
|
29
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
30
|
+
return `plan-${ts}-${rand}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save a plan result to disk.
|
|
35
|
+
* @param {string} projectDir - Absolute path to the project
|
|
36
|
+
* @param {object} planResult - { task, researchContext, architectContext, plan, raw }
|
|
37
|
+
* @returns {Promise<string>} planId
|
|
38
|
+
*/
|
|
39
|
+
export async function savePlan(projectDir, planResult) {
|
|
40
|
+
const dir = plansDir(projectDir);
|
|
41
|
+
await fs.mkdir(dir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
const planId = generatePlanId();
|
|
44
|
+
const record = {
|
|
45
|
+
planId,
|
|
46
|
+
task: planResult.task || null,
|
|
47
|
+
researchContext: planResult.researchContext || null,
|
|
48
|
+
architectContext: planResult.architectContext || null,
|
|
49
|
+
plan: planResult.plan || null,
|
|
50
|
+
raw: planResult.raw || null,
|
|
51
|
+
createdAt: new Date().toISOString()
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const filePath = path.join(dir, `${planId}.json`);
|
|
55
|
+
await fs.writeFile(filePath, JSON.stringify(record, null, 2), "utf8");
|
|
56
|
+
return planId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load a saved plan by ID.
|
|
61
|
+
* @param {string} projectDir
|
|
62
|
+
* @param {string} planId
|
|
63
|
+
* @returns {Promise<object|null>} { planId, task, researchContext, architectContext, plan, createdAt } or null
|
|
64
|
+
*/
|
|
65
|
+
export async function loadPlan(projectDir, planId) {
|
|
66
|
+
const filePath = path.join(plansDir(projectDir), `${planId}.json`);
|
|
67
|
+
try {
|
|
68
|
+
const data = await fs.readFile(filePath, "utf8");
|
|
69
|
+
return JSON.parse(data);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List all plans for a project, sorted by creation time (newest first).
|
|
77
|
+
* @param {string} projectDir
|
|
78
|
+
* @returns {Promise<Array<{planId: string, task: string, createdAt: string}>>}
|
|
79
|
+
*/
|
|
80
|
+
export async function listPlans(projectDir) {
|
|
81
|
+
const dir = plansDir(projectDir);
|
|
82
|
+
let files;
|
|
83
|
+
try {
|
|
84
|
+
files = await fs.readdir(dir);
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const plans = [];
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
if (!file.endsWith(".json")) continue;
|
|
92
|
+
try {
|
|
93
|
+
const data = await fs.readFile(path.join(dir, file), "utf8");
|
|
94
|
+
const record = JSON.parse(data);
|
|
95
|
+
plans.push({
|
|
96
|
+
planId: record.planId,
|
|
97
|
+
task: record.task,
|
|
98
|
+
createdAt: record.createdAt
|
|
99
|
+
});
|
|
100
|
+
} catch { /* skip corrupt files */ }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
plans.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
104
|
+
return plans;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the most recent plan for a project.
|
|
109
|
+
* @param {string} projectDir
|
|
110
|
+
* @returns {Promise<object|null>}
|
|
111
|
+
*/
|
|
112
|
+
export async function getLatestPlan(projectDir) {
|
|
113
|
+
const all = await listPlans(projectDir);
|
|
114
|
+
if (all.length === 0) return null;
|
|
115
|
+
return loadPlan(projectDir, all[0].planId);
|
|
116
|
+
}
|