karajan-code 3.9.0 → 3.10.2

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,10 +1,10 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "3.9.0",
3
+ "version": "3.10.2",
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",
7
- "author": "Manu Fosela <mjfosela@gmail.com>",
7
+ "author": "manufosela (https://github.com/manufosela)",
8
8
  "homepage": "https://github.com/manufosela/karajan-code#readme",
9
9
  "repository": {
10
10
  "type": "git",
@@ -85,10 +85,16 @@ async function main() {
85
85
 
86
86
  // ── Step 4: Inject blob with postject ───────────────────────────
87
87
  console.log("[4/5] Injecting SEA blob with postject...");
88
+ // On macOS the blob MUST land as section NODE_SEA_BLOB inside a segment
89
+ // named NODE_SEA. Without --macho-segment-name postject writes it to a
90
+ // default segment the SEA loader never inspects, so the binary segfaults
91
+ // on startup (KJC-BUG-0096, nodejs/postject#76). The flag is Mach-O only —
92
+ // ELF (Linux) and PE (Windows) don't take it — so add it only on macOS.
88
93
  const postjectCmd = [
89
94
  `npx postject ${binaryPath}`,
90
95
  "NODE_SEA_BLOB dist/sea-prep.blob",
91
96
  "--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
97
+ ...(isMac ? ["--macho-segment-name NODE_SEA"] : []),
92
98
  ].join(" ");
93
99
  run(postjectCmd);
94
100
  console.log(" -> Blob injected.\n");
@@ -183,6 +183,15 @@ const ragStubPlugin = {
183
183
  parseWhere: notAvailable, buildWhereSql: notAvailable,
184
184
  // KJC-TSK-0449 — RAG cross-encoder rerank.
185
185
  rerank: notAvailable, RerankError: notAvailable, _resetPipeline: notAvailable,
186
+ // KJC-BUG-0097 — src/rag/auto-update.js lives under src/rag/ so it is
187
+ // caught by this stub, but its exports are called on the hot path.
188
+ // maybeAutoUpdate() runs on EVERY \`kj run\` (run.js): it must be a
189
+ // silent no-op here (RAG index is unavailable in SEA anyway), NOT an
190
+ // absent symbol — otherwise the whole command crashes with
191
+ // "maybeAutoUpdate is not a function". installPostMergeHook() is only
192
+ // reached from \`kj rag install-hooks\`, so it degrades like the rest.
193
+ maybeAutoUpdate: async () => ({ skipped: true }),
194
+ installPostMergeHook: notAvailable,
186
195
  default: notAvailable,
187
196
  };
188
197
  `,
@@ -77,6 +77,13 @@ try {
77
77
  } else {
78
78
  Write-Host "kj-install: '$installDir' is already on your user PATH — run 'kj --help' to get started."
79
79
  }
80
+
81
+ # The binary bundles no toolchain. kj orchestrates external tools, so name
82
+ # the hard requirements and let `kj doctor` check them for this machine.
83
+ Write-Host ""
84
+ Write-Host "kj-install: next step — run 'kj doctor' to check prerequisites."
85
+ Write-Host " Required: git, plus at least one agent CLI (Claude Code, Codex or Gemini)."
86
+ Write-Host " Optional: Docker (local models, SonarQube) and Node/npm (helper tools: Squeezr, qmd)."
80
87
  } finally {
81
88
  Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
82
89
  }
@@ -35,8 +35,18 @@ esac
35
35
 
36
36
  target="${os}-${arch}"
37
37
  case "$target" in
38
- linux-x64 | darwin-arm64) ;;
39
- *) die "no prebuilt binary for '$target'. Available: linux-x64, darwin-arm64. Use npm instead: npm install -g karajan-code" ;;
38
+ linux-x64) ;;
39
+ darwin-arm64)
40
+ # The macOS standalone binary is not published yet (tracked in
41
+ # KJC-BUG-0096: the darwin-arm64 SEA build crashes under smoke-test).
42
+ # Degrade gracefully to the npm install path instead of trying to
43
+ # download an asset that does not exist.
44
+ echo "kj-install: the macOS standalone binary is not available yet." >&2
45
+ echo "kj-install: install with npm instead (requires Node 18+):" >&2
46
+ echo " npm install -g karajan-code" >&2
47
+ exit 0
48
+ ;;
49
+ *) die "no prebuilt binary for '$target'. Available: linux-x64. On macOS use npm instead: npm install -g karajan-code" ;;
40
50
  esac
41
51
 
42
52
  # --- Resolve the download URL for the requested version (or latest). ---
@@ -101,3 +111,11 @@ case ":${PATH}:" in
101
111
  echo " (add that line to ~/.bashrc, ~/.zshrc or ~/.profile to make it permanent)"
102
112
  ;;
103
113
  esac
114
+
115
+ # --- Prerequisites: the binary bundles no toolchain. kj orchestrates ---
116
+ # --- external tools, so name the hard requirements and let `kj doctor` ---
117
+ # --- check them precisely for this machine.
118
+ echo ""
119
+ echo "kj-install: next step — run 'kj doctor' to check prerequisites."
120
+ echo " Required: git, plus at least one agent CLI (Claude Code, Codex or Gemini)."
121
+ echo " Optional: Docker (local models, SonarQube) and Node/npm (helper tools: Squeezr, qmd)."
@@ -160,6 +160,28 @@ function statusIcon(status) {
160
160
  }
161
161
  }
162
162
 
163
+ // The `kj` binary bundles no toolchain: it orchestrates external tools. So
164
+ // beyond the per-check wall, spell out what is *required* to run kj at all
165
+ // (git + at least one agent CLI) versus what is *optional* (enables extra
166
+ // features). We derive the verdict from the checks already collected rather
167
+ // than re-probing, so it always agrees with the lines printed above.
168
+ function printPrerequisites(report) {
169
+ const git = report.checks.find((c) => c.name === "git");
170
+ const agents = report.checks.filter((c) => c.name?.startsWith("agent:"));
171
+ const okAgent = agents.find((c) => c.status === STATUS.OK);
172
+ const mark = (ok) => (ok ? "OK" : "MISSING");
173
+
174
+ const gitOk = git?.status === STATUS.OK;
175
+ const agentLabel = okAgent
176
+ ? `OK (${okAgent.label.replace(/^Agent:\s*/, "")})`
177
+ : "MISSING — install Claude Code, Codex or Gemini";
178
+
179
+ console.log();
180
+ console.log("Prerequisites (kj orchestrates external tools, it bundles none):");
181
+ console.log(` Required: git ${mark(gitOk)}; agent CLI ${agentLabel}`);
182
+ console.log(" Optional: Docker (local models, SonarQube); Node/npm (helper tools: Squeezr, qmd)");
183
+ }
184
+
163
185
  function printHuman(report, { verbose }) {
164
186
  for (const check of report.checks) {
165
187
  console.log(`${statusIcon(check.status)} ${check.label}: ${check.detail}`);
@@ -186,6 +208,8 @@ function printHuman(report, { verbose }) {
186
208
  console.log(`Runtime overrides applied: ${JSON.stringify(report.overrides)}`);
187
209
  }
188
210
 
211
+ printPrerequisites(report);
212
+
189
213
  // KJC-TSK v2.18 — if any external audit tool (semgrep, osv-scanner,
190
214
  // lighthouse) reported missing, surface the one-line remediation so
191
215
  // the user doesn't have to dig the fix lines out of the wall of
@@ -202,4 +226,4 @@ function printHuman(report, { verbose }) {
202
226
  }
203
227
  }
204
228
 
205
- export const __test = { printHuman };
229
+ export const __test = { printHuman, printPrerequisites };
@@ -26,6 +26,18 @@ const ALLOWED_COMMANDS = [
26
26
  "bundle install", "composer install", "dotnet restore"
27
27
  ];
28
28
 
29
+ /**
30
+ * True when `target` resolves to a path inside `base` (or is `base` itself).
31
+ * Uses path.relative instead of a string prefix check: a naive
32
+ * `resolved.startsWith(base)` lets a sibling with a shared prefix slip through
33
+ * (base `/repo/app` would accept `/repo/app2`). Also rejects absolute paths
34
+ * pointing outside the project.
35
+ */
36
+ function isInsideBase(target, base) {
37
+ const rel = path.relative(base, target);
38
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
39
+ }
40
+
29
41
  /**
30
42
  * Validate that a shell command is in the allow-list.
31
43
  * Prevents arbitrary command execution.
@@ -74,7 +86,7 @@ async function createFile({ filePath, content, cwd, overwrite = false }) {
74
86
  // Path traversal guard
75
87
  const resolved = path.resolve(fullPath);
76
88
  const base = path.resolve(cwd || process.cwd());
77
- if (!resolved.startsWith(base)) {
89
+ if (!isInsideBase(resolved, base)) {
78
90
  return { ok: false, error: "Path traversal denied", action: "create_file" };
79
91
  }
80
92
 
@@ -130,9 +142,10 @@ async function gitAdd({ files, cwd }) {
130
142
  }
131
143
 
132
144
  try {
133
- // Only allow relative paths (no absolute paths, no shell metacharacters)
145
+ // Reject shell metacharacters and anything that resolves outside the project.
146
+ const base = path.resolve(cwd || process.cwd());
134
147
  for (const f of files) {
135
- if (typeof f !== "string" || f.includes("..") || /[;&|`$]/.test(f)) {
148
+ if (typeof f !== "string" || /[;&|`$]/.test(f) || !isInsideBase(path.resolve(base, f), base)) {
136
149
  return { ok: false, error: `Invalid file path: ${f}`, action: "git_add" };
137
150
  }
138
151
  }
@@ -11,6 +11,20 @@ import { createStallDetector } from "../../utils/stall-detector.js";
11
11
 
12
12
  const ROLE_NAMES = ["planner", "researcher", "architect", "refactorer", "reviewer", "tester", "security", "impeccable"];
13
13
 
14
+ // What each role is for. Used to explain — deterministically — why triage would
15
+ // or would not turn a role on. These describe the role's trigger, never a claim
16
+ // about the current task, so the rationale is never invented per run.
17
+ const ROLE_PURPOSE = {
18
+ planner: "planificación multi-fichero",
19
+ researcher: "investigación de contexto externo",
20
+ architect: "decisiones de arquitectura",
21
+ refactorer: "refactor de código existente",
22
+ reviewer: "revisión de cambios",
23
+ tester: "cobertura de tests dedicada",
24
+ security: "auditoría de seguridad",
25
+ impeccable: "auditoría de diseño"
26
+ };
27
+
14
28
  function buildRoleOverrides(recommendedRoles, pipelineConfig) {
15
29
  const overrides = {};
16
30
  for (const role of ROLE_NAMES) {
@@ -19,6 +33,28 @@ function buildRoleOverrides(recommendedRoles, pipelineConfig) {
19
33
  return overrides;
20
34
  }
21
35
 
36
+ // Per-role explanation of the activate/skip decision, mirroring buildRoleOverrides.
37
+ // When triage failed, recommendations are unavailable: roles resolve from config
38
+ // alone and skipped ones get a neutral "sin datos de triage" reason.
39
+ function buildRoleRationale(recommendedRoles, pipelineConfig, triageOk) {
40
+ return ROLE_NAMES.map((role) => {
41
+ const forced = Boolean(pipelineConfig[role]?.enabled);
42
+ const recommended = triageOk && recommendedRoles.has(role);
43
+ const purpose = ROLE_PURPOSE[role];
44
+
45
+ if (recommended) {
46
+ return { role, enabled: true, source: "triage", reason: `recomendado por triage — ${purpose}` };
47
+ }
48
+ if (forced) {
49
+ return { role, enabled: true, source: "config", reason: `forzado por configuración — ${purpose}` };
50
+ }
51
+ const reason = triageOk
52
+ ? `no recomendado — ${purpose} sin aplicar`
53
+ : `sin datos de triage — ${purpose} sin evaluar`;
54
+ return { role, enabled: false, source: "none", reason };
55
+ });
56
+ }
57
+
22
58
  function applyModelSelection(triageOutput, config, emitter, eventBase) {
23
59
  if (!triageOutput.ok || !config?.model_selection?.enabled) return null;
24
60
  const level = triageOutput.result?.level;
@@ -104,7 +140,8 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
104
140
  taskType: triageOutput.result?.taskType || "sw",
105
141
  shouldDecompose,
106
142
  subtasks,
107
- domainHints
143
+ domainHints,
144
+ roleRationale: buildRoleRationale(recommendedRoles, config.pipeline || {}, triageOutput.ok)
108
145
  };
109
146
 
110
147
  const modelSelection = applyModelSelection(triageOutput, config, emitter, eventBase);
@@ -69,6 +69,20 @@ export const EVENT_HANDLERS = {
69
69
  console.log(` \u251c\u2500 ${icon} TDD policy: ${label}${files}${executor}`);
70
70
  },
71
71
 
72
+ "triage:end": (event, icon, elapsed, status) => {
73
+ const detail = event.detail || {};
74
+ const level = detail.level ? ` ${detail.level}` : "";
75
+ const onCount = detail.roles?.length || 0;
76
+ console.log(
77
+ ` \u251c\u2500 ${icon} ${status} Triage${level}${ANSI.dim} \u2014 ${onCount} role(s) on${ANSI.reset} ${elapsed}`
78
+ );
79
+ // Per-role rationale: why each role was activated or skipped (KJC-TSK-0601).
80
+ for (const entry of detail.roleRationale || []) {
81
+ const mark = entry.enabled ? `${ANSI.green}\u2713${ANSI.reset}` : `${ANSI.dim}\u2717${ANSI.reset}`;
82
+ console.log(` \u2502 ${mark} ${ANSI.dim}${entry.role}: ${entry.reason}${ANSI.reset}`);
83
+ }
84
+ },
85
+
72
86
  "researcher:start": (event, icon) => {
73
87
  console.log(` \u251c\u2500 ${icon} Researcher (${event.detail?.researcher || "?"}) investigating...`);
74
88
  },