karajan-code 1.9.5 → 1.9.6

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.9.5",
3
+ "version": "1.9.6",
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",
@@ -71,16 +71,34 @@ function createStreamJsonFilter(onOutput) {
71
71
  }
72
72
 
73
73
  /**
74
- * Build a clean environment for Claude subprocess.
75
- * Claude Code 2.x sets CLAUDECODE=1 to detect nesting. When Karajan's MCP
76
- * server runs inside Claude Code and spawns `claude -p`, the child inherits
77
- * this variable and refuses to start. Stripping it allows the subprocess to
78
- * run normallyit is a separate, non-interactive invocation, not a true
79
- * nested session.
74
+ * Build clean execa options for Claude subprocess.
75
+ *
76
+ * Three critical fixes for running `claude -p` from Node.js:
77
+ *
78
+ * 1. Strip CLAUDECODE env var Claude Code 2.x sets this to block nested
79
+ * sessions. The spawned `claude -p` is a separate non-interactive
80
+ * invocation, not a true nested session.
81
+ *
82
+ * 2. Detach stdin (stdin: "ignore") — When launched from Node.js (which is
83
+ * how Claude Code / Karajan MCP runs), the child inherits the parent's
84
+ * stdin. `claude -p` then blocks waiting to read from a stdin that the
85
+ * parent is already consuming. Ignoring stdin prevents the hang.
86
+ *
87
+ * 3. Claude Code 2.x writes all structured output (stream-json, json) to
88
+ * stderr, NOT stdout. The agent must read from stderr for the actual
89
+ * response data.
80
90
  */
81
- function cleanEnv() {
82
- const { CLAUDECODE, ...rest } = process.env;
83
- return rest;
91
+ function cleanExecaOpts(extra = {}) {
92
+ const { CLAUDECODE, ...env } = process.env;
93
+ return { env, stdin: "ignore", ...extra };
94
+ }
95
+
96
+ /**
97
+ * Pick the best raw output from a claude subprocess result.
98
+ * Claude 2.x sends structured output to stderr; stdout is often empty.
99
+ */
100
+ function pickOutput(res) {
101
+ return res.stdout || res.stderr || "";
84
102
  }
85
103
 
86
104
  export class ClaudeAgent extends BaseAgent {
@@ -90,36 +108,38 @@ export class ClaudeAgent extends BaseAgent {
90
108
  const model = this.getRoleModel(role);
91
109
  if (model) args.push("--model", model);
92
110
 
93
- const env = cleanEnv();
94
-
95
111
  // Use stream-json when onOutput is provided to get real-time feedback
96
112
  if (task.onOutput) {
97
113
  args.push("--output-format", "stream-json");
98
114
  const streamFilter = createStreamJsonFilter(task.onOutput);
99
- const res = await runCommand(resolveBin("claude"), args, {
100
- env,
115
+ const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
101
116
  onOutput: streamFilter,
102
117
  silenceTimeoutMs: task.silenceTimeoutMs,
103
118
  timeout: task.timeoutMs
104
- });
105
- const output = extractTextFromStreamJson(res.stdout);
106
- return { ok: res.exitCode === 0, output, error: res.stderr, exitCode: res.exitCode };
119
+ }));
120
+ const raw = pickOutput(res);
121
+ const output = extractTextFromStreamJson(raw);
122
+ return { ok: res.exitCode === 0, output, error: res.exitCode !== 0 ? raw : "", exitCode: res.exitCode };
107
123
  }
108
124
 
109
- const res = await runCommand(resolveBin("claude"), args, { env });
110
- return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
125
+ // Without streaming, use json output to get structured response via stderr
126
+ args.push("--output-format", "json");
127
+ const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts());
128
+ const raw = pickOutput(res);
129
+ const output = extractTextFromStreamJson(raw);
130
+ return { ok: res.exitCode === 0, output, error: res.exitCode !== 0 ? raw : "", exitCode: res.exitCode };
111
131
  }
112
132
 
113
133
  async reviewTask(task) {
114
- const args = ["-p", task.prompt, "--output-format", "json"];
134
+ const args = ["-p", task.prompt, "--output-format", "stream-json"];
115
135
  const model = this.getRoleModel(task.role || "reviewer");
116
136
  if (model) args.push("--model", model);
117
- const res = await runCommand(resolveBin("claude"), args, {
118
- env: cleanEnv(),
137
+ const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
119
138
  onOutput: task.onOutput,
120
139
  silenceTimeoutMs: task.silenceTimeoutMs,
121
140
  timeout: task.timeoutMs
122
- });
123
- return { ok: res.exitCode === 0, output: res.stdout, error: res.stderr, exitCode: res.exitCode };
141
+ }));
142
+ const raw = pickOutput(res);
143
+ return { ok: res.exitCode === 0, output: raw, error: res.exitCode !== 0 ? raw : "", exitCode: res.exitCode };
124
144
  }
125
145
  }