karajan-code 1.54.0 → 1.55.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": "1.54.0",
3
+ "version": "1.55.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",
package/src/bootstrap.js CHANGED
@@ -22,6 +22,7 @@ import { checkBinary } from "./utils/agent-detect.js";
22
22
  import { exists } from "./utils/fs.js";
23
23
  import { getConfigPath, resolveRole } from "./config.js";
24
24
  import { isSonarReachable, sonarUp } from "./sonar/manager.js";
25
+ import { withDocLink } from "./utils/doc-links.js";
25
26
 
26
27
  const BOOTSTRAP_VERSION = 1;
27
28
  const BOOTSTRAP_TTL_HOURS = 24;
@@ -185,14 +186,14 @@ function formatBootstrapFailure(failures) {
185
186
  const lines = failures.map(f =>
186
187
  ` FAIL ${f.name}: ${f.detail}\n Fix: ${f.fix}`
187
188
  );
188
- return [
189
+ return withDocLink([
189
190
  "BOOTSTRAP FAILED — Environment not ready for Karajan Code.\n",
190
191
  "The following prerequisite(s) are not met:\n",
191
192
  ...lines,
192
193
  "",
193
194
  "Run 'kj_doctor' for a complete environment diagnostic.",
194
195
  "Do NOT work around these issues — fix them properly."
195
- ].join("\n");
196
+ ].join("\n"), "bootstrap_failed");
196
197
  }
197
198
 
198
199
  // ── Public API ───────────────────────────────────────────────────────
package/src/cli.js CHANGED
@@ -24,6 +24,7 @@ import { researcherCommand } from "./commands/researcher.js";
24
24
  import { architectCommand } from "./commands/architect.js";
25
25
  import { auditCommand } from "./commands/audit.js";
26
26
  import { boardCommand } from "./commands/board.js";
27
+ import { undoCommand } from "./commands/undo.js";
27
28
 
28
29
  import { printUpdateNotice } from "./utils/update-check.js";
29
30
 
@@ -324,6 +325,17 @@ program
324
325
  });
325
326
  });
326
327
 
328
+ program
329
+ .command("undo")
330
+ .description("Revert last pipeline run")
331
+ .option("--hard", "Discard all changes (default: soft reset, keeps changes staged)")
332
+ .action(async (flags) => {
333
+ await withConfig("undo", flags, async ({ logger }) => {
334
+ const result = await undoCommand({ hard: !!flags.hard, logger });
335
+ if (!result.ok) process.exit(1);
336
+ });
337
+ });
338
+
327
339
  const sonar = program.command("sonar").description("Manage SonarQube container");
328
340
  sonar.command("status").action(async () => sonarCommand({ action: "status" }));
329
341
  sonar.command("start").action(async () => sonarCommand({ action: "start" }));
@@ -11,6 +11,7 @@ import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
11
11
  import { ensureGitRepo } from "../utils/git.js";
12
12
  import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
13
13
  import { getInstallCommand } from "../utils/os-detect.js";
14
+ import { withDocLink } from "../utils/doc-links.js";
14
15
 
15
16
  function getPackageVersion() {
16
17
  const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
@@ -36,7 +37,7 @@ async function checkConfigFile() {
36
37
  label: "Config file",
37
38
  ok: configExists,
38
39
  detail: configExists ? configPath : "Not found",
39
- fix: configExists ? null : "Run 'kj init' to create the config file."
40
+ fix: configExists ? null : withDocLink("Run 'kj init' to create the config file.", "config_missing")
40
41
  };
41
42
  }
42
43
 
@@ -63,7 +64,7 @@ async function checkDocker() {
63
64
  label: "Docker",
64
65
  ok: docker.ok,
65
66
  detail: docker.ok ? docker.version : "Not found",
66
- fix: docker.ok ? null : "Install Docker: https://docs.docker.com/get-docker/"
67
+ fix: docker.ok ? null : withDocLink("Install Docker: https://docs.docker.com/get-docker/", "sonar_docker")
67
68
  };
68
69
  }
69
70
 
@@ -91,7 +92,7 @@ async function checkSonarQube(config) {
91
92
  detail: sonarDetail(config, sonarOk, sonarHost),
92
93
  fix: isOkOrDisabled
93
94
  ? null
94
- : "Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip."
95
+ : withDocLink("Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip.", "sonar_docker")
95
96
  };
96
97
  }
97
98
 
@@ -104,7 +105,7 @@ async function checkAgentCLIs() {
104
105
  label: `Agent: ${agent.name}`,
105
106
  ok: result.ok,
106
107
  detail: result.ok ? `${result.version} (${result.path})` : "Not found",
107
- fix: result.ok ? null : `Install: ${agent.install}`
108
+ fix: result.ok ? null : withDocLink(`Install: ${agent.install}`, "agent_not_found")
108
109
  });
