@nathapp/nax 0.48.3 → 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.3",
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("fa27043"))
22287
- return "fa27043";
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 = [];
@@ -33389,10 +33443,10 @@ var init_parallel_executor = __esm(() => {
33389
33443
  // src/pipeline/subscribers/events-writer.ts
33390
33444
  import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
33391
33445
  import { homedir as homedir7 } from "os";
33392
- import { basename as basename4, join as join46 } from "path";
33446
+ import { basename as basename5, join as join46 } from "path";
33393
33447
  function wireEventsWriter(bus, feature, runId, workdir) {
33394
33448
  const logger = getSafeLogger();
33395
- const project = basename4(workdir);
33449
+ const project = basename5(workdir);
33396
33450
  const eventsDir = join46(homedir7(), ".nax", "events", project);
33397
33451
  const eventsFile = join46(eventsDir, "events.jsonl");
33398
33452
  let dirReady = false;
@@ -33554,10 +33608,10 @@ var init_interaction2 = __esm(() => {
33554
33608
  // src/pipeline/subscribers/registry.ts
33555
33609
  import { mkdir as mkdir3, writeFile } from "fs/promises";
33556
33610
  import { homedir as homedir8 } from "os";
33557
- import { basename as basename5, join as join47 } from "path";
33611
+ import { basename as basename6, join as join47 } from "path";
33558
33612
  function wireRegistry(bus, feature, runId, workdir) {
33559
33613
  const logger = getSafeLogger();
33560
- const project = basename5(workdir);
33614
+ const project = basename6(workdir);
33561
33615
  const runDir = join47(homedir8(), ".nax", "runs", `${project}-${feature}-${runId}`);
33562
33616
  const metaFile = join47(runDir, "meta.json");
33563
33617
  const unsub = bus.on("run:started", (_ev) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.48.3",
3
+ "version": "0.48.4",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
  };
@@ -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}`);