@nathapp/nax 0.48.2 → 0.48.4

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
@@ -22210,7 +22210,7 @@ var package_default;
22210
22210
  var init_package = __esm(() => {
22211
22211
  package_default = {
22212
22212
  name: "@nathapp/nax",
22213
- version: "0.48.2",
22213
+ version: "0.48.4",
22214
22214
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22215
22215
  type: "module",
22216
22216
  bin: {
@@ -22283,8 +22283,8 @@ var init_version = __esm(() => {
22283
22283
  NAX_VERSION = package_default.version;
22284
22284
  NAX_COMMIT = (() => {
22285
22285
  try {
22286
- if (/^[0-9a-f]{6,10}$/.test("c1ac720"))
22287
- return "c1ac720";
22286
+ if (/^[0-9a-f]{6,10}$/.test("59e08c0"))
22287
+ return "59e08c0";
22288
22288
  } catch {}
22289
22289
  try {
22290
22290
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -24397,8 +24397,23 @@ async function runReview(config2, workdir, executionConfig) {
24397
24397
  const checks3 = [];
24398
24398
  let firstFailure;
24399
24399
  const allUncommittedFiles = await _deps4.getUncommittedFiles(workdir);
24400
- const NAX_RUNTIME_FILES = new Set(["nax/status.json", ".nax-verifier-verdict.json"]);
24401
- const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_FILES.has(f) && !f.match(/^nax\/features\/.+\/prd\.json$/));
24400
+ const NAX_RUNTIME_PATTERNS = [
24401
+ /nax\.lock$/,
24402
+ /nax\/metrics\.json$/,
24403
+ /nax\/status\.json$/,
24404
+ /nax\/features\/[^/]+\/status\.json$/,
24405
+ /nax\/features\/[^/]+\/prd\.json$/,
24406
+ /nax\/features\/[^/]+\/runs\//,
24407
+ /nax\/features\/[^/]+\/plan\//,
24408
+ /nax\/features\/[^/]+\/acp-sessions\.json$/,
24409
+ /nax\/features\/[^/]+\/interactions\//,
24410
+ /nax\/features\/[^/]+\/progress\.txt$/,
24411
+ /nax\/features\/[^/]+\/acceptance-refined\.json$/,
24412
+ /\.nax-verifier-verdict\.json$/,
24413
+ /\.nax-pids$/,
24414
+ /\.nax-wt\//
24415
+ ];
24416
+ const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(f)));
24402
24417
  if (uncommittedFiles.length > 0) {
24403
24418
  const fileList = uncommittedFiles.join(", ");
24404
24419
  logger?.warn("review", `Uncommitted changes detected before review: ${fileList}`);
@@ -29551,6 +29566,23 @@ function buildScopedCommand2(testFiles, baseCommand, testScopedTemplate) {
29551
29566
  }
29552
29567
  return _smartRunnerDeps.buildSmartTestCommand(testFiles, baseCommand);
29553
29568
  }
29569
+ async function readPackageName(dir) {
29570
+ try {
29571
+ const content = await Bun.file(join26(dir, "package.json")).json();
29572
+ return typeof content.name === "string" ? content.name : null;
29573
+ } catch {
29574
+ return null;
29575
+ }
29576
+ }
29577
+ async function resolvePackageTemplate(template, packageDir) {
29578
+ if (!template.includes("{{package}}"))
29579
+ return template;
29580
+ const name = await _verifyDeps.readPackageName(packageDir);
29581
+ if (name === null) {
29582
+ return null;
29583
+ }
29584
+ return template.replaceAll("{{package}}", name);
29585
+ }
29554
29586
  var DEFAULT_SMART_RUNNER_CONFIG2, verifyStage, _verifyDeps;
29555
29587
  var init_verify = __esm(() => {
29556
29588
  init_loader2();
@@ -29558,6 +29590,7 @@ var init_verify = __esm(() => {
29558
29590
  init_crash_detector();
29559
29591
  init_runners();
29560
29592
  init_smart_runner();
29593
+ init_scoped();
29561
29594
  DEFAULT_SMART_RUNNER_CONFIG2 = {
29562
29595
  enabled: true,
29563
29596
  testFilePatterns: ["test/**/*.test.ts"],
@@ -29586,14 +29619,34 @@ var init_verify = __esm(() => {
29586
29619
  let isFullSuite = true;
29587
29620
  const smartRunnerConfig = coerceSmartTestRunner(ctx.config.execution.smartTestRunner);
29588
29621
  const regressionMode = ctx.config.execution.regressionGate?.mode ?? "deferred";
29589
- if (smartRunnerConfig.enabled) {
29622
+ let resolvedTestScopedTemplate = testScopedTemplate;
29623
+ if (testScopedTemplate && ctx.story.workdir) {
29624
+ const resolved = await resolvePackageTemplate(testScopedTemplate, effectiveWorkdir);
29625
+ resolvedTestScopedTemplate = resolved ?? undefined;
29626
+ }
29627
+ const isMonorepoOrchestrator = isMonorepoOrchestratorCommand(testCommand);
29628
+ if (isMonorepoOrchestrator) {
29629
+ if (resolvedTestScopedTemplate && ctx.story.workdir) {
29630
+ effectiveCommand = resolvedTestScopedTemplate;
29631
+ isFullSuite = false;
29632
+ logger.info("verify", "Monorepo orchestrator \u2014 using testScoped template", {
29633
+ storyId: ctx.story.id,
29634
+ command: effectiveCommand
29635
+ });
29636
+ } else {
29637
+ logger.info("verify", "Monorepo orchestrator \u2014 running full suite (no package context)", {
29638
+ storyId: ctx.story.id,
29639
+ command: effectiveCommand
29640
+ });
29641
+ }
29642
+ } else if (smartRunnerConfig.enabled) {
29590
29643
  const sourceFiles = await _smartRunnerDeps.getChangedSourceFiles(effectiveWorkdir, ctx.storyGitRef, ctx.story.workdir);
29591
29644
  const pass1Files = await _smartRunnerDeps.mapSourceToTests(sourceFiles, effectiveWorkdir);
29592
29645
  if (pass1Files.length > 0) {
29593
29646
  logger.info("verify", `[smart-runner] Pass 1: path convention matched ${pass1Files.length} test files`, {
29594
29647
  storyId: ctx.story.id
29595
29648
  });
29596
- effectiveCommand = buildScopedCommand2(pass1Files, testCommand, testScopedTemplate);
29649
+ effectiveCommand = buildScopedCommand2(pass1Files, testCommand, resolvedTestScopedTemplate);
29597
29650
  isFullSuite = false;
29598
29651
  } else if (smartRunnerConfig.fallback === "import-grep") {
29599
29652
  const pass2Files = await _smartRunnerDeps.importGrepFallback(sourceFiles, effectiveWorkdir, smartRunnerConfig.testFilePatterns);
@@ -29601,7 +29654,7 @@ var init_verify = __esm(() => {
29601
29654
  logger.info("verify", `[smart-runner] Pass 2: import-grep matched ${pass2Files.length} test files`, {
29602
29655
  storyId: ctx.story.id
29603
29656
  });
29604
- effectiveCommand = buildScopedCommand2(pass2Files, testCommand, testScopedTemplate);
29657
+ effectiveCommand = buildScopedCommand2(pass2Files, testCommand, resolvedTestScopedTemplate);
29605
29658
  isFullSuite = false;
29606
29659
  }
29607
29660
  }
@@ -29668,7 +29721,8 @@ var init_verify = __esm(() => {
29668
29721
  };
29669
29722
  _verifyDeps = {
29670
29723
  regression,
29671
- loadConfigForWorkdir
29724
+ loadConfigForWorkdir,
29725
+ readPackageName
29672
29726
  };
29673
29727
  });
29674
29728
 
@@ -29755,7 +29809,7 @@ __export(exports_init_context, {
29755
29809
  });
29756
29810
  import { existsSync as existsSync20 } from "fs";
29757
29811
  import { mkdir } from "fs/promises";
29758
- import { basename, join as join30 } from "path";
29812
+ import { basename as basename2, join as join30 } from "path";
29759
29813
  async function findFiles(dir, maxFiles = 200) {
29760
29814
  try {
29761
29815
  const proc = Bun.spawnSync([
@@ -29843,7 +29897,7 @@ async function scanProject(projectRoot) {
29843
29897
  const readmeSnippet = await readReadmeSnippet(projectRoot);
29844
29898
  const entryPoints = await detectEntryPoints(projectRoot);
29845
29899
  const configFiles = await detectConfigFiles(projectRoot);
29846
- const projectName = packageManifest?.name || basename(projectRoot);
29900
+ const projectName = packageManifest?.name || basename2(projectRoot);
29847
29901
  return {
29848
29902
  projectName,
29849
29903
  fileTree,
@@ -30352,11 +30406,11 @@ function getSafeLogger6() {
30352
30406
  return getSafeLogger();
30353
30407
  }
30354
30408
  function extractPluginName(pluginPath) {
30355
- const basename3 = path12.basename(pluginPath);
30356
- if (basename3 === "index.ts" || basename3 === "index.js" || basename3 === "index.mjs") {
30409
+ const basename4 = path12.basename(pluginPath);
30410
+ if (basename4 === "index.ts" || basename4 === "index.js" || basename4 === "index.mjs") {
30357
30411
  return path12.basename(path12.dirname(pluginPath));
30358
30412
  }
30359
- return basename3.replace(/\.(ts|js|mjs)$/, "");
30413
+ return basename4.replace(/\.(ts|js|mjs)$/, "");
30360
30414
  }
30361
30415
  async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins) {
30362
30416
  const loadedPlugins = [];
@@ -30597,6 +30651,7 @@ var init_checks_git = __esm(() => {
30597
30651
  /^.{2} nax\.lock$/,
30598
30652
  /^.{2} nax\/metrics\.json$/,
30599
30653
  /^.{2} nax\/features\/[^/]+\/status\.json$/,
30654
+ /^.{2} nax\/features\/[^/]+\/prd\.json$/,
30600
30655
  /^.{2} nax\/features\/[^/]+\/runs\//,
30601
30656
  /^.{2} nax\/features\/[^/]+\/plan\//,
30602
30657
  /^.{2} nax\/features\/[^/]+\/acp-sessions\.json$/,
@@ -31659,6 +31714,9 @@ function createSignalHandler(ctx) {
31659
31714
  if (ctx.pidRegistry) {
31660
31715
  await ctx.pidRegistry.killAll();
31661
31716
  }
31717
+ if (ctx.onShutdown) {
31718
+ await ctx.onShutdown().catch(() => {});
31719
+ }
31662
31720
  ctx.emitError?.(signal.toLowerCase());
31663
31721
  await writeFatalLog(ctx.jsonlFilePath, signal);
31664
31722
  await writeRunComplete(ctx, signal.toLowerCase());
@@ -31677,6 +31735,9 @@ function createUncaughtExceptionHandler(ctx) {
31677
31735
  if (ctx.pidRegistry) {
31678
31736
  await ctx.pidRegistry.killAll();
31679
31737
  }
31738
+ if (ctx.onShutdown) {
31739
+ await ctx.onShutdown().catch(() => {});
31740
+ }
31680
31741
  ctx.emitError?.("uncaughtException");
31681
31742
  await writeFatalLog(ctx.jsonlFilePath, "uncaughtException", error48);
31682
31743
  await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "uncaughtException", ctx.featureDir);
@@ -31694,6 +31755,9 @@ function createUnhandledRejectionHandler(ctx) {
31694
31755
  if (ctx.pidRegistry) {
31695
31756
  await ctx.pidRegistry.killAll();
31696
31757
  }
31758
+ if (ctx.onShutdown) {
31759
+ await ctx.onShutdown().catch(() => {});
31760
+ }
31697
31761
  ctx.emitError?.("unhandledRejection");
31698
31762
  await writeFatalLog(ctx.jsonlFilePath, "unhandledRejection", error48);
31699
31763
  await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "unhandledRejection", ctx.featureDir);
@@ -33379,10 +33443,10 @@ var init_parallel_executor = __esm(() => {
33379
33443
  // src/pipeline/subscribers/events-writer.ts
33380
33444
  import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
33381
33445
  import { homedir as homedir7 } from "os";
33382
- import { basename as basename4, join as join46 } from "path";
33446
+ import { basename as basename5, join as join46 } from "path";
33383
33447
  function wireEventsWriter(bus, feature, runId, workdir) {
33384
33448
  const logger = getSafeLogger();
33385
- const project = basename4(workdir);
33449
+ const project = basename5(workdir);
33386
33450
  const eventsDir = join46(homedir7(), ".nax", "events", project);
33387
33451
  const eventsFile = join46(eventsDir, "events.jsonl");
33388
33452
  let dirReady = false;
@@ -33544,10 +33608,10 @@ var init_interaction2 = __esm(() => {
33544
33608
  // src/pipeline/subscribers/registry.ts
33545
33609
  import { mkdir as mkdir3, writeFile } from "fs/promises";
33546
33610
  import { homedir as homedir8 } from "os";
33547
- import { basename as basename5, join as join47 } from "path";
33611
+ import { basename as basename6, join as join47 } from "path";
33548
33612
  function wireRegistry(bus, feature, runId, workdir) {
33549
33613
  const logger = getSafeLogger();
33550
- const project = basename5(workdir);
33614
+ const project = basename6(workdir);
33551
33615
  const runDir = join47(homedir8(), ".nax", "runs", `${project}-${feature}-${runId}`);
33552
33616
  const metaFile = join47(runDir, "meta.json");
33553
33617
  const unsub = bus.on("run:started", (_ev) => {
@@ -34965,6 +35029,10 @@ async function setupRun(options) {
34965
35029
  getStoriesCompleted: options.getStoriesCompleted,
34966
35030
  emitError: (reason) => {
34967
35031
  pipelineEventBus.emit({ type: "run:errored", reason, feature: options.feature });
35032
+ },
35033
+ onShutdown: async () => {
35034
+ const { sweepFeatureSessions: sweepFeatureSessions2 } = await Promise.resolve().then(() => (init_adapter2(), exports_adapter));
35035
+ await sweepFeatureSessions2(workdir, feature).catch(() => {});
34968
35036
  }
34969
35037
  });
34970
35038
  let prd = await loadPRD(prdPath);
@@ -66820,10 +66888,11 @@ async function generateFor(agent, options, config2) {
66820
66888
  return { agent, outputFile: generator.outputFile, content: "", written: false, error: error48 };
66821
66889
  }
66822
66890
  }
66823
- async function generateAll(options, config2) {
66891
+ async function generateAll(options, config2, agentFilter) {
66824
66892
  const context = await loadContextContent(options, config2);
66825
66893
  const results = [];
66826
- for (const [agentKey, generator] of Object.entries(GENERATORS)) {
66894
+ const entries = Object.entries(GENERATORS).filter(([agentKey]) => !agentFilter || agentFilter.length === 0 || agentFilter.includes(agentKey));
66895
+ for (const [agentKey, generator] of entries) {
66827
66896
  try {
66828
66897
  const content = generator.generate(context);
66829
66898
  const outputPath = join11(options.outputDir, generator.outputFile);
@@ -66925,34 +66994,42 @@ async function discoverWorkspacePackages(repoRoot) {
66925
66994
  async function generateForPackage(packageDir, config2, dryRun = false) {
66926
66995
  const contextPath = join11(packageDir, "nax", "context.md");
66927
66996
  if (!existsSync10(contextPath)) {
66928
- return {
66929
- packageDir,
66930
- outputFile: "CLAUDE.md",
66931
- content: "",
66932
- written: false,
66933
- error: `context.md not found: ${contextPath}`
66934
- };
66997
+ return [
66998
+ {
66999
+ packageDir,
67000
+ outputFile: "CLAUDE.md",
67001
+ content: "",
67002
+ written: false,
67003
+ error: `context.md not found: ${contextPath}`
67004
+ }
67005
+ ];
66935
67006
  }
66936
- try {
66937
- const options = {
66938
- contextPath,
66939
- outputDir: packageDir,
66940
- workdir: packageDir,
66941
- dryRun,
66942
- autoInject: true
66943
- };
66944
- const result = await generateFor("claude", options, config2);
66945
- return {
66946
- packageDir,
66947
- outputFile: result.outputFile,
66948
- content: result.content,
66949
- written: result.written,
66950
- error: result.error
66951
- };
66952
- } catch (err) {
66953
- const error48 = err instanceof Error ? err.message : String(err);
66954
- return { packageDir, outputFile: "CLAUDE.md", content: "", written: false, error: error48 };
67007
+ const agentsToGenerate = config2?.generate?.agents && config2.generate.agents.length > 0 ? config2.generate.agents : ["claude"];
67008
+ const options = {
67009
+ contextPath,
67010
+ outputDir: packageDir,
67011
+ workdir: packageDir,
67012
+ dryRun,
67013
+ autoInject: true
67014
+ };
67015
+ const results = [];
67016
+ for (const agent of agentsToGenerate) {
67017
+ try {
67018
+ const result = await generateFor(agent, options, config2);
67019
+ results.push({
67020
+ packageDir,
67021
+ outputFile: result.outputFile,
67022
+ content: result.content,
67023
+ written: result.written,
67024
+ error: result.error
67025
+ });
67026
+ } catch (err) {
67027
+ const error48 = err instanceof Error ? err.message : String(err);
67028
+ const fallbackFile = GENERATORS[agent]?.outputFile ?? `${agent}.md`;
67029
+ results.push({ packageDir, outputFile: fallbackFile, content: "", written: false, error: error48 });
67030
+ }
66955
67031
  }
67032
+ return results;
66956
67033
  }
66957
67034
 
66958
67035
  // src/cli/plan.ts
@@ -68773,16 +68850,18 @@ async function generateCommand(options) {
68773
68850
  console.log(source_default.yellow(" No packages found (no */nax/context.md or */*/nax/context.md)"));
68774
68851
  return;
68775
68852
  }
68776
- console.log(source_default.blue(`\u2192 Generating CLAUDE.md for ${packages.length} package(s)...`));
68853
+ console.log(source_default.blue(`\u2192 Generating agent files for ${packages.length} package(s)...`));
68777
68854
  let errorCount = 0;
68778
68855
  for (const pkgDir of packages) {
68779
- const result = await generateForPackage(pkgDir, config2, dryRun);
68780
- if (result.error) {
68781
- console.error(source_default.red(`\u2717 ${pkgDir}: ${result.error}`));
68782
- errorCount++;
68783
- } else {
68784
- const suffix = dryRun ? " (dry run)" : "";
68785
- console.log(source_default.green(`\u2713 ${pkgDir}/${result.outputFile} (${result.content.length} bytes${suffix})`));
68856
+ const results = await generateForPackage(pkgDir, config2, dryRun);
68857
+ for (const result of results) {
68858
+ if (result.error) {
68859
+ console.error(source_default.red(`\u2717 ${pkgDir}: ${result.error}`));
68860
+ errorCount++;
68861
+ } else {
68862
+ const suffix = dryRun ? " (dry run)" : "";
68863
+ console.log(source_default.green(`\u2713 ${pkgDir}/${result.outputFile} (${result.content.length} bytes${suffix})`));
68864
+ }
68786
68865
  }
68787
68866
  }
68788
68867
  if (errorCount > 0) {
@@ -68797,14 +68876,20 @@ async function generateCommand(options) {
68797
68876
  if (dryRun) {
68798
68877
  console.log(source_default.yellow("\u26A0 Dry run \u2014 no files will be written"));
68799
68878
  }
68800
- console.log(source_default.blue(`\u2192 Generating CLAUDE.md for package: ${options.package}`));
68801
- const result = await generateForPackage(packageDir, config2, dryRun);
68802
- if (result.error) {
68803
- console.error(source_default.red(`\u2717 ${result.error}`));
68804
- process.exit(1);
68879
+ console.log(source_default.blue(`\u2192 Generating agent files for package: ${options.package}`));
68880
+ const pkgResults = await generateForPackage(packageDir, config2, dryRun);
68881
+ let pkgHasError = false;
68882
+ for (const result of pkgResults) {
68883
+ if (result.error) {
68884
+ console.error(source_default.red(`\u2717 ${result.error}`));
68885
+ pkgHasError = true;
68886
+ } else {
68887
+ const suffix = dryRun ? " (dry run)" : "";
68888
+ console.log(source_default.green(`\u2713 ${options.package}/${result.outputFile} (${result.content.length} bytes${suffix})`));
68889
+ }
68805
68890
  }
68806
- const suffix = dryRun ? " (dry run)" : "";
68807
- console.log(source_default.green(`\u2713 ${options.package}/${result.outputFile} (${result.content.length} bytes${suffix})`));
68891
+ if (pkgHasError)
68892
+ process.exit(1);
68808
68893
  return;
68809
68894
  }
68810
68895
  const contextPath = options.context ? join34(workdir, options.context) : join34(workdir, "nax/context.md");
@@ -68859,8 +68944,7 @@ async function generateCommand(options) {
68859
68944
  } else {
68860
68945
  console.log(source_default.blue("\u2192 Generating configs for all agents..."));
68861
68946
  }
68862
- const allResults = await generateAll(genOptions, config2);
68863
- const results = agentFilter ? allResults.filter((r) => agentFilter.includes(r.agent)) : allResults;
68947
+ const results = await generateAll(genOptions, config2, agentFilter ?? undefined);
68864
68948
  let errorCount = 0;
68865
68949
  for (const result of results) {
68866
68950
  if (result.error) {
@@ -68879,17 +68963,19 @@ async function generateCommand(options) {
68879
68963
  const packages = await discoverPackages(workdir);
68880
68964
  if (packages.length > 0) {
68881
68965
  console.log(source_default.blue(`
68882
- \u2192 Discovered ${packages.length} package(s) with nax/context.md \u2014 generating CLAUDE.md...`));
68966
+ \u2192 Discovered ${packages.length} package(s) with nax/context.md \u2014 generating agent files...`));
68883
68967
  let pkgErrorCount = 0;
68884
68968
  for (const pkgDir of packages) {
68885
- const result = await generateForPackage(pkgDir, config2, dryRun);
68886
- if (result.error) {
68887
- console.error(source_default.red(`\u2717 ${pkgDir}: ${result.error}`));
68888
- pkgErrorCount++;
68889
- } else {
68890
- const suffix = dryRun ? " (dry run)" : "";
68891
- const rel = pkgDir.startsWith(workdir) ? pkgDir.slice(workdir.length + 1) : pkgDir;
68892
- console.log(source_default.green(`\u2713 ${rel}/${result.outputFile} (${result.content.length} bytes${suffix})`));
68969
+ const pkgResults = await generateForPackage(pkgDir, config2, dryRun);
68970
+ for (const result of pkgResults) {
68971
+ if (result.error) {
68972
+ console.error(source_default.red(`\u2717 ${pkgDir}: ${result.error}`));
68973
+ pkgErrorCount++;
68974
+ } else {
68975
+ const suffix = dryRun ? " (dry run)" : "";
68976
+ const rel = pkgDir.startsWith(workdir) ? pkgDir.slice(workdir.length + 1) : pkgDir;
68977
+ console.log(source_default.green(`\u2713 ${rel}/${result.outputFile} (${result.content.length} bytes${suffix})`));
68978
+ }
68893
68979
  }
68894
68980
  }
68895
68981
  if (pkgErrorCount > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.48.2",
3
+ "version": "0.48.4",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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)
@@ -9,7 +9,7 @@
9
9
  * - `escalate`: Tests failed (retry with escalation)
10
10
  */
11
11
 
12
- import { join } from "node:path";
12
+ import { basename, join } from "node:path";
13
13
  import { loadConfigForWorkdir } from "../../config/loader";
14
14
  import type { SmartTestRunnerConfig } from "../../config/types";
15
15
  import { getLogger } from "../../logger";
@@ -18,6 +18,7 @@ import { detectRuntimeCrash } from "../../verification/crash-detector";
18
18
  import type { VerifyStatus } from "../../verification/orchestrator-types";
19
19
  import { regression } from "../../verification/runners";
20
20
  import { _smartRunnerDeps } from "../../verification/smart-runner";
21
+ import { isMonorepoOrchestratorCommand } from "../../verification/strategies/scoped";
21
22
  import type { PipelineContext, PipelineStage, StageResult } from "../types";
22
23
 
23
24
  const DEFAULT_SMART_RUNNER_CONFIG: SmartTestRunnerConfig = {
@@ -47,6 +48,41 @@ function buildScopedCommand(testFiles: string[], baseCommand: string, testScoped
47
48
  return _smartRunnerDeps.buildSmartTestCommand(testFiles, baseCommand);
48
49
  }
49
50
 
51
+ /**
52
+ * Read the npm package name from <dir>/package.json.
53
+ * Returns null if not found or file has no name field.
54
+ */
55
+ async function readPackageName(dir: string): Promise<string | null> {
56
+ try {
57
+ const content = await Bun.file(join(dir, "package.json")).json();
58
+ return typeof content.name === "string" ? content.name : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Substitute {{package}} placeholder in a testScoped template.
66
+ *
67
+ * Reads the npm package name from <packageDir>/package.json.
68
+ * Returns null when package.json is absent or has no name field — callers
69
+ * should skip the template entirely in that case (non-JS/non-Node projects
70
+ * have no package identity to inject, so don't fall back to a dir name guess).
71
+ *
72
+ * @param template - Template string (e.g. "bunx turbo test --filter={{package}}")
73
+ * @param packageDir - Absolute path to the package directory
74
+ * @returns Resolved template, or null if {{package}} cannot be resolved
75
+ */
76
+ async function resolvePackageTemplate(template: string, packageDir: string): Promise<string | null> {
77
+ if (!template.includes("{{package}}")) return template;
78
+ const name = await _verifyDeps.readPackageName(packageDir);
79
+ if (name === null) {
80
+ // No package.json or no name field — skip template, can't resolve {{package}}
81
+ return null;
82
+ }
83
+ return template.replaceAll("{{package}}", name);
84
+ }
85
+
50
86
  export const verifyStage: PipelineStage = {
51
87
  name: "verify",
52
88
  enabled: (ctx: PipelineContext) => !ctx.fullSuiteGatePassed,
@@ -85,7 +121,35 @@ export const verifyStage: PipelineStage = {
85
121
  const smartRunnerConfig = coerceSmartTestRunner(ctx.config.execution.smartTestRunner);
86
122
  const regressionMode = ctx.config.execution.regressionGate?.mode ?? "deferred";
87
123
 
88
- if (smartRunnerConfig.enabled) {
124
+ // Resolve {{package}} in testScoped template for monorepo stories.
125
+ // Returns null if package.json is absent (non-JS project) — falls through to smart-runner.
126
+ let resolvedTestScopedTemplate: string | undefined = testScopedTemplate;
127
+ if (testScopedTemplate && ctx.story.workdir) {
128
+ const resolved = await resolvePackageTemplate(testScopedTemplate, effectiveWorkdir);
129
+ resolvedTestScopedTemplate = resolved ?? undefined; // null → skip template
130
+ }
131
+
132
+ // Monorepo orchestrators (turbo, nx) handle change-aware scoping natively via their own
133
+ // filter syntax. Skip nax's smart runner — appending file paths would produce invalid syntax.
134
+ // Instead, use the testScoped template (with {{package}} resolved) to scope per-story.
135
+ const isMonorepoOrchestrator = isMonorepoOrchestratorCommand(testCommand);
136
+
137
+ if (isMonorepoOrchestrator) {
138
+ if (resolvedTestScopedTemplate && ctx.story.workdir) {
139
+ // Use the resolved scoped template (e.g. "bunx turbo test --filter=@koda/cli")
140
+ effectiveCommand = resolvedTestScopedTemplate;
141
+ isFullSuite = false;
142
+ logger.info("verify", "Monorepo orchestrator — using testScoped template", {
143
+ storyId: ctx.story.id,
144
+ command: effectiveCommand,
145
+ });
146
+ } else {
147
+ logger.info("verify", "Monorepo orchestrator — running full suite (no package context)", {
148
+ storyId: ctx.story.id,
149
+ command: effectiveCommand,
150
+ });
151
+ }
152
+ } else if (smartRunnerConfig.enabled) {
89
153
  // MW-006: pass packagePrefix so git diff is scoped to the package in monorepos
90
154
  const sourceFiles = await _smartRunnerDeps.getChangedSourceFiles(
91
155
  effectiveWorkdir,
@@ -99,7 +163,7 @@ export const verifyStage: PipelineStage = {
99
163
  logger.info("verify", `[smart-runner] Pass 1: path convention matched ${pass1Files.length} test files`, {
100
164
  storyId: ctx.story.id,
101
165
  });
102
- effectiveCommand = buildScopedCommand(pass1Files, testCommand, testScopedTemplate);
166
+ effectiveCommand = buildScopedCommand(pass1Files, testCommand, resolvedTestScopedTemplate);
103
167
  isFullSuite = false;
104
168
  } else if (smartRunnerConfig.fallback === "import-grep") {
105
169
  // Pass 2: import-grep fallback
@@ -112,7 +176,7 @@ export const verifyStage: PipelineStage = {
112
176
  logger.info("verify", `[smart-runner] Pass 2: import-grep matched ${pass2Files.length} test files`, {
113
177
  storyId: ctx.story.id,
114
178
  });
115
- effectiveCommand = buildScopedCommand(pass2Files, testCommand, testScopedTemplate);
179
+ effectiveCommand = buildScopedCommand(pass2Files, testCommand, resolvedTestScopedTemplate);
116
180
  isFullSuite = false;
117
181
  }
118
182
  }
@@ -215,4 +279,5 @@ export const verifyStage: PipelineStage = {
215
279
  export const _verifyDeps = {
216
280
  regression,
217
281
  loadConfigForWorkdir,
282
+ readPackageName,
218
283
  };
@@ -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$/,
@@ -221,11 +221,26 @@ export async function runReview(
221
221
 
222
222
  // RQ-001: Check for uncommitted tracked files before running checks
223
223
  const allUncommittedFiles = await _deps.getUncommittedFiles(workdir);
224
- // Exclude nax runtime files — written by nax itself during the run, not by the agent
225
- const NAX_RUNTIME_FILES = new Set(["nax/status.json", ".nax-verifier-verdict.json"]);
226
- const uncommittedFiles = allUncommittedFiles.filter(
227
- (f) => !NAX_RUNTIME_FILES.has(f) && !f.match(/^nax\/features\/.+\/prd\.json$/),
228
- );
224
+ // Exclude nax runtime files — written by nax itself during the run, not by the agent.
225
+ // Patterns use a suffix match (no leading ^) so they work in both single-package repos
226
+ // (nax/features/…) and monorepos where paths are prefixed (apps/cli/nax/features/…).
227
+ const NAX_RUNTIME_PATTERNS = [
228
+ /nax\.lock$/,
229
+ /nax\/metrics\.json$/,
230
+ /nax\/status\.json$/,
231
+ /nax\/features\/[^/]+\/status\.json$/,
232
+ /nax\/features\/[^/]+\/prd\.json$/,
233
+ /nax\/features\/[^/]+\/runs\//,
234
+ /nax\/features\/[^/]+\/plan\//,
235
+ /nax\/features\/[^/]+\/acp-sessions\.json$/,
236
+ /nax\/features\/[^/]+\/interactions\//,
237
+ /nax\/features\/[^/]+\/progress\.txt$/,
238
+ /nax\/features\/[^/]+\/acceptance-refined\.json$/,
239
+ /\.nax-verifier-verdict\.json$/,
240
+ /\.nax-pids$/,
241
+ /\.nax-wt\//,
242
+ ];
243
+ const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(f)));
229
244
  if (uncommittedFiles.length > 0) {
230
245
  const fileList = uncommittedFiles.join(", ");
231
246
  logger?.warn("review", `Uncommitted changes detected before review: ${fileList}`);