@nathapp/nax 0.42.4 → 0.42.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/dist/nax.js CHANGED
@@ -3244,7 +3244,10 @@ async function executeComplete(binary, prompt, options) {
3244
3244
  if (options?.jsonMode) {
3245
3245
  cmd.push("--output-format", "json");
3246
3246
  }
3247
- const proc = _completeDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
3247
+ const spawnOpts = { stdout: "pipe", stderr: "pipe" };
3248
+ if (options?.workdir)
3249
+ spawnOpts.cwd = options.workdir;
3250
+ const proc = _completeDeps.spawn(cmd, spawnOpts);
3248
3251
  const exitCode = await proc.exited;
3249
3252
  const stdout = await new Response(proc.stdout).text();
3250
3253
  const stderr = await new Response(proc.stderr).text();
@@ -18960,6 +18963,8 @@ class SpawnAcpSession {
18960
18963
  timeoutSeconds;
18961
18964
  permissionMode;
18962
18965
  env;
18966
+ pidRegistry;
18967
+ activeProc = null;
18963
18968
  constructor(opts) {
18964
18969
  this.agentName = opts.agentName;
18965
18970
  this.sessionName = opts.sessionName;
@@ -18968,6 +18973,7 @@ class SpawnAcpSession {
18968
18973
  this.timeoutSeconds = opts.timeoutSeconds;
18969
18974
  this.permissionMode = opts.permissionMode;
18970
18975
  this.env = opts.env;
18976
+ this.pidRegistry = opts.pidRegistry;
18971
18977
  }
18972
18978
  async prompt(text) {
18973
18979
  const cmd = [
@@ -18994,35 +19000,50 @@ class SpawnAcpSession {
18994
19000
  stderr: "pipe",
18995
19001
  env: this.env
18996
19002
  });
18997
- proc.stdin.write(text);
18998
- proc.stdin.end();
18999
- const exitCode = await proc.exited;
19000
- const stdout = await new Response(proc.stdout).text();
19001
- const stderr = await new Response(proc.stderr).text();
19002
- if (exitCode !== 0) {
19003
- getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
19004
- stderr: stderr.slice(0, 200)
19005
- });
19006
- return {
19007
- messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
19008
- stopReason: "error"
19009
- };
19010
- }
19003
+ this.activeProc = proc;
19004
+ const processPid = proc.pid;
19005
+ await this.pidRegistry?.register(processPid);
19011
19006
  try {
19012
- const parsed = parseAcpxJsonOutput(stdout);
19013
- return {
19014
- messages: [{ role: "assistant", content: parsed.text || "" }],
19015
- stopReason: "end_turn",
19016
- cumulative_token_usage: parsed.tokenUsage
19017
- };
19018
- } catch (err) {
19019
- getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
19020
- stderr: stderr.slice(0, 200)
19021
- });
19022
- throw err;
19007
+ proc.stdin.write(text);
19008
+ proc.stdin.end();
19009
+ const exitCode = await proc.exited;
19010
+ const stdout = await new Response(proc.stdout).text();
19011
+ const stderr = await new Response(proc.stderr).text();
19012
+ if (exitCode !== 0) {
19013
+ getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
19014
+ stderr: stderr.slice(0, 200)
19015
+ });
19016
+ return {
19017
+ messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
19018
+ stopReason: "error"
19019
+ };
19020
+ }
19021
+ try {
19022
+ const parsed = parseAcpxJsonOutput(stdout);
19023
+ return {
19024
+ messages: [{ role: "assistant", content: parsed.text || "" }],
19025
+ stopReason: "end_turn",
19026
+ cumulative_token_usage: parsed.tokenUsage
19027
+ };
19028
+ } catch (err) {
19029
+ getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
19030
+ stderr: stderr.slice(0, 200)
19031
+ });
19032
+ throw err;
19033
+ }
19034
+ } finally {
19035
+ this.activeProc = null;
19036
+ await this.pidRegistry?.unregister(processPid);
19023
19037
  }
