karajan-code 2.1.0 → 2.2.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/commands/board.js +38 -3
- package/src/hu/auto-generator.js +29 -0
- package/src/orchestrator.js +28 -2
package/package.json
CHANGED
package/src/commands/board.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import net from "node:net";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { getKarajanHome } from "../utils/paths.js";
|
|
5
6
|
|
|
@@ -24,10 +25,44 @@ function isProcessAlive(pid) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Check if a TCP port is available on localhost.
|
|
30
|
+
* Returns true when we can successfully bind (port is free).
|
|
31
|
+
*/
|
|
32
|
+
function isPortAvailable(port) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const server = net.createServer();
|
|
35
|
+
server.once("error", () => resolve(false));
|
|
36
|
+
server.once("listening", () => {
|
|
37
|
+
server.close(() => resolve(true));
|
|
38
|
+
});
|
|
39
|
+
server.listen(port, "127.0.0.1");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Starting from `desiredPort`, return the first free port within `maxTries`.
|
|
45
|
+
* Returns null if none available.
|
|
46
|
+
*/
|
|
47
|
+
async function findAvailablePort(desiredPort, maxTries = 10) {
|
|
48
|
+
for (let i = 0; i < maxTries; i++) {
|
|
49
|
+
const port = desiredPort + i;
|
|
50
|
+
if (await isPortAvailable(port)) return port;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function startBoard(desiredPort = 4000) {
|
|
28
56
|
const existingPid = readPid();
|
|
29
57
|
if (existingPid && isProcessAlive(existingPid)) {
|
|
30
|
-
|
|
58
|
+
// Trust the saved PID — port info comes from the PID file if present
|
|
59
|
+
return { ok: true, alreadyRunning: true, pid: existingPid, url: `http://localhost:${desiredPort}` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find a free port starting from desiredPort
|
|
63
|
+
const port = await findAvailablePort(desiredPort, 10);
|
|
64
|
+
if (port === null) {
|
|
65
|
+
throw new Error(`No free port in range ${desiredPort}-${desiredPort + 9}`);
|
|
31
66
|
}
|
|
32
67
|
|
|
33
68
|
const serverPath = path.join(BOARD_DIR, "src/server.js");
|
|
@@ -50,7 +85,7 @@ export async function startBoard(port = 4000) {
|
|
|
50
85
|
throw new Error("Failed to spawn HU Board server");
|
|
51
86
|
}
|
|
52
87
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
53
|
-
return { ok: true, alreadyRunning: false, pid: child.pid, url: `http://localhost:${port}
|
|
88
|
+
return { ok: true, alreadyRunning: false, pid: child.pid, url: `http://localhost:${port}`, port };
|
|
54
89
|
}
|
|
55
90
|
|
|
56
91
|
export async function stopBoard() {
|
package/src/hu/auto-generator.js
CHANGED
|
@@ -7,6 +7,34 @@
|
|
|
7
7
|
* and a dependency graph (setup blocks everything; remaining linear by default).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Derive a human-readable project name from a task prompt.
|
|
12
|
+
* Strips common action verbs + stopwords, takes up to 6 meaningful words,
|
|
13
|
+
* and title-cases the result. Max 60 chars.
|
|
14
|
+
*/
|
|
15
|
+
export function deriveProjectName(originalTask) {
|
|
16
|
+
if (!originalTask || typeof originalTask !== "string") return "Untitled Project";
|
|
17
|
+
const STOPWORDS = new Set([
|
|
18
|
+
"a", "an", "the", "and", "or", "with", "for", "to", "of", "in", "on",
|
|
19
|
+
"build", "create", "implement", "make", "develop", "add", "set", "up",
|
|
20
|
+
"setup", "write", "code", "new", "complete", "that", "from", "scratch",
|
|
21
|
+
"application", "app", "tool", "system", "project", "using", "use"
|
|
22
|
+
]);
|
|
23
|
+
const words = originalTask
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, " ")
|
|
26
|
+
.split(/\s+/)
|
|
27
|
+
.filter(w => w && !STOPWORDS.has(w));
|
|
28
|
+
const meaningful = words.slice(0, 6);
|
|
29
|
+
if (meaningful.length === 0) {
|
|
30
|
+
return originalTask.slice(0, 60).trim() || "Untitled Project";
|
|
31
|
+
}
|
|
32
|
+
const titled = meaningful
|
|
33
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
34
|
+
.join(" ");
|
|
35
|
+
return titled.length > 60 ? titled.slice(0, 57) + "..." : titled;
|
|
36
|
+
}
|
|
37
|
+
|
|
10
38
|
/**
|
|
11
39
|
* Classify a subtask into a Karajan task_type.
|
|
12
40
|
* Maps free-text subtask descriptions to {infra|sw|add-tests|doc|refactor|nocode}.
|
|
@@ -157,6 +185,7 @@ export function generateHuBatch({
|
|
|
157
185
|
stories,
|
|
158
186
|
total: stories.length,
|
|
159
187
|
certified: stories.length,
|
|
188
|
+
projectName: deriveProjectName(originalTask),
|
|
160
189
|
generated: true,
|
|
161
190
|
source: { triage_subtasks: subtasks.length, researcher: Boolean(researcherContext), architect: Boolean(architectContext) }
|
|
162
191
|
};
|
package/src/orchestrator.js
CHANGED
|
@@ -771,10 +771,12 @@ async function maybeGenerateAutoHuBatch({ flags, stageResults, task, plannedTask
|
|
|
771
771
|
const fs = await import("node:fs/promises");
|
|
772
772
|
const path = await import("node:path");
|
|
773
773
|
const { getKarajanHome } = await import("./utils/paths.js");
|
|
774
|
-
const huDir = path.join(getKarajanHome(), "hu", batchSessionId);
|
|
774
|
+
const huDir = path.join(getKarajanHome(), "hu-stories", batchSessionId);
|
|
775
775
|
await fs.mkdir(huDir, { recursive: true });
|
|
776
776
|
const persistBatch = {
|
|
777
777
|
session_id: batchSessionId,
|
|
778
|
+
project_id: batchSessionId,
|
|
779
|
+
project_name: batch.projectName,
|
|
778
780
|
created_at: new Date().toISOString(),
|
|
779
781
|
updated_at: new Date().toISOString(),
|
|
780
782
|
stories: batch.stories
|
|
@@ -798,8 +800,32 @@ async function maybeGenerateAutoHuBatch({ flags, stageResults, task, plannedTask
|
|
|
798
800
|
logger.info(`Auto-HU: generated ${batch.total} stories (${batch.source.triage_subtasks} subtasks${isNewProject ? ", new project" : ""}${stackHints.length ? `, stack: ${stackHints.join(",")}` : ""})`);
|
|
799
801
|
emitProgress(emitter, makeEvent("hu:auto-generated", { ...eventBase, stage: "hu-auto-gen" }, {
|
|
800
802
|
message: `Auto-generated ${batch.total} HU(s) from triage decomposition`,
|
|
801
|
-
detail: { total: batch.total, subtasks: batch.source.triage_subtasks, isNewProject, stackHints }
|
|
803
|
+
detail: { total: batch.total, subtasks: batch.source.triage_subtasks, isNewProject, stackHints, projectName: batch.projectName }
|
|
802
804
|
}));
|
|
805
|
+
|
|
806
|
+
// Auto-start the board so the user can see the generated HUs.
|
|
807
|
+
// Always fires when auto-HU runs, independent of hu_board.auto_start flag.
|
|
808
|
+
try {
|
|
809
|
+
const { startBoard } = await import("./commands/board.js");
|
|
810
|
+
const desiredPort = session.config_snapshot?.hu_board?.port ?? 4000;
|
|
811
|
+
const boardResult = await startBoard(desiredPort);
|
|
812
|
+
const url = boardResult.url;
|
|
813
|
+
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
814
|
+
// Highlighted URL box — visible regardless of log level
|
|
815
|
+
const title = batch.projectName || "Auto-generated HUs";
|
|
816
|
+
const box = [
|
|
817
|
+
"",
|
|
818
|
+
"\u001b[36m\u256d\u2500 HU Board \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e",
|
|
819
|
+
`\u001b[36m\u2502\u001b[0m \u001b[1m${status}\u001b[0m \u001b[4m${url}\u001b[0m`,
|
|
820
|
+
`\u001b[36m\u2502\u001b[0m Project: \u001b[1m${title}\u001b[0m`,
|
|
821
|
+
"\u001b[36m\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u001b[0m",
|
|
822
|
+
""
|
|
823
|
+
].join("\n");
|
|
824
|
+
console.log(box);
|
|
825
|
+
logger.info(`HU Board ${status} at ${url} (project: ${title})`);
|
|
826
|
+
} catch (err) {
|
|
827
|
+
logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
|
|
828
|
+
}
|
|
803
829
|
}
|
|
804
830
|
|
|
805
831
|
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
|