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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -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
- export async function startBoard(port = 4000) {
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
- return { ok: true, alreadyRunning: true, pid: existingPid, url: `http://localhost:${port}` };
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() {
@@ -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
  };
@@ -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 }) {