109
110
  }
110
111
  return checks;
@@ -220,7 +221,7 @@ async function checkRtk() {
220
221
  label: "RTK (Rust Token Killer)",
221
222
  ok: false,
222
223
  detail: "Not found — 60-90% token savings available",
223
- fix: `Install: ${installCmd}`
224
+ fix: withDocLink(`Install: ${installCmd}`, "rtk_install")
224
225
  };
225
226
  }
226
227
 
@@ -0,0 +1,62 @@
1
+ import { loadMostRecentSession } from "../session-store.js";
2
+ import { runCommand } from "../utils/process.js";
3
+
4
+ /**
5
+ * Revert the last pipeline run by resetting to the pre-pipeline commit.
6
+ * Default: soft reset (keeps changes staged). With --hard: discards all changes.
7
+ */
8
+ export async function undoCommand({ hard = false, logger } = {}) {
9
+ const session = await loadMostRecentSession();
10
+ if (!session) {
11
+ const msg = "No session to undo. No sessions found in the sessions directory.";
12
+ if (logger) logger.error(msg);
13
+ return { ok: false, error: msg };
14
+ }
15
+
16
+ const sha = session.session_start_sha;
17
+ if (!sha) {
18
+ const msg = `Session ${session.id} has no session_start_sha. Cannot determine the pre-pipeline commit.`;
19
+ if (logger) logger.error(msg);
20
+ return { ok: false, error: msg };
21
+ }
22
+
23
+ // Verify the SHA exists in git history
24
+ const verifyRes = await runCommand("git", ["cat-file", "-t", sha]);
25
+ if (verifyRes.exitCode !== 0 || verifyRes.stdout.trim() !== "commit") {
26
+ const msg = `Commit ${sha} not found in git history. It may have been garbage-collected or belong to a different repository.`;
27
+ if (logger) logger.error(msg);
28
+ return { ok: false, error: msg };
29
+ }
30
+
31
+ // Check there are commits after the SHA (HEAD is ahead)
32
+ const logRes = await runCommand("git", ["log", "--oneline", `${sha}..HEAD`]);
33
+ if (logRes.exitCode !== 0 || !logRes.stdout.trim()) {
34
+ const msg = `No commits to undo. HEAD is already at or before ${sha.slice(0, 8)}.`;
35
+ if (logger) logger.warn(msg);
36
+ return { ok: false, error: msg };
37
+ }
38
+
39
+ const mode = hard ? "--hard" : "--soft";
40
+ const resetRes = await runCommand("git", ["reset", mode, sha]);
41
+ if (resetRes.exitCode !== 0) {
42
+ const msg = `git reset ${mode} ${sha} failed: ${resetRes.stderr || resetRes.stdout}`;
43
+ if (logger) logger.error(msg);
44
+ return { ok: false, error: msg };
45
+ }
46
+
47
+ const shortSha = sha.slice(0, 8);
48
+ const message = hard
49
+ ? `Reverted to pre-pipeline state (commit ${shortSha}). All changes discarded.`
50
+ : `Reverted to pre-pipeline state (commit ${shortSha}). Changes are staged. Use \`git diff --cached\` to review.`;
51
+
52
+ if (logger) logger.info(message);
53
+ console.log(message);
54
+
55
+ return {
56
+ ok: true,
57
+ message,
58
+ sessionId: session.id,
59
+ sha,
60
+ mode: hard ? "hard" : "soft"
61
+ };
62
+ }
@@ -163,6 +163,18 @@ export async function handleBoard(a) {
163
163
  }
164
164
  }
165
165
 
