@nathapp/nax 0.38.0 → 0.38.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/dist/nax.js +3294 -2907
- package/package.json +2 -2
- 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/plugins.ts +15 -4
- 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 +451 -0
- package/src/config/schema-types.ts +53 -0
- package/src/config/schemas.ts +2 -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/lifecycle/run-setup.ts +7 -1
- 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 +40 -4
- package/src/plugins/types.ts +18 -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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax",
|
|
3
|
-
"version": "0.38.
|
|
4
|
-
"description": "AI Coding Agent Orchestrator
|
|
3
|
+
"version": "0.38.2",
|
|
4
|
+
"description": "AI Coding Agent Orchestrator — loops until done",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nax": "./dist/nax.js"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Agent - Completion API
|
|
3
|
+
*
|
|
4
|
+
* Standalone completion endpoint for simple prompts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CompleteOptions } from "./types";
|
|
8
|
+
import { CompleteError } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Injectable dependencies for complete() — allows tests to intercept
|
|
12
|
+
* Bun.spawn calls and verify correct CLI args without the claude binary.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export const _completeDeps = {
|
|
17
|
+
spawn(
|
|
18
|
+
cmd: string[],
|
|
19
|
+
opts: { stdout: "pipe"; stderr: "pipe" | "inherit" },
|
|
20
|
+
): { stdout: ReadableStream<Uint8Array>; stderr: ReadableStream<Uint8Array>; exited: Promise<number>; pid: number } {
|
|
21
|
+
return Bun.spawn(cmd, opts) as unknown as {
|
|
22
|
+
stdout: ReadableStream<Uint8Array>;
|
|
23
|
+
stderr: ReadableStream<Uint8Array>;
|
|
24
|
+
exited: Promise<number>;
|
|
25
|
+
pid: number;
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute a simple completion request without starting a full agent session.
|
|
32
|
+
*
|
|
33
|
+
* @param binary - Path to claude binary
|
|
34
|
+
* @param prompt - Prompt text
|
|
35
|
+
* @param options - Completion options (model, tokens, format)
|
|
36
|
+
* @returns Completion text output
|
|
37
|
+
* @throws CompleteError if execution fails
|
|
38
|
+
*/
|
|
39
|
+
export async function executeComplete(binary: string, prompt: string, options?: CompleteOptions): Promise<string> {
|
|
40
|
+
const cmd = [binary, "-p", prompt];
|
|
41
|
+
|
|
42
|
+
if (options?.model) {
|
|
43
|
+
cmd.push("--model", options.model);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options?.maxTokens !== undefined) {
|
|
47
|
+
cmd.push("--max-tokens", String(options.maxTokens));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (options?.jsonMode) {
|
|
51
|
+
cmd.push("--output-format", "json");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const proc = _completeDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
55
|
+
const exitCode = await proc.exited;
|
|
56
|
+
|
|
57
|
+
const stdout = await new Response(proc.stdout).text();
|
|
58
|
+
const stderr = await new Response(proc.stderr).text();
|
|
59
|
+
const trimmed = stdout.trim();
|
|
60
|
+
|
|
61
|
+
if (exitCode !== 0) {
|
|
62
|
+
const errorDetails = stderr.trim() || trimmed;
|
|
63
|
+
const errorMessage = errorDetails || `complete() failed with exit code ${exitCode}`;
|
|
64
|
+
throw new CompleteError(errorMessage, exitCode);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!trimmed) {
|
|
68
|
+
throw new CompleteError("complete() returned empty output");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Agent - Execution Layer
|
|
3
|
+
*
|
|
4
|
+
* Handles building commands, preparing environment, and process execution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PidRegistry } from "../execution/pid-registry";
|
|
8
|
+
import { withProcessTimeout } from "../execution/timeout-handler";
|
|
9
|
+
import { getLogger } from "../logger";
|
|
10
|
+
import { estimateCostByDuration, estimateCostFromOutput } from "./cost";
|
|
11
|
+
import type { AgentResult, AgentRunOptions } from "./types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maximum characters to capture from agent stdout.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Maximum characters to capture from agent stderr.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_AGENT_STDERR_CHARS = 1000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Grace period in ms between SIGTERM and SIGKILL on timeout.
|
|
25
|
+
*/
|
|
26
|
+
const SIGKILL_GRACE_PERIOD_MS = 5000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Injectable dependencies for runOnce() — allows tests to verify
|
|
30
|
+
* that PID cleanup (unregister) always runs even if kill() throws.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export const _runOnceDeps = {
|
|
35
|
+
killProc(proc: { kill(signal?: number | NodeJS.Signals): void }, signal: NodeJS.Signals): void {
|
|
36
|
+
proc.kill(signal);
|
|
37
|
+
},
|
|
38
|
+
buildCmd(binary: string, options: AgentRunOptions): string[] {
|
|
39
|
+
return buildCommand(binary, options);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build Claude Code command with model and permissions.
|
|
45
|
+
*
|
|
46
|
+
* @param binary - Path to claude binary
|
|
47
|
+
* @param options - Agent run options
|
|
48
|
+
* @returns Command array for Bun.spawn()
|
|
49
|
+
*/
|
|
50
|
+
export function buildCommand(binary: string, options: AgentRunOptions): string[] {
|
|
51
|
+
const model = options.modelDef.model;
|
|
52
|
+
const skipPermissions = options.dangerouslySkipPermissions ?? true;
|
|
53
|
+
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
54
|
+
return [binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build allowed environment variables for spawned agents.
|
|
59
|
+
* SEC-4: Only pass essential env vars to prevent leaking sensitive data.
|
|
60
|
+
*
|
|
61
|
+
* @param options - Agent run options
|
|
62
|
+
* @returns Filtered environment variables
|
|
63
|
+
*/
|
|
64
|
+
export function buildAllowedEnv(options: AgentRunOptions): Record<string, string | undefined> {
|
|
65
|
+
const allowed: Record<string, string | undefined> = {};
|
|
66
|
+
|
|
67
|
+
const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
68
|
+
for (const varName of essentialVars) {
|
|
69
|
+
if (process.env[varName]) {
|
|
70
|
+
allowed[varName] = process.env[varName];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
75
|
+
for (const varName of apiKeyVars) {
|
|
76
|
+
if (process.env[varName]) {
|
|
77
|
+
allowed[varName] = process.env[varName];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_"];
|
|
82
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
83
|
+
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
84
|
+
allowed[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (options.modelDef.env) {
|
|
89
|
+
Object.assign(allowed, options.modelDef.env);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (options.env) {
|
|
93
|
+
Object.assign(allowed, options.env);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return allowed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Execute agent process once with timeout and signal handling.
|
|
101
|
+
*
|
|
102
|
+
* @param binary - Path to claude binary
|
|
103
|
+
* @param options - Agent run options
|
|
104
|
+
* @param pidRegistry - PID registry for cleanup
|
|
105
|
+
* @returns Agent execution result
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export async function executeOnce(
|
|
110
|
+
binary: string,
|
|
111
|
+
options: AgentRunOptions,
|
|
112
|
+
pidRegistry: PidRegistry,
|
|
113
|
+
): Promise<AgentResult> {
|
|
114
|
+
const cmd = _runOnceDeps.buildCmd(binary, options);
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
|
|
117
|
+
const proc = Bun.spawn(cmd, {
|
|
118
|
+
cwd: options.workdir,
|
|
119
|
+
stdout: "pipe",
|
|
120
|
+
stderr: "inherit",
|
|
121
|
+
env: buildAllowedEnv(options),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const processPid = proc.pid;
|
|
125
|
+
await pidRegistry.register(processPid);
|
|
126
|
+
|
|
127
|
+
let timedOut = false;
|
|
128
|
+
let exitCode: number;
|
|
129
|
+
try {
|
|
130
|
+
const timeoutResult = await withProcessTimeout(proc, options.timeoutSeconds * 1000, {
|
|
131
|
+
graceMs: SIGKILL_GRACE_PERIOD_MS,
|
|
132
|
+
onTimeout: () => {
|
|
133
|
+
timedOut = true;
|
|
134
|
+
},
|
|
135
|
+
killFn: (p, signal) => _runOnceDeps.killProc(p, signal),
|
|
136
|
+
});
|
|
137
|
+
exitCode = timeoutResult.exitCode;
|
|
138
|
+
timedOut = timeoutResult.timedOut;
|
|
139
|
+
} finally {
|
|
140
|
+
await pidRegistry.unregister(processPid);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let stdoutTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
144
|
+
const stdout = await Promise.race([
|
|
145
|
+
new Response(proc.stdout).text(),
|
|
146
|
+
new Promise<string>((resolve) => {
|
|
147
|
+
stdoutTimeoutId = setTimeout(() => resolve(""), 5000);
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
clearTimeout(stdoutTimeoutId); // prevent leaked timer when stdout resolves first
|
|
151
|
+
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
152
|
+
const durationMs = Date.now() - startTime;
|
|
153
|
+
|
|
154
|
+
const fullOutput = stdout + stderr;
|
|
155
|
+
const rateLimited =
|
|
156
|
+
fullOutput.toLowerCase().includes("rate limit") ||
|
|
157
|
+
fullOutput.includes("429") ||
|
|
158
|
+
fullOutput.toLowerCase().includes("too many requests");
|
|
159
|
+
|
|
160
|
+
let costEstimate = estimateCostFromOutput(options.modelTier, fullOutput);
|
|
161
|
+
const logger = getLogger();
|
|
162
|
+
if (!costEstimate) {
|
|
163
|
+
const fallbackEstimate = estimateCostByDuration(options.modelTier, durationMs);
|
|
164
|
+
costEstimate = {
|
|
165
|
+
cost: fallbackEstimate.cost * 1.5,
|
|
166
|
+
confidence: "fallback",
|
|
167
|
+
};
|
|
168
|
+
logger.warn("agent", "Cost estimation fallback (duration-based)", {
|
|
169
|
+
modelTier: options.modelTier,
|
|
170
|
+
cost: costEstimate.cost,
|
|
171
|
+
});
|
|
172
|
+
} else if (costEstimate.confidence === "estimated") {
|
|
173
|
+
logger.warn("agent", "Cost estimation using regex parsing (estimated confidence)", { cost: costEstimate.cost });
|
|
174
|
+
}
|
|
175
|
+
const cost = costEstimate.cost;
|
|
176
|
+
|
|
177
|
+
const actualExitCode = timedOut ? 124 : exitCode;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: exitCode === 0 && !timedOut,
|
|
181
|
+
exitCode: actualExitCode,
|
|
182
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
183
|
+
stderr: stderr.slice(-MAX_AGENT_STDERR_CHARS),
|
|
184
|
+
rateLimited,
|
|
185
|
+
durationMs,
|
|
186
|
+
estimatedCost: cost,
|
|
187
|
+
pid: processPid,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Agent - Interactive (TUI) Mode
|
|
3
|
+
*
|
|
4
|
+
* Handles terminal UI interactions with the Claude agent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PidRegistry } from "../execution/pid-registry";
|
|
8
|
+
import { getLogger } from "../logger";
|
|
9
|
+
import { buildAllowedEnv } from "./claude-execution";
|
|
10
|
+
import type { AgentRunOptions, InteractiveRunOptions, PtyHandle } from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run Claude agent in interactive (TTY) mode for TUI output.
|
|
14
|
+
*
|
|
15
|
+
* @param binary - Path to claude binary
|
|
16
|
+
* @param options - Interactive run options
|
|
17
|
+
* @param pidRegistry - PID registry for cleanup
|
|
18
|
+
* @returns PTY handle for stdin/stdout/kill control
|
|
19
|
+
*/
|
|
20
|
+
export function runInteractiveMode(
|
|
21
|
+
binary: string,
|
|
22
|
+
options: InteractiveRunOptions,
|
|
23
|
+
pidRegistry: PidRegistry,
|
|
24
|
+
): PtyHandle {
|
|
25
|
+
const model = options.modelDef.model;
|
|
26
|
+
const cmd = [binary, "--model", model, options.prompt];
|
|
27
|
+
|
|
28
|
+
// BUN-001: Replaced node-pty with Bun.spawn (piped stdio).
|
|
29
|
+
// runInteractive() is TUI-only and currently dormant in headless nax runs.
|
|
30
|
+
// TERM + FORCE_COLOR preserve formatting output from Claude Code.
|
|
31
|
+
const allowedEnv = buildAllowedEnv(options as unknown as AgentRunOptions);
|
|
32
|
+
const proc = Bun.spawn(cmd, {
|
|
33
|
+
cwd: options.workdir,
|
|
34
|
+
env: { ...allowedEnv, TERM: "xterm-256color", FORCE_COLOR: "1" },
|
|
35
|
+
stdin: "pipe",
|
|
36
|
+
stdout: "pipe",
|
|
37
|
+
stderr: "inherit",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
pidRegistry.register(proc.pid).catch(() => {});
|
|
41
|
+
|
|
42
|
+
// Stream stdout to onOutput callback
|
|
43
|
+
(async () => {
|
|
44
|
+
try {
|
|
45
|
+
for await (const chunk of proc.stdout) {
|
|
46
|
+
options.onOutput(Buffer.from(chunk));
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// BUG-21: Handle stream errors to avoid unhandled rejections
|
|
50
|
+
getLogger()?.error("agent", "runInteractive stdout error", { err });
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
// Fire onExit when process completes
|
|
55
|
+
proc.exited
|
|
56
|
+
.then((code) => {
|
|
57
|
+
pidRegistry.unregister(proc.pid).catch(() => {});
|
|
58
|
+
options.onExit(code ?? 1);
|
|
59
|
+
})
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
// BUG-22: Guard against onExit or unregister throws
|
|
62
|
+
getLogger()?.error("agent", "runInteractive exit error", { err });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
write: (data: string) => {
|
|
67
|
+
proc.stdin.write(data);
|
|
68
|
+
},
|
|
69
|
+
resize: (_cols: number, _rows: number) => {
|
|
70
|
+
/* no-op: Bun.spawn has no PTY resize */
|
|
71
|
+
},
|
|
72
|
+
kill: () => {
|
|
73
|
+
proc.kill();
|
|
74
|
+
},
|
|
75
|
+
pid: proc.pid,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -8,6 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { PidRegistry } from "../execution/pid-registry";
|
|
11
|
+
import { withProcessTimeout } from "../execution/timeout-handler";
|
|
11
12
|
import { getLogger } from "../logger";
|
|
12
13
|
import { resolveBalancedModelDef } from "./model-resolution";
|
|
13
14
|
import type { AgentRunOptions } from "./types";
|
|
@@ -91,6 +92,8 @@ export async function runPlan(
|
|
|
91
92
|
timeoutSeconds: 600,
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
const PLAN_TIMEOUT_MS = 600_000; // 10 minutes
|
|
96
|
+
|
|
94
97
|
if (options.interactive) {
|
|
95
98
|
// Interactive mode: inherit stdio
|
|
96
99
|
const proc = Bun.spawn(cmd, {
|
|
@@ -104,10 +107,16 @@ export async function runPlan(
|
|
|
104
107
|
// Register PID
|
|
105
108
|
await pidRegistry.register(proc.pid);
|
|
106
109
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
let exitCode: number;
|
|
111
|
+
try {
|
|
112
|
+
const timeoutResult = await withProcessTimeout(proc, PLAN_TIMEOUT_MS, {
|
|
113
|
+
graceMs: 5000,
|
|
114
|
+
});
|
|
115
|
+
exitCode = timeoutResult.exitCode;
|
|
116
|
+
} finally {
|
|
117
|
+
// Unregister PID after exit
|
|
118
|
+
await pidRegistry.unregister(proc.pid);
|
|
119
|
+
}
|
|
111
120
|
|
|
112
121
|
if (exitCode !== 0) {
|
|
113
122
|
throw new Error(`Plan mode failed with exit code ${exitCode}`);
|
|
@@ -133,10 +142,16 @@ export async function runPlan(
|
|
|
133
142
|
// Register PID
|
|
134
143
|
await pidRegistry.register(proc.pid);
|
|
135
144
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
let exitCode: number;
|
|
146
|
+
try {
|
|
147
|
+
const timeoutResult = await withProcessTimeout(proc, PLAN_TIMEOUT_MS, {
|
|
148
|
+
graceMs: 5000,
|
|
149
|
+
});
|
|
150
|
+
exitCode = timeoutResult.exitCode;
|
|
151
|
+
} finally {
|
|
152
|
+
// Unregister PID after exit
|
|
153
|
+
await pidRegistry.unregister(proc.pid);
|
|
154
|
+
}
|
|
140
155
|
|
|
141
156
|
const specContent = await Bun.file(outFile).text();
|
|
142
157
|
const conversationLog = await Bun.file(errFile).text();
|