karajan-code 3.10.1 → 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.10.1",
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",
@@ -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
  },