19024
19038
  }
19025
19039
  async close() {
19040
+ if (this.activeProc) {
19041
+ try {
19042
+ this.activeProc.kill(15);
19043
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
19044
+ } catch {}
19045
+ this.activeProc = null;
19046
+ }
19026
19047
  const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
19027
19048
  getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
19028
19049
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
@@ -19036,6 +19057,12 @@ class SpawnAcpSession {
19036
19057
  }
19037
19058
  }
19038
19059
  async cancelActivePrompt() {
19060
+ if (this.activeProc) {
19061
+ try {
19062
+ this.activeProc.kill(15);
19063
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
19064
+ } catch {}
19065
+ }
19039
19066
  const cmd = ["acpx", this.agentName, "cancel"];
19040
19067
  getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
19041
19068
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
@@ -19049,20 +19076,26 @@ class SpawnAcpClient {
19049
19076
  cwd;
19050
19077
  timeoutSeconds;
19051
19078
  env;
19052
- constructor(cmdStr, cwd, timeoutSeconds) {
19079
+ pidRegistry;
19080
+ constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
19053
19081
  const parts = cmdStr.split(/\s+/);
19054
19082
  const modelIdx = parts.indexOf("--model");
19055
19083
  this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
19056
- this.agentName = parts[parts.length - 1] || "claude";
19084
+ const lastToken = parts[parts.length - 1];
19085
+ if (!lastToken || lastToken.startsWith("-")) {
19086
+ throw new Error(`[acp-adapter] Could not parse agentName from cmdStr: "${cmdStr}"`);
19087
+ }
19088
+ this.agentName = lastToken;
19057
19089
  this.cwd = cwd || process.cwd();
19058
19090
  this.timeoutSeconds = timeoutSeconds || 1800;
19059
19091
  this.env = buildAllowedEnv2();
19092
+ this.pidRegistry = pidRegistry;
19060
19093
  }
19061
19094
  async start() {}
19062
19095
  async createSession(opts) {
19063
19096
  const sessionName = opts.sessionName || `nax-${Date.now()}`;
19064
- const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "new", "--name", sessionName];
19065
- getSafeLogger()?.debug("acp-adapter", `Creating new session: ${sessionName}`);
19097
+ const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "ensure", "--name", sessionName];
19098
+ getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
19066
19099
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19067
19100
  const exitCode = await proc.exited;
19068
19101
  if (exitCode !== 0) {
@@ -19076,30 +19109,32 @@ class SpawnAcpClient {
19076
19109
  model: this.model,
19077
19110
  timeoutSeconds: this.timeoutSeconds,
19078
19111
  permissionMode: opts.permissionMode,
19079
- env: this.env
19112
+ env: this.env,
19113
+ pidRegistry: this.pidRegistry
19080
19114
  });
19081
19115
  }
19082
- async loadSession(sessionName) {
19083
- const cmd = ["acpx", "--cwd", this.cwd, this.agentName, "sessions", "ensure", "--name", sessionName];
19116
+ async loadSession(sessionName, agentName) {
19117
+ const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
19084
19118
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
19085
19119
  const exitCode = await proc.exited;
19086
19120
  if (exitCode !== 0) {
19087
19121
  return null;
19088
19122
  }
19089
19123
  return new SpawnAcpSession({
19090
- agentName: this.agentName,
19124
+ agentName,
19091
19125
  sessionName,
19092
19126
  cwd: this.cwd,
19093
19127
  model: this.model,
19094
19128
  timeoutSeconds: this.timeoutSeconds,
19095
19129
  permissionMode: "approve-all",
19096
- env: this.env
19130
+ env: this.env,
19131
+ pidRegistry: this.pidRegistry
19097
19132
  });
19098
19133
  }
19099
19134
  async close() {}
19100
19135
  }
