@nathapp/nax 0.37.0 → 0.38.1
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/dist/nax.js +3258 -2894
- package/package.json +4 -1
- package/src/agents/claude-complete.ts +72 -0
- package/src/agents/claude-execution.ts +189 -0
- package/src/agents/claude-interactive.ts +77 -0
- package/src/agents/claude-plan.ts +23 -8
- package/src/agents/claude.ts +64 -349
- package/src/analyze/classifier.ts +2 -1
- package/src/cli/config-descriptions.ts +206 -0
- package/src/cli/config-diff.ts +103 -0
- package/src/cli/config-display.ts +285 -0
- package/src/cli/config-get.ts +55 -0
- package/src/cli/config.ts +7 -618
- package/src/cli/prompts-export.ts +58 -0
- package/src/cli/prompts-init.ts +200 -0
- package/src/cli/prompts-main.ts +237 -0
- package/src/cli/prompts-tdd.ts +78 -0
- package/src/cli/prompts.ts +10 -541
- package/src/commands/logs-formatter.ts +201 -0
- package/src/commands/logs-reader.ts +171 -0
- package/src/commands/logs.ts +11 -362
- package/src/config/loader.ts +4 -15
- package/src/config/runtime-types.ts +448 -0
- package/src/config/schema-types.ts +53 -0
- package/src/config/types.ts +49 -486
- package/src/context/auto-detect.ts +2 -1
- package/src/context/builder.ts +3 -2
- package/src/execution/crash-heartbeat.ts +77 -0
- package/src/execution/crash-recovery.ts +23 -365
- package/src/execution/crash-signals.ts +149 -0
- package/src/execution/crash-writer.ts +154 -0
- package/src/execution/parallel-coordinator.ts +278 -0
- package/src/execution/parallel-executor-rectification-pass.ts +117 -0
- package/src/execution/parallel-executor-rectify.ts +135 -0
- package/src/execution/parallel-executor.ts +19 -211
- package/src/execution/parallel-worker.ts +148 -0
- package/src/execution/parallel.ts +5 -404
- package/src/execution/pid-registry.ts +3 -8
- package/src/execution/runner-completion.ts +160 -0
- package/src/execution/runner-execution.ts +221 -0
- package/src/execution/runner-setup.ts +82 -0
- package/src/execution/runner.ts +53 -202
- package/src/execution/timeout-handler.ts +100 -0
- package/src/hooks/runner.ts +11 -21
- package/src/metrics/tracker.ts +7 -30
- package/src/pipeline/runner.ts +2 -1
- package/src/pipeline/stages/completion.ts +0 -1
- package/src/pipeline/stages/context.ts +2 -1
- package/src/plugins/extensions.ts +225 -0
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/types.ts +16 -221
- package/src/prd/index.ts +2 -1
- package/src/prd/validate.ts +41 -0
- package/src/precheck/checks-blockers.ts +15 -419
- package/src/precheck/checks-cli.ts +68 -0
- package/src/precheck/checks-config.ts +102 -0
- package/src/precheck/checks-git.ts +87 -0
- package/src/precheck/checks-system.ts +163 -0
- package/src/review/orchestrator.ts +19 -6
- package/src/review/runner.ts +17 -5
- package/src/routing/chain.ts +2 -1
- package/src/routing/loader.ts +2 -5
- package/src/tdd/orchestrator.ts +2 -1
- package/src/tdd/verdict-reader.ts +266 -0
- package/src/tdd/verdict.ts +6 -271
- package/src/utils/errors.ts +12 -0
- package/src/utils/git.ts +12 -5
- package/src/utils/json-file.ts +72 -0
- package/src/verification/executor.ts +2 -1
- package/src/verification/smart-runner.ts +23 -3
- package/src/worktree/manager.ts +9 -3
- package/src/worktree/merge.ts +3 -2
package/src/agents/claude.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code Agent Adapter
|
|
3
|
+
*
|
|
4
|
+
* Main adapter class coordinating execution, completion, decomposition, and interactive modes.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import { PidRegistry } from "../execution/pid-registry";
|
|
8
|
+
import { withProcessTimeout } from "../execution/timeout-handler";
|
|
6
9
|
import { getLogger } from "../logger";
|
|
10
|
+
import { _completeDeps, executeComplete } from "./claude-complete";
|
|
7
11
|
import { buildDecomposePrompt, parseDecomposeOutput } from "./claude-decompose";
|
|
12
|
+
import { _runOnceDeps, buildAllowedEnv, buildCommand, executeOnce } from "./claude-execution";
|
|
13
|
+
import { runInteractiveMode } from "./claude-interactive";
|
|
8
14
|
import { runPlan } from "./claude-plan";
|
|
9
|
-
import { estimateCostByDuration, estimateCostFromOutput } from "./cost";
|
|
10
15
|
import type {
|
|
11
16
|
AgentAdapter,
|
|
12
17
|
AgentCapabilities,
|
|
@@ -20,49 +25,6 @@ import type {
|
|
|
20
25
|
PlanResult,
|
|
21
26
|
PtyHandle,
|
|
22
27
|
} from "./types";
|
|
23
|
-
import { CompleteError } from "./types";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Maximum characters to capture from agent stdout.
|
|
27
|
-
*
|
|
28
|
-
* Last 5000 chars typically contain the most relevant info (final status, summary, errors).
|
|
29
|
-
* This limit prevents memory bloat while preserving actionable output.
|
|
30
|
-
*/
|
|
31
|
-
const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Maximum characters to capture from agent stderr.
|
|
35
|
-
*
|
|
36
|
-
* Last 1000 chars typically contain the actual error message (e.g., 401, 500, crash).
|
|
37
|
-
* Smaller than stdout since stderr is more focused on errors.
|
|
38
|
-
*/
|
|
39
|
-
const MAX_AGENT_STDERR_CHARS = 1000;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Grace period in ms between SIGTERM and SIGKILL on timeout.
|
|
43
|
-
* Mirrors the pattern in src/verification/executor.ts:executeWithTimeout().
|
|
44
|
-
*/
|
|
45
|
-
const SIGKILL_GRACE_PERIOD_MS = 5000;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Injectable dependencies for complete() — allows tests to intercept
|
|
49
|
-
* Bun.spawn calls and verify correct CLI args without the claude binary.
|
|
50
|
-
*
|
|
51
|
-
* @internal
|
|
52
|
-
*/
|
|
53
|
-
export const _completeDeps = {
|
|
54
|
-
spawn(
|
|
55
|
-
cmd: string[],
|
|
56
|
-
opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
|
|
57
|
-
): { stdout: ReadableStream<Uint8Array>; stderr: ReadableStream<Uint8Array>; exited: Promise<number>; pid: number } {
|
|
58
|
-
return Bun.spawn(cmd, opts) as unknown as {
|
|
59
|
-
stdout: ReadableStream<Uint8Array>;
|
|
60
|
-
stderr: ReadableStream<Uint8Array>;
|
|
61
|
-
exited: Promise<number>;
|
|
62
|
-
pid: number;
|
|
63
|
-
};
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
28
|
|
|
67
29
|
/**
|
|
68
30
|
* Injectable dependencies for decompose() — allows tests to intercept
|
|
@@ -91,17 +53,8 @@ export const _decomposeDeps = {
|
|
|
91
53
|
},
|
|
92
54
|
};
|
|
93
55
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* that PID cleanup (unregister) always runs even if kill() throws.
|
|
97
|
-
*
|
|
98
|
-
* @internal
|
|
99
|
-
*/
|
|
100
|
-
export const _runOnceDeps = {
|
|
101
|
-
killProc(proc: { kill(signal?: number | NodeJS.Signals): void }, signal: NodeJS.Signals): void {
|
|
102
|
-
proc.kill(signal);
|
|
103
|
-
},
|
|
104
|
-
};
|
|
56
|
+
// Re-export deps for testing
|
|
57
|
+
export { _runOnceDeps, _completeDeps };
|
|
105
58
|
|
|
106
59
|
/**
|
|
107
60
|
* Claude Code agent adapter implementation.
|
|
@@ -146,240 +99,62 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
146
99
|
}
|
|
147
100
|
|
|
148
101
|
buildCommand(options: AgentRunOptions): string[] {
|
|
149
|
-
|
|
150
|
-
const skipPermissions = options.dangerouslySkipPermissions ?? true;
|
|
151
|
-
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
152
|
-
return [this.binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
102
|
+
return buildCommand(this.binary, options);
|
|
153
103
|
}
|
|
154
104
|
|
|
155
|
-
async run(options: AgentRunOptions): Promise<AgentResult> {
|
|
156
|
-
const maxRetries = 3;
|
|
157
|
-
let lastError: Error | null = null;
|
|
158
|
-
|
|
159
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
160
|
-
try {
|
|
161
|
-
const result = await this.runOnce(options, attempt);
|
|
162
|
-
|
|
163
|
-
if (result.rateLimited && attempt < maxRetries) {
|
|
164
|
-
const backoffMs = 2 ** attempt * 1000;
|
|
165
|
-
const logger = getLogger();
|
|
166
|
-
logger.warn("agent", "Rate limited, retrying", { backoffSeconds: backoffMs / 1000, attempt, maxRetries });
|
|
167
|
-
await Bun.sleep(backoffMs);
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return result;
|
|
172
|
-
} catch (error) {
|
|
173
|
-
lastError = error as Error;
|
|
174
|
-
const isSpawnError = lastError.message.includes("spawn") || lastError.message.includes("ENOENT");
|
|
175
|
-
|
|
176
|
-
if (isSpawnError && attempt < maxRetries) {
|
|
177
|
-
const backoffMs = 2 ** attempt * 1000;
|
|
178
|
-
const logger = getLogger();
|
|
179
|
-
logger.warn("agent", "Agent spawn error, retrying", {
|
|
180
|
-
error: lastError.message,
|
|
181
|
-
backoffSeconds: backoffMs / 1000,
|
|
182
|
-
attempt,
|
|
183
|
-
maxRetries,
|
|
184
|
-
});
|
|
185
|
-
await Bun.sleep(backoffMs);
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
throw lastError;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
throw lastError || new Error("Agent execution failed after all retries");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Build allowed environment variables for spawned agents.
|
|
198
|
-
* SEC-4: Only pass essential env vars to prevent leaking sensitive data.
|
|
199
|
-
*/
|
|
200
105
|
buildAllowedEnv(options: AgentRunOptions): Record<string, string | undefined> {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
204
|
-
for (const varName of essentialVars) {
|
|
205
|
-
if (process.env[varName]) {
|
|
206
|
-
allowed[varName] = process.env[varName];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
211
|
-
for (const varName of apiKeyVars) {
|
|
212
|
-
if (process.env[varName]) {
|
|
213
|
-
allowed[varName] = process.env[varName];
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_"];
|
|
218
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
219
|
-
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
220
|
-
allowed[key] = value;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (options.modelDef.env) {
|
|
225
|
-
Object.assign(allowed, options.modelDef.env);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (options.env) {
|
|
229
|
-
Object.assign(allowed, options.env);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return allowed;
|
|
106
|
+
return buildAllowedEnv(options);
|
|
233
107
|
}
|
|
234
108
|
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const proc = Bun.spawn(cmd, {
|
|
240
|
-
cwd: options.workdir,
|
|
241
|
-
stdout: "pipe",
|
|
242
|
-
stderr: "inherit", // MEM-3: Inherit stderr to avoid blocking on unread pipe
|
|
243
|
-
env: this.buildAllowedEnv(options),
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const processPid = proc.pid;
|
|
247
|
-
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
248
|
-
await pidRegistry.register(processPid);
|
|
249
|
-
|
|
250
|
-
let timedOut = false;
|
|
251
|
-
const timeoutId = setTimeout(() => {
|
|
252
|
-
timedOut = true;
|
|
253
|
-
try {
|
|
254
|
-
_runOnceDeps.killProc(proc, "SIGTERM" as NodeJS.Signals);
|
|
255
|
-
} catch {
|
|
256
|
-
/* already exited */
|
|
257
|
-
}
|
|
258
|
-
setTimeout(() => {
|
|
259
|
-
try {
|
|
260
|
-
_runOnceDeps.killProc(proc, "SIGKILL" as NodeJS.Signals);
|
|
261
|
-
} catch {
|
|
262
|
-
/* already exited */
|
|
263
|
-
}
|
|
264
|
-
}, SIGKILL_GRACE_PERIOD_MS);
|
|
265
|
-
}, options.timeoutSeconds * 1000);
|
|
109
|
+
async run(options: AgentRunOptions): Promise<AgentResult> {
|
|
110
|
+
const maxRetries = 3;
|
|
111
|
+
let lastError: Error | null = null;
|
|
266
112
|
|
|
267
|
-
let exitCode: number;
|
|
268
113
|
try {
|
|
269
|
-
|
|
270
|
-
// (Bun subprocess edge case on some environments), fall back to -1 so the
|
|
271
|
-
// caller can move on. timedOut flag ensures the result is still marked 124.
|
|
272
|
-
const hardDeadlineMs = options.timeoutSeconds * 1000 + SIGKILL_GRACE_PERIOD_MS + 3000;
|
|
273
|
-
exitCode = await Promise.race([
|
|
274
|
-
proc.exited,
|
|
275
|
-
new Promise<number>((resolve) => setTimeout(() => resolve(-1), hardDeadlineMs)),
|
|
276
|
-
]);
|
|
277
|
-
|
|
278
|
-
// If hard deadline fired, the subprocess may still be alive (Bun SIGKILL edge case).
|
|
279
|
-
// Force-kill via OS-level signal so that the stdout pipe closes and we don't block.
|
|
280
|
-
if (exitCode === -1) {
|
|
281
|
-
try {
|
|
282
|
-
process.kill(processPid, "SIGKILL");
|
|
283
|
-
} catch {
|
|
284
|
-
/* already gone */
|
|
285
|
-
}
|
|
286
|
-
// Note: process.kill(-pid) (process group) only works if the child called
|
|
287
|
-
// setpgid/setsid. Bun does not do this, so this will silently throw ESRCH.
|
|
288
|
-
// Left here as a best-effort safety net for environments that do set a pgid.
|
|
114
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
289
115
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
116
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
117
|
+
const result = await executeOnce(this.binary, options, pidRegistry);
|
|
118
|
+
|
|
119
|
+
if (result.rateLimited && attempt < maxRetries) {
|
|
120
|
+
const backoffMs = 2 ** attempt * 1000;
|
|
121
|
+
const logger = getLogger();
|
|
122
|
+
logger.warn("agent", "Rate limited, retrying", { backoffSeconds: backoffMs / 1000, attempt, maxRetries });
|
|
123
|
+
await Bun.sleep(backoffMs);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
lastError = error as Error;
|
|
130
|
+
const isSpawnError = lastError.message.includes("spawn") || lastError.message.includes("ENOENT");
|
|
131
|
+
|
|
132
|
+
if (isSpawnError && attempt < maxRetries) {
|
|
133
|
+
const backoffMs = 2 ** attempt * 1000;
|
|
134
|
+
const logger = getLogger();
|
|
135
|
+
logger.warn("agent", "Agent spawn error, retrying", {
|
|
136
|
+
error: lastError.message,
|
|
137
|
+
backoffSeconds: backoffMs / 1000,
|
|
138
|
+
attempt,
|
|
139
|
+
maxRetries,
|
|
140
|
+
});
|
|
141
|
+
await Bun.sleep(backoffMs);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw lastError;
|
|
293
146
|
}
|
|
294
147
|
}
|
|
295
|
-
} finally {
|
|
296
|
-
clearTimeout(timeoutId);
|
|
297
|
-
await pidRegistry.unregister(processPid);
|
|
298
|
-
}
|
|
299
148
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
new Promise<string>((resolve) => setTimeout(() => resolve(""), 5000)),
|
|
305
|
-
]);
|
|
306
|
-
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
307
|
-
const durationMs = Date.now() - startTime;
|
|
308
|
-
|
|
309
|
-
// Claude Code emits rate limit messages as part of its output.
|
|
310
|
-
const fullOutput = stdout + stderr;
|
|
311
|
-
const rateLimited =
|
|
312
|
-
fullOutput.toLowerCase().includes("rate limit") ||
|
|
313
|
-
fullOutput.includes("429") ||
|
|
314
|
-
fullOutput.toLowerCase().includes("too many requests");
|
|
315
|
-
|
|
316
|
-
let costEstimate = estimateCostFromOutput(options.modelTier, fullOutput);
|
|
317
|
-
const logger = getLogger();
|
|
318
|
-
if (!costEstimate) {
|
|
319
|
-
const fallbackEstimate = estimateCostByDuration(options.modelTier, durationMs);
|
|
320
|
-
costEstimate = {
|
|
321
|
-
cost: fallbackEstimate.cost * 1.5,
|
|
322
|
-
confidence: "fallback",
|
|
323
|
-
};
|
|
324
|
-
logger.warn("agent", "Cost estimation fallback (duration-based)", {
|
|
325
|
-
modelTier: options.modelTier,
|
|
326
|
-
cost: costEstimate.cost,
|
|
327
|
-
});
|
|
328
|
-
} else if (costEstimate.confidence === "estimated") {
|
|
329
|
-
logger.warn("agent", "Cost estimation using regex parsing (estimated confidence)", { cost: costEstimate.cost });
|
|
149
|
+
throw lastError || new Error("Agent execution failed after all retries");
|
|
150
|
+
} finally {
|
|
151
|
+
// Clean up pidRegistry entry for this workdir to prevent unbounded Map growth
|
|
152
|
+
this.pidRegistries.delete(options.workdir);
|
|
330
153
|
}
|
|
331
|
-
const cost = costEstimate.cost;
|
|
332
|
-
|
|
333
|
-
const actualExitCode = timedOut ? 124 : exitCode;
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
success: exitCode === 0 && !timedOut,
|
|
337
|
-
exitCode: actualExitCode,
|
|
338
|
-
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
339
|
-
stderr: stderr.slice(-MAX_AGENT_STDERR_CHARS),
|
|
340
|
-
rateLimited,
|
|
341
|
-
durationMs,
|
|
342
|
-
estimatedCost: cost,
|
|
343
|
-
pid: processPid,
|
|
344
|
-
};
|
|
345
154
|
}
|
|
346
155
|
|
|
347
156
|
async complete(prompt: string, options?: CompleteOptions): Promise<string> {
|
|
348
|
-
|
|
349
|
-
const cmd = ["claude", "-p", prompt];
|
|
350
|
-
|
|
351
|
-
if (options?.model) {
|
|
352
|
-
cmd.push("--model", options.model);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (options?.maxTokens !== undefined) {
|
|
356
|
-
cmd.push("--max-tokens", String(options.maxTokens));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (options?.jsonMode) {
|
|
360
|
-
cmd.push("--output-format", "json");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const proc = _completeDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
364
|
-
const exitCode = await proc.exited;
|
|
365
|
-
|
|
366
|
-
// Read stdout and stderr for error messages
|
|
367
|
-
const stdout = await new Response(proc.stdout).text();
|
|
368
|
-
const stderr = await new Response(proc.stderr).text();
|
|
369
|
-
const trimmed = stdout.trim();
|
|
370
|
-
|
|
371
|
-
// Validate exit code and output
|
|
372
|
-
if (exitCode !== 0) {
|
|
373
|
-
const errorDetails = stderr.trim() || trimmed;
|
|
374
|
-
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
375
|
-
throw new CompleteError(errorMessage, exitCode);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (!trimmed) {
|
|
379
|
-
throw new CompleteError("complete() returned empty output");
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return trimmed;
|
|
157
|
+
return executeComplete(this.binary, prompt, options);
|
|
383
158
|
}
|
|
384
159
|
|
|
385
160
|
async plan(options: PlanOptions): Promise<PlanResult> {
|
|
@@ -392,7 +167,6 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
392
167
|
|
|
393
168
|
const prompt = buildDecomposePrompt(options);
|
|
394
169
|
|
|
395
|
-
// Resolve model: explicit modelDef > config.models.balanced > throw
|
|
396
170
|
let modelDef = options.modelDef;
|
|
397
171
|
if (!modelDef) {
|
|
398
172
|
if (!options.config) {
|
|
@@ -408,7 +182,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
408
182
|
const proc = _decomposeDeps.spawn(cmd, {
|
|
409
183
|
cwd: options.workdir,
|
|
410
184
|
stdout: "pipe",
|
|
411
|
-
stderr: "inherit",
|
|
185
|
+
stderr: "inherit",
|
|
412
186
|
env: this.buildAllowedEnv({
|
|
413
187
|
workdir: options.workdir,
|
|
414
188
|
modelDef,
|
|
@@ -420,30 +194,19 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
420
194
|
|
|
421
195
|
await pidRegistry.register(proc.pid);
|
|
422
196
|
|
|
423
|
-
|
|
424
|
-
const DECOMPOSE_TIMEOUT_MS = 300_000; // 5 minutes
|
|
197
|
+
const DECOMPOSE_TIMEOUT_MS = 300_000;
|
|
425
198
|
let timedOut = false;
|
|
426
|
-
const decomposeTimerId = setTimeout(() => {
|
|
427
|
-
timedOut = true;
|
|
428
|
-
try {
|
|
429
|
-
proc.kill("SIGTERM");
|
|
430
|
-
} catch {
|
|
431
|
-
/* already exited */
|
|
432
|
-
}
|
|
433
|
-
setTimeout(() => {
|
|
434
|
-
try {
|
|
435
|
-
proc.kill("SIGKILL");
|
|
436
|
-
} catch {
|
|
437
|
-
/* already exited */
|
|
438
|
-
}
|
|
439
|
-
}, 5000);
|
|
440
|
-
}, DECOMPOSE_TIMEOUT_MS);
|
|
441
199
|
|
|
442
200
|
let exitCode: number;
|
|
443
201
|
try {
|
|
444
|
-
|
|
202
|
+
const timeoutResult = await withProcessTimeout(proc, DECOMPOSE_TIMEOUT_MS, {
|
|
203
|
+
graceMs: 5000,
|
|
204
|
+
onTimeout: () => {
|
|
205
|
+
timedOut = true;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
exitCode = timeoutResult.exitCode;
|
|
445
209
|
} finally {
|
|
446
|
-
clearTimeout(decomposeTimerId);
|
|
447
210
|
await pidRegistry.unregister(proc.pid);
|
|
448
211
|
}
|
|
449
212
|
|
|
@@ -451,12 +214,14 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
451
214
|
throw new Error(`Decompose timed out after ${DECOMPOSE_TIMEOUT_MS / 1000}s`);
|
|
452
215
|
}
|
|
453
216
|
|
|
454
|
-
|
|
455
|
-
// (e.g. hard-deadline fired but process didn't fully die), don't block forever.
|
|
217
|
+
let stdoutTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
456
218
|
const stdout = await Promise.race([
|
|
457
219
|
new Response(proc.stdout).text(),
|
|
458
|
-
new Promise<string>((resolve) =>
|
|
220
|
+
new Promise<string>((resolve) => {
|
|
221
|
+
stdoutTimeoutId = setTimeout(() => resolve(""), 5000);
|
|
222
|
+
}),
|
|
459
223
|
]);
|
|
224
|
+
clearTimeout(stdoutTimeoutId);
|
|
460
225
|
const stderr = await new Response(proc.stderr).text();
|
|
461
226
|
|
|
462
227
|
if (exitCode !== 0) {
|
|
@@ -469,57 +234,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
469
234
|
}
|
|
470
235
|
|
|
471
236
|
runInteractive(options: InteractiveRunOptions): PtyHandle {
|
|
472
|
-
const model = options.modelDef.model;
|
|
473
|
-
const cmd = [this.binary, "--model", model, options.prompt];
|
|
474
|
-
|
|
475
|
-
// BUN-001: Replaced node-pty with Bun.spawn (piped stdio).
|
|
476
|
-
// runInteractive() is TUI-only and currently dormant in headless nax runs.
|
|
477
|
-
// TERM + FORCE_COLOR preserve formatting output from Claude Code.
|
|
478
|
-
const proc = Bun.spawn(cmd, {
|
|
479
|
-
cwd: options.workdir,
|
|
480
|
-
env: { ...this.buildAllowedEnv(options), TERM: "xterm-256color", FORCE_COLOR: "1" },
|
|
481
|
-
stdin: "pipe",
|
|
482
|
-
stdout: "pipe",
|
|
483
|
-
stderr: "inherit", // MEM-3: Inherit stderr to avoid blocking on unread pipe
|
|
484
|
-
});
|
|
485
|
-
|
|
486
237
|
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// Stream stdout to onOutput callback
|
|
490
|
-
(async () => {
|
|
491
|
-
try {
|
|
492
|
-
for await (const chunk of proc.stdout) {
|
|
493
|
-
options.onOutput(Buffer.from(chunk));
|
|
494
|
-
}
|
|
495
|
-
} catch (err) {
|
|
496
|
-
// BUG-21: Handle stream errors to avoid unhandled rejections
|
|
497
|
-
getLogger()?.error("agent", "runInteractive stdout error", { err });
|
|
498
|
-
}
|
|
499
|
-
})();
|
|
500
|
-
|
|
501
|
-
// Fire onExit when process completes
|
|
502
|
-
proc.exited
|
|
503
|
-
.then((code) => {
|
|
504
|
-
pidRegistry.unregister(proc.pid).catch(() => {});
|
|
505
|
-
options.onExit(code ?? 1);
|
|
506
|
-
})
|
|
507
|
-
.catch((err) => {
|
|
508
|
-
// BUG-22: Guard against onExit or unregister throws
|
|
509
|
-
getLogger()?.error("agent", "runInteractive exit error", { err });
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
return {
|
|
513
|
-
write: (data: string) => {
|
|
514
|
-
proc.stdin.write(data);
|
|
515
|
-
},
|
|
516
|
-
resize: (_cols: number, _rows: number) => {
|
|
517
|
-
/* no-op: Bun.spawn has no PTY resize */
|
|
518
|
-
},
|
|
519
|
-
kill: () => {
|
|
520
|
-
proc.kill();
|
|
521
|
-
},
|
|
522
|
-
pid: proc.pid,
|
|
523
|
-
};
|
|
238
|
+
return runInteractiveMode(this.binary, options, pidRegistry);
|
|
524
239
|
}
|
|
525
240
|
}
|
|
@@ -12,6 +12,7 @@ import { resolveModel } from "../config/schema";
|
|
|
12
12
|
import { getLogger } from "../logger";
|
|
13
13
|
import type { UserStory } from "../prd";
|
|
14
14
|
import { classifyComplexity } from "../routing";
|
|
15
|
+
import { errorMessage } from "../utils/errors";
|
|
15
16
|
import type { ClassificationResult, CodebaseScan, StoryClassification } from "./types";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -79,7 +80,7 @@ export async function classifyStories(
|
|
|
79
80
|
};
|
|
80
81
|
} catch (error) {
|
|
81
82
|
// Fall back to keyword matching
|
|
82
|
-
const reason =
|
|
83
|
+
const reason = errorMessage(error);
|
|
83
84
|
const logger = getLogger();
|
|
84
85
|
logger.warn("analyze", "LLM classification failed, falling back to keyword matching", { error: reason });
|
|
85
86
|
|