@nathapp/nax 0.48.1 → 0.48.3

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": "@nathapp/nax",
3
- "version": "0.48.1",
3
+ "version": "0.48.3",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -218,7 +218,7 @@ class SpawnAcpSession implements AcpSession {
218
218
  this.activeProc = null;
219
219
  }
220
220
 
221
- const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
221
+ const cmd = ["acpx", "--cwd", this.cwd, this.agentName, "sessions", "close", this.sessionName];
222
222
  getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
223
223
 
224
224
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
@@ -67,17 +67,19 @@ export async function generateCommand(options: GenerateCommandOptions): Promise<
67
67
  return;
68
68
  }
69
69
 
70
- console.log(chalk.blue(`→ Generating CLAUDE.md for ${packages.length} package(s)...`));
70
+ console.log(chalk.blue(`→ Generating agent files for ${packages.length} package(s)...`));
71
71
  let errorCount = 0;
72
72
 
73
73
  for (const pkgDir of packages) {
74
- const result = await generateForPackage(pkgDir, config, dryRun);
75
- if (result.error) {
76
- console.error(chalk.red(`✗ ${pkgDir}: ${result.error}`));
77
- errorCount++;
78
- } else {
79
- const suffix = dryRun ? " (dry run)" : "";
80
- console.log(chalk.green(`✓ ${pkgDir}/${result.outputFile} (${result.content.length} bytes${suffix})`));
74
+ const results = await generateForPackage(pkgDir, config, dryRun);
75
+ for (const result of results) {
76
+ if (result.error) {
77
+ console.error(chalk.red(`✗ ${pkgDir}: ${result.error}`));
78
+ errorCount++;
79
+ } else {
80
+ const suffix = dryRun ? " (dry run)" : "";
81
+ console.log(chalk.green(`✓ ${pkgDir}/${result.outputFile} (${result.content.length} bytes${suffix})`));
82
+ }
81
83
  }
82
84
  }
83
85
 
@@ -94,14 +96,19 @@ export async function generateCommand(options: GenerateCommandOptions): Promise<
94
96
  if (dryRun) {
95
97
  console.log(chalk.yellow("⚠ Dry run — no files will be written"));
96
98
  }
97
- console.log(chalk.blue(`→ Generating CLAUDE.md for package: ${options.package}`));
98
- const result = await generateForPackage(packageDir, config, dryRun);
99
- if (result.error) {
100
- console.error(chalk.red(`✗ ${result.error}`));
101
- process.exit(1);
99
+ console.log(chalk.blue(`→ Generating agent files for package: ${options.package}`));
100
+ const pkgResults = await generateForPackage(packageDir, config, dryRun);
101
+ let pkgHasError = false;
102
+ for (const result of pkgResults) {
103
+ if (result.error) {
104
+ console.error(chalk.red(`✗ ${result.error}`));
105
+ pkgHasError = true;
106
+ } else {
107
+ const suffix = dryRun ? " (dry run)" : "";
108
+ console.log(chalk.green(`✓ ${options.package}/${result.outputFile} (${result.content.length} bytes${suffix})`));
109
+ }
102
110
  }
103
- const suffix = dryRun ? " (dry run)" : "";
104
- console.log(chalk.green(`✓ ${options.package}/${result.outputFile} (${result.content.length} bytes${suffix})`));
111
+ if (pkgHasError) process.exit(1);
105
112
  return;
106
113
  }
107
114
 
@@ -183,8 +190,8 @@ export async function generateCommand(options: GenerateCommandOptions): Promise<
183
190
  console.log(chalk.blue("→ Generating configs for all agents..."));
184
191
  }
185
192
 
186
- const allResults = await generateAll(genOptions, config);
187
- const results = agentFilter ? allResults.filter((r) => agentFilter.includes(r.agent as AgentType)) : allResults;
193
+ // Pass agentFilter to generateAll so only matching agents are written to disk
194
+ const results = await generateAll(genOptions, config, agentFilter ?? undefined);
188
195
 
189
196
  let errorCount = 0;
190
197
 
@@ -205,22 +212,24 @@ export async function generateCommand(options: GenerateCommandOptions): Promise<
205
212
  process.exit(1);
206
213
  }
207
214
 
208
- // Auto-generate per-package CLAUDE.md when packages with nax/context.md are discovered
215
+ // Auto-generate per-package agent files when packages with nax/context.md are discovered
209
216
  const packages = await discoverPackages(workdir);
