@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 +72 -18
- package/package.json +1 -1
- package/src/pipeline/stages/verify.ts +69 -4
- package/src/review/runner.ts +20 -5
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.
|
|
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("
|
|
22287
|
-
return "
|
|
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
|
|
24401
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 ||
|
|
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
|
|
30356
|
-
if (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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
|
};
|
package/src/review/runner.ts
CHANGED
|
@@ -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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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}`);
|