166
+ export async function handleUndo(a, server) {
167
+ const projectDir = await resolveProjectDir(server, a.projectDir);
168
+ const origCwd = process.cwd();
169
+ try {
170
+ process.chdir(projectDir);
171
+ const { undoCommand } = await import("../../commands/undo.js");
172
+ return undoCommand({ hard: !!a.hard });
173
+ } finally {
174
+ process.chdir(origCwd);
175
+ }
176
+ }
177
+
166
178
  export async function buildPreflightRequiredResponse(toolName) {
167
179
  const { config } = await loadConfig();
168
180
  const { listAgents } = await import("../../commands/agents.js");
@@ -10,11 +10,12 @@ import { loadConfig, applyRunOverrides, validateConfig } from "../config.js";
10
10
  import { currentBranch } from "../utils/git.js";
11
11
  import { buildProgressNotifier } from "./progress.js";
12
12
  import { compressResponse, compactStringify } from "./response-compressor.js";
13
+ import { withDocLink } from "../utils/doc-links.js";
13
14
 
14
15
  // ── Sub-module re-exports ────────────────────────────────────────────
15
16
  export { handleRunDirect, handleResumeDirect, validateResumeAnswer, handleRun, handleResume } from "./handlers/run-handler.js";
16
17
  export { handlePlanDirect, handleCodeDirect, handleReviewDirect, handleDiscoverDirect, handleTriageDirect, handleResearcherDirect, handleAuditDirect, handleArchitectDirect, handleCode, handleReview, handlePlan, handleDiscover, handleTriage, handleResearcher, handleArchitect, handleAudit } from "./handlers/direct-handlers.js";
17
- export { handleStatus, handleAgents, handlePreflight, handleRoles, handleReport, handleInit, handleDoctor, handleConfig, handleScan, handleBoard, buildPreflightRequiredResponse } from "./handlers/management-handlers.js";
18
+ export { handleStatus, handleAgents, handlePreflight, handleRoles, handleReport, handleInit, handleDoctor, handleConfig, handleScan, handleBoard, handleUndo, buildPreflightRequiredResponse } from "./handlers/management-handlers.js";
18
19
  export { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
19
20
 
20
21
  // ── Shared helpers (used by sub-modules and external consumers) ──────
@@ -82,22 +83,22 @@ const ERROR_CLASSIFIERS = [
82
83
  {
83
84
  test: (lower) => lower.includes("sonar") && (lower.includes("connect") || lower.includes("econnrefused") || lower.includes("not available") || lower.includes("not running")),
84
85
  category: "sonar_unavailable",
85
- suggestion: "SonarQube is not reachable. Try: kj_init to set up SonarQube, or run 'docker start sonarqube' if already installed. Use --no-sonar to skip SonarQube."
86
+ suggestion: withDocLink("SonarQube is not reachable. Try: kj_init to set up SonarQube, or run 'docker start sonarqube' if already installed. Use --no-sonar to skip SonarQube.", "sonar_docker")
86
87
  },
87
88
  {
88
89
  test: (lower) => lower.includes("401") || lower.includes("unauthorized") || lower.includes("invalid token"),
89
90
  category: "auth_error",
90
- suggestion: "Authentication failed. Regenerate the SonarQube token and update it via kj_init or in ~/.karajan/kj.config.yml under sonarqube.token."
91
+ suggestion: withDocLink("Authentication failed. Regenerate the SonarQube token and update it via kj_init or in ~/.karajan/kj.config.yml under sonarqube.token.", "sonar_token")
91
92
  },
92
93
  {
93
94
  test: (lower) => lower.includes("config") && (lower.includes("missing") || lower.includes("not found") || lower.includes("invalid")),
94
95
  category: "config_error",
95
- suggestion: "Configuration issue detected. Run kj_doctor to diagnose, or kj_init to create a fresh config."
96
+ suggestion: withDocLink("Configuration issue detected. Run kj_doctor to diagnose, or kj_init to create a fresh config.", "config_missing")
96
97
  },
97
98
  {
98
99
  test: (lower) => lower.includes("missing provider") || lower.includes("not found") && (lower.includes("claude") || lower.includes("codex") || lower.includes("gemini") || lower.includes("aider")),
99
100
  category: "agent_missing",
100
- suggestion: "Required agent CLI not found. Run kj_doctor to check which agents are installed and get installation instructions."
101
+ suggestion: withDocLink("Required agent CLI not found. Run kj_doctor to check which agents are installed and get installation instructions.", "agent_not_found")
101
102
  },
102
103
  {
103
104
  test: (lower) => lower.includes("timed out") || lower.includes("timeout"),
@@ -107,7 +108,7 @@ const ERROR_CLASSIFIERS = [
107
108
  {
108
109
  test: (lower) => lower.includes("you are on the base branch"),
109
110
  category: "branch_error",
110
- suggestion: "Create a feature branch before running Karajan. Use 'git checkout -b feat/<task-description>' and then retry. Do NOT run kj_code directly on the base branch."
111
+ suggestion: withDocLink("Create a feature branch before running Karajan. Use 'git checkout -b feat/<task-description>' and then retry. Do NOT run kj_code directly on the base branch.", "branch_error")
111
112
  },
112
113
  {
113
114
  test: (lower) => lower.includes("not a git repository"),
@@ -117,7 +118,7 @@ const ERROR_CLASSIFIERS = [
117
118
  {
118
119
  test: (lower) => lower.includes("bootstrap failed"),
119
120
  category: "bootstrap_error",
120
- suggestion: "Environment prerequisites not met. Run kj_doctor for diagnostics, then fix the issues listed. Do NOT work around these — fix them properly."
121
+ suggestion: withDocLink("Environment prerequisites not met. Run kj_doctor for diagnostics, then fix the issues listed. Do NOT work around these — fix them properly.", "bootstrap_failed")
121
122
  }
122
123
  ];
123
124
 
@@ -218,7 +219,7 @@ import {
218
219
  import {
219
220
  handleStatus, handleAgents, handlePreflight, handleRoles,
220
221
  handleReport, handleInit, handleDoctor, handleConfig,
221
- handleScan, handleBoard
222
+ handleScan, handleBoard, handleUndo
222
223
  } from "./handlers/management-handlers.js";
223
224
  import { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
224
225
 
@@ -245,7 +246,8 @@ const toolHandlers = {
245
246
  kj_board: (a) => handleBoard(a),
246
247
  kj_hu: (a, server) => handleHu(a, server),
247
248
  kj_suggest: (a) => handleSuggest(a),
248
- kj_skills: (a) => handleSkills(a)
249
+ kj_skills: (a) => handleSkills(a),
250
+ kj_undo: (a, server) => handleUndo(a, server)
249
251
  };
250
252
 
251
253
  export async function handleToolCall(name, args, server, extra) {
package/src/mcp/tools.js CHANGED
@@ -364,5 +364,16 @@ export const tools = [
364
364
  },
365
365
  required: ["action"]
366
366
  }
367
+ },
368
+ {
369
+ name: "kj_undo",
370
+ description: "Revert the last pipeline run by resetting to the pre-pipeline commit. Default: soft reset (keeps changes staged). Use hard=true to discard all changes.",
371
+ inputSchema: {
372
+ type: "object",
373
+ properties: {
374
+ hard: { type: "boolean", description: "If true, discard all changes (git reset --hard). Default: false (soft reset, keeps changes staged)." },
375
+ projectDir: { type: "string", description: "Absolute path to the project directory" }
376
+ }
377
+ }
367
378
  }
368
379
  ];
@@ -19,6 +19,7 @@ import {
19
19
  resolveSonarToken,
20
20
  resolveSonarCredentials,
21
21
  } from "../sonar/config-resolver.js";
22
+ import { withDocLink } from "../utils/doc-links.js";
22
23
 
23
24
  function parseJsonSafe(text) {
24
25
  try {
@@ -188,7 +189,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
188
189
  result.errors.push({
189
190
  check: "docker",
190
191
  message: "Docker not available but SonarQube is enabled.",
191
- fix: "Start Docker, or disable SonarQube: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
192
+ fix: withDocLink("Start Docker, or disable SonarQube: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar.", "sonar_docker")
192
193
  });
193
194
  logger.error("Preflight: Docker not found — SonarQube requires Docker");
194
195
 
@@ -222,7 +223,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
222
223
  result.errors.push({
223
224
  check: "sonar-reachable",
224
225
  message: `SonarQube not reachable at ${sonarHost}.`,
225
- fix: "Start SonarQube: 'docker start karajan-sonarqube', or disable it: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
226
+ fix: withDocLink("Start SonarQube: 'docker start karajan-sonarqube', or disable it: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar.", "sonar_docker")
226
227
  });
227
228
  logger.error("Preflight: SonarQube not reachable after remediation attempt");
228
229
  }
@@ -248,7 +249,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
248
249
  result.errors.push({
249
250
  check: "sonar-auth",
250
251
  message: "SonarQube is running but no authentication token is configured.",
251
- fix: "Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml."
252
+ fix: withDocLink("Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml.", "sonar_token")
252
253
  });
253
254
  logger.error("Preflight: Sonar auth failed");
254
255
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Documentation links for error messages.
3
+ *
4
+ * Maps error types to their relevant documentation page so that
5
+ * every actionable error includes a "See: <url>" pointer.
6
+ */
7
+
8
+ const BASE_URL = "https://karajancode.com/docs";
9
+
10
+ const ERROR_DOCS = {
11
+ sonar_token: `${BASE_URL}/guides/configuration/#sonarqube`,
12
+ sonar_docker: `${BASE_URL}/getting-started/installation/#docker`,
13
+ agent_not_found: `${BASE_URL}/getting-started/installation/#agents`,
14
+ bootstrap_failed: `${BASE_URL}/guides/troubleshooting/`,
15
+ config_missing: `${BASE_URL}/getting-started/quick-start/`,
16
+ branch_error: `${BASE_URL}/guides/pipeline/#git-workflow`,
17
+ rtk_install: `${BASE_URL}/guides/configuration/#rtk`,
18
+ };
19
+
20
+ export function docLink(errorType) {
21
+ return ERROR_DOCS[errorType] || `${BASE_URL}/guides/troubleshooting/`;
22
+ }
23
+
24
+ export function withDocLink(message, errorType) {
25
+ const link = docLink(errorType);
26
+ return `${message}\n See: ${link}`;
27
+ }