210
217
  if (packages.length > 0) {
211
218
  console.log(
212
- chalk.blue(`\n→ Discovered ${packages.length} package(s) with nax/context.md — generating CLAUDE.md...`),
219
+ chalk.blue(`\n→ Discovered ${packages.length} package(s) with nax/context.md — generating agent files...`),
213
220
  );
214
221
  let pkgErrorCount = 0;
215
222
  for (const pkgDir of packages) {
216
- const result = await generateForPackage(pkgDir, config, dryRun);
217
- if (result.error) {
218
- console.error(chalk.red(`✗ ${pkgDir}: ${result.error}`));
219
- pkgErrorCount++;
220
- } else {
221
- const suffix = dryRun ? " (dry run)" : "";
222
- const rel = pkgDir.startsWith(workdir) ? pkgDir.slice(workdir.length + 1) : pkgDir;
223
- console.log(chalk.green(`✓ ${rel}/${result.outputFile} (${result.content.length} bytes${suffix})`));
223
+ const pkgResults = await generateForPackage(pkgDir, config, dryRun);
224
+ for (const result of pkgResults) {
225
+ if (result.error) {
226
+ console.error(chalk.red(`✗ ${pkgDir}: ${result.error}`));
227
+ pkgErrorCount++;
228
+ } else {
229
+ const suffix = dryRun ? " (dry run)" : "";
230
+ const rel = pkgDir.startsWith(workdir) ? pkgDir.slice(workdir.length + 1) : pkgDir;
231
+ console.log(chalk.green(`✓ ${rel}/${result.outputFile} (${result.content.length} bytes${suffix})`));
232
+ }
224
233
  }
225
234
  }
226
235
  if (pkgErrorCount > 0) {
package/src/cli/init.ts CHANGED
@@ -51,7 +51,6 @@ const NAX_GITIGNORE_ENTRIES = [
51
51
  "nax/features/*/acceptance-refined.json",
52
52
  ".nax-pids",
53
53
  ".nax-wt/",
54
- "~/",
55
54
  ];
56
55
 
57
56
  /**
@@ -100,15 +100,26 @@ async function generateFor(agent: AgentType, options: GenerateOptions, config: N
100
100
  }
101
101
 
102
102
  /**
103
- * Generate configs for all agents.
103
+ * Generate configs for all agents (or a filtered subset).
104
+ *
105
+ * @param agentFilter - Optional list of agent names to generate. When provided,
106
+ * only those agents are written to disk. When omitted, all agents are generated.
104
107
  */
105
- async function generateAll(options: GenerateOptions, config: NaxConfig): Promise<GenerationResult[]> {
108
+ async function generateAll(
109
+ options: GenerateOptions,
110
+ config: NaxConfig,
111
+ agentFilter?: AgentType[],
112
+ ): Promise<GenerationResult[]> {
106
113
  // Load context once and share across generators
107
114
  const context = await loadContextContent(options, config);
108
115
 
109
116
  const results: GenerationResult[] = [];
110
117
 
111
- for (const [agentKey, generator] of Object.entries(GENERATORS) as [AgentType, AgentContextGenerator][]) {
118
+ const entries = (Object.entries(GENERATORS) as [AgentType, AgentContextGenerator][]).filter(
119
+ ([agentKey]) => !agentFilter || agentFilter.length === 0 || agentFilter.includes(agentKey),
120
+ );
121
+
122
+ for (const [agentKey, generator] of entries) {
112
123
  try {
113
124
  const content = generator.generate(context);
114
125
  const outputPath = join(options.outputDir, generator.outputFile);
@@ -262,51 +273,70 @@ export async function discoverWorkspacePackages(repoRoot: string): Promise<strin
262
273
  }
263
274
 
264
275
  /**
265
- * Generate the claude CLAUDE.md for a specific package.
276
+ * Generate agent config file(s) for a specific package.
277
+ *
278
+ * Reads `<packageDir>/nax/context.md` and writes agent files (e.g. CLAUDE.md,
279
+ * AGENTS.md) into the package directory. Respects `config.generate.agents` — when
280
+ * set, only generates for those agents; defaults to `["claude"]` when unset.
281
+ *
282
+ * Per-package files contain only package-specific content — Claude Code's native
283
+ * directory hierarchy merges root CLAUDE.md + package CLAUDE.md at runtime.
266
284
  *
267
- * Reads `<packageDir>/nax/context.md` and writes `<packageDir>/CLAUDE.md`.
268
- * Per-package CLAUDE.md contains only package-specific content — Claude Code's
269
- * native directory hierarchy merges root CLAUDE.md + package CLAUDE.md at runtime.
285
+ * Returns one result per generated agent.
270
286
  */
271
287
  export async function generateForPackage(
272
288
  packageDir: string,
273
289
  config: NaxConfig,
274
290
  dryRun = false,
275
- ): Promise<PackageGenerationResult> {
291
+ ): Promise<PackageGenerationResult[]> {
276
292
  const contextPath = join(packageDir, "nax", "context.md");
277
293
 
278
294
  if (!existsSync(contextPath)) {
279
- return {
280
- packageDir,
281
- outputFile: "CLAUDE.md",
282
- content: "",
283
- written: false,
284
- error: `context.md not found: ${contextPath}`,
285
- };
295
+ return [
296
+ {
297
+ packageDir,
298
+ outputFile: "CLAUDE.md",
299
+ content: "",
300
+ written: false,
301
+ error: `context.md not found: ${contextPath}`,
302
+ },
303
+ ];
286
304
  }
287
305
 
288
- try {
289
- const options: GenerateOptions = {
290
- contextPath,
291
- outputDir: packageDir,
292
- workdir: packageDir,
293
- dryRun,
294
- autoInject: true,
295
- };
296
-
297
- const result = await generateFor("claude", options, config);
298
-
299
- return {
300
- packageDir,
301
- outputFile: result.outputFile,
302
- content: result.content,
303
- written: result.written,
304
- error: result.error,
305
- };
306
- } catch (err) {
307
- const error = err instanceof Error ? err.message : String(err);
308
- return { packageDir, outputFile: "CLAUDE.md", content: "", written: false, error };
306
+ // Respect config.generate.agents; default to ["claude"] when unset
307
+ const agentsToGenerate: AgentType[] =
308
+ config?.generate?.agents && config.generate.agents.length > 0
309
+ ? (config.generate.agents as AgentType[])
310
+ : ["claude"];
311
+
312
+ const options: GenerateOptions = {
313
+ contextPath,
314
+ outputDir: packageDir,
315
+ workdir: packageDir,
316
+ dryRun,
317
+ autoInject: true,
318
+ };
319
+
320
+ const results: PackageGenerationResult[] = [];
321
+
322
+ for (const agent of agentsToGenerate) {
323
+ try {
324
+ const result = await generateFor(agent, options, config);
325
+ results.push({
326
+ packageDir,
327
+ outputFile: result.outputFile,
328
+ content: result.content,
329
+ written: result.written,
330
+ error: result.error,
331
+ });
332
+ } catch (err) {
333
+ const error = err instanceof Error ? err.message : String(err);
334
+ const fallbackFile = GENERATORS[agent]?.outputFile ?? `${agent}.md`;
335
+ results.push({ packageDir, outputFile: fallbackFile, content: "", written: false, error });
336
+ }
309
337
  }
338
+
339
+ return results;
310
340
  }
311
341
 
312
342
  export { generateFor, generateAll };
@@ -45,6 +45,8 @@ export interface CrashRecoveryContext {
45
45
  getTotalStories?: () => number;
46
46
  getStoriesCompleted?: () => number;
47
47
  emitError?: (reason: string) => void;
48
+ /** Called during graceful shutdown before process.exit — use to close ACP sessions etc. */
49
+ onShutdown?: () => Promise<void>;
48
50
  }
49
51
 
50
52
  let handlersInstalled = false;
@@ -15,6 +15,8 @@ export interface SignalHandlerContext extends RunCompleteContext {
15
15
  pidRegistry?: PidRegistry;
16
16
  featureDir?: string;
17
17
  emitError?: (reason: string) => void;
18
+ /** Called during graceful shutdown (signal/exception) before process.exit — use to close ACP sessions etc. */
19
+ onShutdown?: () => Promise<void>;
18
20
  }
19
21
 
20
22
  /**
@@ -46,6 +48,11 @@ function createSignalHandler(ctx: SignalHandlerContext): (signal: NodeJS.Signals
46
48
  await ctx.pidRegistry.killAll();
47
49
  }
48
50
 
51
+ // Close any open ACP sessions before exiting (prevents orphaned acpx processes)
52
+ if (ctx.onShutdown) {
53
+ await ctx.onShutdown().catch(() => {});
54
+ }
55
+
49
56
  ctx.emitError?.(signal.toLowerCase());
50
57
 
51
58
  await writeFatalLog(ctx.jsonlFilePath, signal);
@@ -72,6 +79,10 @@ function createUncaughtExceptionHandler(ctx: SignalHandlerContext): (error: Erro
72
79
  await ctx.pidRegistry.killAll();
73
80
  }
74
81
 
82
+ if (ctx.onShutdown) {
83
+ await ctx.onShutdown().catch(() => {});
84
+ }
85
+
75
86
  ctx.emitError?.("uncaughtException");
76
87
  await writeFatalLog(ctx.jsonlFilePath, "uncaughtException", error);
77
88
  await updateStatusToCrashed(
@@ -102,6 +113,10 @@ function createUnhandledRejectionHandler(ctx: SignalHandlerContext): (reason: un
102
113
  await ctx.pidRegistry.killAll();
103
114
  }
104
115
 
116
+ if (ctx.onShutdown) {
117
+ await ctx.onShutdown().catch(() => {});
118
+ }
119
+
105
120
  ctx.emitError?.("unhandledRejection");
106
121
  await writeFatalLog(ctx.jsonlFilePath, "unhandledRejection", error);
107
122
  await updateStatusToCrashed(
@@ -130,6 +130,11 @@ export async function setupRun(options: RunSetupOptions): Promise<RunSetupResult
130
130
  emitError: (reason: string) => {
131
131
  pipelineEventBus.emit({ type: "run:errored", reason, feature: options.feature });
132
132
  },
133
+ // Close open ACP sessions on SIGINT/SIGTERM so acpx processes don't stay alive
134
+ onShutdown: async () => {
135
+ const { sweepFeatureSessions } = await import("../../agents/acp/adapter");
136
+ await sweepFeatureSessions(workdir, feature).catch(() => {});
137
+ },
133
138
  });
134
139
 
135
140
  // Load PRD (before try block so it's accessible in finally for onRunEnd)
@@ -25,6 +25,7 @@
25
25
  * ```
26
26
  */
27
27
 
28
+ import { join } from "node:path";
28
29
  import { getAgent } from "../../agents/registry";
29
30
  import type { NaxConfig } from "../../config";
30
31
  import { isGreenfieldStory } from "../../context/greenfield";
@@ -140,13 +141,18 @@ export const routingStage: PipelineStage = {
140
141
  }
141
142
 
142
143
  // BUG-010: Greenfield detection — force test-after if no test files exist
144
+ // MW-011: For monorepo stories, scan the story's package workdir (story.workdir), not the
145
+ // repo root. Scanning the repo root would find tests in OTHER packages and incorrectly
146
+ // classify the story as non-greenfield even when the target package has zero tests.
143
147
  const greenfieldDetectionEnabled = ctx.config.tdd.greenfieldDetection ?? true;
144
148
  if (greenfieldDetectionEnabled && routing.testStrategy.startsWith("three-session-tdd")) {
145
- const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, ctx.workdir);
149
+ const greenfieldScanDir = ctx.story.workdir ? join(ctx.workdir, ctx.story.workdir) : ctx.workdir;
150
+ const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, greenfieldScanDir);
146
151
  if (isGreenfield) {
147
152
  logger.info("routing", "Greenfield detected — forcing test-after strategy", {
148
153
  storyId: ctx.story.id,
149
154
  originalStrategy: routing.testStrategy,
155
+ scanDir: greenfieldScanDir,
150
156
  });
151
157
  routing.testStrategy = "test-after";
152
158
  routing.reasoning = `${routing.reasoning} [GREENFIELD OVERRIDE: No test files exist, using test-after instead of TDD]`;
@@ -40,6 +40,7 @@ const NAX_RUNTIME_PATTERNS = [
40
40
  /^.{2} nax\.lock$/,
41
41
  /^.{2} nax\/metrics\.json$/,
42
42
  /^.{2} nax\/features\/[^/]+\/status\.json$/,
43
+ /^.{2} nax\/features\/[^/]+\/prd\.json$/,
43
44
  /^.{2} nax\/features\/[^/]+\/runs\//,
44
45
  /^.{2} nax\/features\/[^/]+\/plan\//,
45
46
  /^.{2} nax\/features\/[^/]+\/acp-sessions\.json$/,