19101
- function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds) {
19102
- return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19136
+ function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry) {
19137
+ return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
19103
19138
  }
19104
19139
  var _spawnClientDeps;
19105
19140
  var init_spawn_client = __esm(() => {
@@ -19177,9 +19212,12 @@ function buildSessionName(workdir, featureName, storyId, sessionRole) {
19177
19212
  return parts.join("-");
19178
19213
  }
19179
19214
  async function ensureAcpSession(client, sessionName, agentName, permissionMode) {
19215
+ if (!agentName) {
19216
+ throw new Error("[acp-adapter] agentName is required for ensureAcpSession");
19217
+ }
19180
19218
  if (client.loadSession) {
19181
19219
  try {
19182
- const existing = await client.loadSession(sessionName);
19220
+ const existing = await client.loadSession(sessionName, agentName);
19183
19221
  if (existing) {
19184
19222
  getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
19185
19223
  return existing;
@@ -19357,7 +19395,7 @@ class AcpAgentAdapter {
19357
19395
  }
19358
19396
  async _runWithClient(options, startTime) {
19359
19397
  const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
19360
- const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
19398
+ const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds, options.pidRegistry);
19361
19399
  await client.start();
19362
19400
  let sessionName = options.acpSessionName;
19363
19401
  if (!sessionName && options.featureName && options.storyId) {
@@ -19445,10 +19483,11 @@ class AcpAgentAdapter {
19445
19483
  const model = _options?.model ?? "default";
19446
19484
  const timeoutMs = _options?.timeoutMs ?? 120000;
19447
19485
  const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
19486
+ const workdir = _options?.workdir;
19448
19487
  let lastError;
19449
19488
  for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
19450
19489
  const cmdStr = `acpx --model ${model} ${this.name}`;
19451
- const client = _acpAdapterDeps.createClient(cmdStr);
19490
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir);
19452
19491
  await client.start();
19453
19492
  let session = null;
19454
19493
  try {
@@ -19528,7 +19567,8 @@ class AcpAgentAdapter {
19528
19567
  maxInteractionTurns: options.maxInteractionTurns,
19529
19568
  featureName: options.featureName,
19530
19569
  storyId: options.storyId,
19531
- sessionRole: options.sessionRole
19570
+ sessionRole: options.sessionRole,
19571
+ pidRegistry: options.pidRegistry
19532
19572
  });
19533
19573
  if (!result.success) {
19534
19574
  throw new Error(`[acp-adapter] plan() failed: ${result.output}`);
@@ -19598,8 +19638,8 @@ var init_adapter = __esm(() => {
19598
19638
  async sleep(ms) {
19599
19639
  await Bun.sleep(ms);
19600
19640
  },
19601
- createClient(cmdStr, cwd, timeoutSeconds) {
19602
- return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19641
+ createClient(cmdStr, cwd, timeoutSeconds, pidRegistry) {
19642
+ return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
19603
19643
  }
19604
19644
  };
19605
19645
  });
@@ -21859,7 +21899,7 @@ var package_default;
21859
21899
  var init_package = __esm(() => {
21860
21900
  package_default = {
21861
21901
  name: "@nathapp/nax",
21862
- version: "0.42.4",
21902
+ version: "0.42.6",
21863
21903
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
21864
21904
  type: "module",
21865
21905
  bin: {
@@ -21932,8 +21972,8 @@ var init_version = __esm(() => {
21932
21972
  NAX_VERSION = package_default.version;
21933
21973
  NAX_COMMIT = (() => {
21934
21974
  try {
21935
- if (/^[0-9a-f]{6,10}$/.test("35ef278"))
21936
- return "35ef278";
21975
+ if (/^[0-9a-f]{6,10}$/.test("deb8333"))
21976
+ return "deb8333";
21937
21977
  } catch {}
21938
21978
  try {
21939
21979
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -65591,6 +65631,7 @@ init_registry();
65591
65631
  import { existsSync as existsSync9 } from "fs";
65592
65632
  import { join as join10 } from "path";
65593
65633
  import { createInterface } from "readline";
65634
+ init_pid_registry();
65594
65635
  init_logger2();
65595
65636
 
65596
65637
  // src/prd/schema.ts
@@ -65781,7 +65822,7 @@ async function planCommand(workdir, config2, options) {
65781
65822
  const cliAdapter = _deps2.getAgent(agentName);
65782
65823
  if (!cliAdapter)
65783
65824
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
65784
- rawResponse = await cliAdapter.complete(prompt, { jsonMode: true });
65825
+ rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir });
65785
65826
  try {
65786
65827
  const envelope = JSON.parse(rawResponse);
65787
65828
  if (envelope?.type === "result" && typeof envelope?.result === "string") {
@@ -65794,6 +65835,7 @@ async function planCommand(workdir, config2, options) {
65794
65835
  if (!adapter)
65795
65836
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
65796
65837
  const interactionBridge = createCliInteractionBridge();
65838
+ const pidRegistry = new PidRegistry(workdir);
65797
65839
  logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
65798
65840
  try {
65799
65841
  await adapter.plan({
@@ -65805,9 +65847,12 @@ async function planCommand(workdir, config2, options) {
65805
65847
  config: config2,
65806
65848
  modelTier: config2?.plan?.model ?? "balanced",
65807
65849
  dangerouslySkipPermissions: config2?.execution?.dangerouslySkipPermissions ?? false,
65808
- maxInteractionTurns: config2?.agent?.maxInteractionTurns
65850
+ maxInteractionTurns: config2?.agent?.maxInteractionTurns,
65851
+ featureName: options.feature,
65852
+ pidRegistry
65809
65853
  });
65810
65854
  } finally {
65855
+ await pidRegistry.killAll().catch(() => {});
65811
65856
  logger?.info("plan", "Interactive session ended");
65812
65857
  }
65813
65858
  if (!_deps2.existsSync(outputPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.42.4",
3
+ "version": "0.42.6",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -92,7 +92,7 @@ export interface AcpClient {
92
92
  start(): Promise<void>;
93
93
  createSession(opts: { agentName: string; permissionMode: string; sessionName?: string }): Promise<AcpSession>;
94
94
  /** Resume an existing named session. Returns null if the session is not found. */
95
- loadSession?(sessionName: string): Promise<AcpSession | null>;
95
+ loadSession?(sessionName: string, agentName: string): Promise<AcpSession | null>;
96
96
  close(): Promise<void>;
97
97
  }
98
98
 
@@ -114,8 +114,13 @@ export const _acpAdapterDeps = {
114
114
  * Default: spawn-based client (shells out to acpx CLI).
115
115
  * Override in tests via: _acpAdapterDeps.createClient = mock(...)
116
116
  */
117
- createClient(cmdStr: string, cwd?: string, timeoutSeconds?: number): AcpClient {
118
- return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds);
117
+ createClient(
118
+ cmdStr: string,
119
+ cwd?: string,
120
+ timeoutSeconds?: number,
121
+ pidRegistry?: import("../../execution/pid-registry").PidRegistry,
122
+ ): AcpClient {
123
+ return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
119
124
  },
120
125
  };
121
126
 
@@ -180,10 +185,14 @@ export async function ensureAcpSession(
180
185
  agentName: string,
181
186
  permissionMode: string,
182
187
  ): Promise<AcpSession> {
188
+ if (!agentName) {
189
+ throw new Error("[acp-adapter] agentName is required for ensureAcpSession");
190
+ }
191
+
183
192
  // Try to resume existing session first
184
193
  if (client.loadSession) {
185
194
  try {
186
- const existing = await client.loadSession(sessionName);
195
+ const existing = await client.loadSession(sessionName, agentName);
187
196
  if (existing) {
188
197
  getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
189
198
  return existing;
@@ -432,7 +441,7 @@ export class AcpAgentAdapter implements AgentAdapter {
432
441
 
433
442
  private async _runWithClient(options: AgentRunOptions, startTime: number): Promise<AgentResult> {
434
443
  const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
435
- const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
444
+ const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds, options.pidRegistry);
436
445
  await client.start();
437
446
 
438
447
  // 1. Resolve session name: explicit > sidecar > derived
@@ -554,12 +563,13 @@ export class AcpAgentAdapter implements AgentAdapter {
554
563
  const model = _options?.model ?? "default";
555
564
  const timeoutMs = _options?.timeoutMs ?? 120_000; // 2-min safety net by default
556
565
  const permissionMode = _options?.dangerouslySkipPermissions ? "approve-all" : "default";
566
+ const workdir = _options?.workdir;
557
567
 
558
568
  let lastError: Error | undefined;
559
569
 
560
570
  for (let attempt = 0; attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
561
571
  const cmdStr = `acpx --model ${model} ${this.name}`;
562
- const client = _acpAdapterDeps.createClient(cmdStr);
572
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir);
563
573
  await client.start();
564
574
 
565
575
  let session: AcpSession | null = null;
@@ -668,6 +678,7 @@ export class AcpAgentAdapter implements AgentAdapter {
668
678
  featureName: options.featureName,
669
679
  storyId: options.storyId,
670
680
  sessionRole: options.sessionRole,
681
+ pidRegistry: options.pidRegistry,
671
682
  });
672
683
 
673
684
  if (!result.success) {
@@ -12,6 +12,7 @@
12
12
  * acpx <agent> cancel → session.cancelActivePrompt()
13
13
  */
14
14
 
15
+ import type { PidRegistry } from "../../execution/pid-registry";
15
16
  import { getSafeLogger } from "../../logger";
16
17
  import type { AcpClient, AcpSession, AcpSessionResponse } from "./adapter";
17
18
  import { parseAcpxJsonOutput } from "./parser";
@@ -96,6 +97,8 @@ class SpawnAcpSession implements AcpSession {
96
97
  private readonly timeoutSeconds: number;
97
98
  private readonly permissionMode: string;
98
99
  private readonly env: Record<string, string | undefined>;
100
+ private readonly pidRegistry?: PidRegistry;
101
+ private activeProc: { pid: number; kill(signal?: number): void } | null = null;
99
102
 
100
103
  constructor(opts: {
101
104
  agentName: string;
@@ -105,6 +108,7 @@ class SpawnAcpSession implements AcpSession {
105
108
  timeoutSeconds: number;
106
109
  permissionMode: string;
107
110
  env: Record<string, string | undefined>;
111
+ pidRegistry?: PidRegistry;
108
112
  }) {
109
113
  this.agentName = opts.agentName;
110
114
  this.sessionName = opts.sessionName;
@@ -113,6 +117,7 @@ class SpawnAcpSession implements AcpSession {
113
117
  this.timeoutSeconds = opts.timeoutSeconds;
114
118
  this.permissionMode = opts.permissionMode;
115
119
  this.env = opts.env;
120
+ this.pidRegistry = opts.pidRegistry;
116
121
  }
117
122
 
118
123
  async prompt(text: string): Promise<AcpSessionResponse> {
@@ -143,40 +148,60 @@ class SpawnAcpSession implements AcpSession {
143
148
  env: this.env,
144
149
  });
145
150
 
146
- proc.stdin.write(text);
147
- proc.stdin.end();
151
+ this.activeProc = proc;
152
+ const processPid = proc.pid;
153
+ await this.pidRegistry?.register(processPid);
148
154
 
149
- const exitCode = await proc.exited;
150
- const stdout = await new Response(proc.stdout).text();
151
- const stderr = await new Response(proc.stderr).text();
155
+ try {
156
+ proc.stdin.write(text);
157
+ proc.stdin.end();
152
158
 
153
- if (exitCode !== 0) {
154
- getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
155
- stderr: stderr.slice(0, 200),
156
- });
157
- // Return error response so the adapter can handle it
158
- return {
159
- messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
160
- stopReason: "error",
161
- };
162
- }
159
+ const exitCode = await proc.exited;
160
+ const stdout = await new Response(proc.stdout).text();
161
+ const stderr = await new Response(proc.stderr).text();
163
162
 
164
- try {
165
- const parsed = parseAcpxJsonOutput(stdout);
166
- return {
167
- messages: [{ role: "assistant", content: parsed.text || "" }],
168
- stopReason: "end_turn",
169
- cumulative_token_usage: parsed.tokenUsage,
170
- };
171
- } catch (err) {
172
- getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
173
- stderr: stderr.slice(0, 200),
174
- });
175
- throw err;
163
+ if (exitCode !== 0) {
164
+ getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
165
+ stderr: stderr.slice(0, 200),
166
+ });
167
+ // Return error response so the adapter can handle it
168
+ return {
169
+ messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
170
+ stopReason: "error",
171
+ };
172
+ }
173
+
174
+ try {
175
+ const parsed = parseAcpxJsonOutput(stdout);
176
+ return {
177
+ messages: [{ role: "assistant", content: parsed.text || "" }],
178
+ stopReason: "end_turn",
179
+ cumulative_token_usage: parsed.tokenUsage,
180
+ };
181
+ } catch (err) {
182
+ getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
183
+ stderr: stderr.slice(0, 200),
184
+ });
185
+ throw err;
186
+ }
187
+ } finally {
188
+ this.activeProc = null;
189
+ await this.pidRegistry?.unregister(processPid);
176
190
  }
177
191
  }
178
192
 
179
193
  async close(): Promise<void> {
194
+ // Kill in-flight prompt process first (if any)
195
+ if (this.activeProc) {
196
+ try {
197
+ this.activeProc.kill(15); // SIGTERM
198
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
199
+ } catch {
200
+ // Process may have already exited
201
+ }
202
+ this.activeProc = null;
203
+ }
204
+
180
205
  const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
181
206
  getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
182
207
 
@@ -193,6 +218,16 @@ class SpawnAcpSession implements AcpSession {
193
218
  }
194
219
 
195
220
  async cancelActivePrompt(): Promise<void> {
221
+ // Kill in-flight prompt process directly (faster than acpx cancel)
222
+ if (this.activeProc) {
223
+ try {
224
+ this.activeProc.kill(15); // SIGTERM
225
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
226
+ } catch {
227
+ // Process may have already exited
228
+ }
229
+ }
230
+
196
231
  const cmd = ["acpx", this.agentName, "cancel"];
197
232
  getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
198
233
 
@@ -211,7 +246,7 @@ class SpawnAcpSession implements AcpSession {
211
246
  * The cmdStr is parsed to extract --model and agent name:
212
247
  * "acpx --model claude-sonnet-4-5 claude" → model=claude-sonnet-4-5, agent=claude
213
248
  *
214
- * createSession() spawns: acpx --cwd <dir> <agent> sessions new --name <name>
249
+ * createSession() spawns: acpx <agent> sessions ensure --name <name>
215
250
  * loadSession() tries to resume an existing named session.
216
251
  */
217
252
  export class SpawnAcpClient implements AcpClient {
@@ -220,17 +255,23 @@ export class SpawnAcpClient implements AcpClient {
220
255
  private readonly cwd: string;
221
256
  private readonly timeoutSeconds: number;
222
257
  private readonly env: Record<string, string | undefined>;
258
+ private readonly pidRegistry?: PidRegistry;
223
259
 
224
- constructor(cmdStr: string, cwd?: string, timeoutSeconds?: number) {
260
+ constructor(cmdStr: string, cwd?: string, timeoutSeconds?: number, pidRegistry?: PidRegistry) {
225
261
  // Parse: "acpx --model <model> <agentName>"
226
262
  const parts = cmdStr.split(/\s+/);
227
263
  const modelIdx = parts.indexOf("--model");
228
264
  this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
229
- // Agent name is the last non-flag token
230
- this.agentName = parts[parts.length - 1] || "claude";
265
+ // Agent name is the last non-flag token — must be present and not a flag
266
+ const lastToken = parts[parts.length - 1];
267
+ if (!lastToken || lastToken.startsWith("-")) {
268
+ throw new Error(`[acp-adapter] Could not parse agentName from cmdStr: "${cmdStr}"`);
269
+ }
270
+ this.agentName = lastToken;
231
271
  this.cwd = cwd || process.cwd();
232
272
  this.timeoutSeconds = timeoutSeconds || 1800;
233
273
  this.env = buildAllowedEnv();
274
+ this.pidRegistry = pidRegistry;
234
275
  }
235
276
 
236
277
  async start(): Promise<void> {
@@ -244,9 +285,9 @@ export class SpawnAcpClient implements AcpClient {
244
285
  }): Promise<AcpSession> {
245
286
  const sessionName = opts.sessionName || `nax-${Date.now()}`;
246
287
 
247
- // Create new session via CLI (sessions new = always creates; sessions ensure = lookup only)
248
- const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "new", "--name", sessionName];
249
- getSafeLogger()?.debug("acp-adapter", `Creating new session: ${sessionName}`);
288
+ // Ensure session exists via CLI
289
+ const cmd = ["acpx", "--cwd", this.cwd, opts.agentName, "sessions", "ensure", "--name", sessionName];
290
+ getSafeLogger()?.debug("acp-adapter", `Ensuring session: ${sessionName}`);
250
291
 
251
292
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
252
293
  const exitCode = await proc.exited;
@@ -264,12 +305,13 @@ export class SpawnAcpClient implements AcpClient {
264
305
  timeoutSeconds: this.timeoutSeconds,
265
306
  permissionMode: opts.permissionMode,
266
307
  env: this.env,
308
+ pidRegistry: this.pidRegistry,
267
309
  });
268
310
  }
269
311
 
270
- async loadSession(sessionName: string): Promise<AcpSession | null> {
312
+ async loadSession(sessionName: string, agentName: string): Promise<AcpSession | null> {
271
313
  // Try to ensure session exists — if it does, acpx returns success
272
- const cmd = ["acpx", "--cwd", this.cwd, this.agentName, "sessions", "ensure", "--name", sessionName];
314
+ const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
273
315
 
274
316
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
275
317
  const exitCode = await proc.exited;
@@ -279,13 +321,14 @@ export class SpawnAcpClient implements AcpClient {
279
321
  }
280
322
 
281
323
  return new SpawnAcpSession({
282
- agentName: this.agentName,
324
+ agentName,
283
325
  sessionName,
284
326
  cwd: this.cwd,
285
327
  model: this.model,
286
328
  timeoutSeconds: this.timeoutSeconds,
287
329
  permissionMode: "approve-all", // Default for resumed sessions
288
330
  env: this.env,
331
+ pidRegistry: this.pidRegistry,
289
332
  });
290
333
  }
291
334
 
@@ -302,6 +345,11 @@ export class SpawnAcpClient implements AcpClient {
302
345
  * Create a spawn-based ACP client. This is the default production factory.
303
346
  * The cmdStr format is: "acpx --model <model> <agentName>"
304
347
  */
305
- export function createSpawnAcpClient(cmdStr: string, cwd?: string, timeoutSeconds?: number): AcpClient {
306
- return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
348
+ export function createSpawnAcpClient(
349
+ cmdStr: string,
350
+ cwd?: string,
351
+ timeoutSeconds?: number,
352
+ pidRegistry?: PidRegistry,
353
+ ): AcpClient {
354
+ return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
307
355
  }
@@ -51,7 +51,9 @@ export async function executeComplete(binary: string, prompt: string, options?:
51
51
  cmd.push("--output-format", "json");
52
52
  }
53
53
 
54
- const proc = _completeDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
54
+ const spawnOpts: { stdout: "pipe"; stderr: "pipe"; cwd?: string } = { stdout: "pipe", stderr: "pipe" };
55
+ if (options?.workdir) spawnOpts.cwd = options.workdir;
56
+ const proc = _completeDeps.spawn(cmd, spawnOpts);
55
57
  const exitCode = await proc.exited;
56
58
 
57
59
  const stdout = await new Response(proc.stdout).text();
@@ -55,6 +55,8 @@ export interface PlanOptions {
55
55
  * Used to persist the name to status.json for plan→run session continuity.
56
56
  */
57
57
  onAcpSessionCreated?: (sessionName: string) => Promise<void> | void;
58
+ /** PID registry for tracking spawned agent processes — cleanup on crash/SIGTERM */
59
+ pidRegistry?: import("../execution/pid-registry").PidRegistry;
58
60
  }
59
61
 
60
62
  /**
@@ -102,6 +102,12 @@ export interface CompleteOptions {
102
102
  model?: string;
103
103
  /** Whether to skip permission prompts (maps to permissionMode in ACP) */
104
104
  dangerouslySkipPermissions?: boolean;
105
+ /**
106
+ * Working directory for the completion call.
107
+ * Used by ACP adapter to set --cwd on the spawned acpx session.
108
+ * CLI adapter uses this as the process cwd when spawning the agent binary.
109
+ */
110
+ workdir?: string;
105
111
  /**
106
112
  * Timeout for the completion call in milliseconds.
107
113
  * Adapters that support it (e.g. ACP) will enforce this as a hard deadline.
package/src/cli/plan.ts CHANGED
@@ -15,6 +15,7 @@ import type { AgentAdapter } from "../agents/types";
15
15
  import { scanCodebase } from "../analyze/scanner";
16
16
  import type { CodebaseScan } from "../analyze/types";
17
17
  import type { NaxConfig } from "../config";
18
+ import { PidRegistry } from "../execution/pid-registry";
18
19
  import { getLogger } from "../logger";
19
20
  import { validatePlanOutput } from "../prd/schema";
20
21
 
@@ -107,7 +108,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
107
108
  const prompt = buildPlanningPrompt(specContent, codebaseContext);
108
109
  const cliAdapter = _deps.getAgent(agentName);
109
110
  if (!cliAdapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
110
- rawResponse = await cliAdapter.complete(prompt, { jsonMode: true });
111
+ rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir });
111
112
  // CLI adapter returns {"type":"result","result":"..."} envelope — unwrap it
112
113
  try {
113
114
  const envelope = JSON.parse(rawResponse) as Record<string, unknown>;
@@ -123,6 +124,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
123
124
  const adapter = _deps.getAgent(agentName, config);
124
125
  if (!adapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
125
126
  const interactionBridge = createCliInteractionBridge();
127
+ const pidRegistry = new PidRegistry(workdir);
126
128
  logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
127
129
  try {
128
130
  await adapter.plan({
@@ -135,8 +137,11 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
135
137
  modelTier: config?.plan?.model ?? "balanced",
136
138
  dangerouslySkipPermissions: config?.execution?.dangerouslySkipPermissions ?? false,
137
139
  maxInteractionTurns: config?.agent?.maxInteractionTurns,
140
+ featureName: options.feature,
141
+ pidRegistry,
138
142
  });
139
143
  } finally {
144
+ await pidRegistry.killAll().catch(() => {});
140
145
  logger?.info("plan", "Interactive session ended");
141
146
  }
142
147
  // Read back from file written by agent