@nathapp/nax 0.48.3 → 0.49.0
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 +306 -189
- package/package.json +1 -1
- package/src/acceptance/generator.ts +4 -5
- package/src/cli/prompts-main.ts +1 -0
- package/src/config/merge.ts +55 -9
- package/src/execution/iteration-runner.ts +15 -0
- package/src/execution/lifecycle/acceptance-loop.ts +2 -0
- package/src/execution/parallel-coordinator.ts +1 -0
- package/src/execution/parallel-executor-rectify.ts +1 -0
- package/src/execution/parallel-worker.ts +1 -0
- package/src/execution/sequential-executor.ts +1 -0
- package/src/pipeline/stages/acceptance.ts +6 -2
- package/src/pipeline/stages/autofix.ts +15 -7
- package/src/pipeline/stages/execution.ts +6 -0
- package/src/pipeline/stages/prompt.ts +5 -2
- package/src/pipeline/stages/rectify.ts +4 -2
- package/src/pipeline/stages/regression.ts +10 -6
- package/src/pipeline/stages/review.ts +11 -7
- package/src/pipeline/stages/verify.ts +92 -24
- package/src/pipeline/types.ts +7 -0
- package/src/review/runner.ts +20 -5
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
* This is a lightweight verification before the full review stage.
|
|
6
6
|
*
|
|
7
7
|
* @returns
|
|
8
|
-
* - `continue`: Tests passed
|
|
9
|
-
* - `escalate`:
|
|
8
|
+
* - `continue`: Tests passed, OR TEST_FAILURE (ctx.verifyResult.success===false → rectifyStage handles it)
|
|
9
|
+
* - `escalate`: TIMEOUT or RUNTIME_CRASH (structural — rectify can't fix these)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { loadConfigForWorkdir } from "../../config/loader";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
14
13
|
import type { SmartTestRunnerConfig } from "../../config/types";
|
|
15
14
|
import { getLogger } from "../../logger";
|
|
16
15
|
import { logTestOutput } from "../../utils/log-test-output";
|
|
@@ -18,6 +17,7 @@ import { detectRuntimeCrash } from "../../verification/crash-detector";
|
|
|
18
17
|
import type { VerifyStatus } from "../../verification/orchestrator-types";
|
|
19
18
|
import { regression } from "../../verification/runners";
|
|
20
19
|
import { _smartRunnerDeps } from "../../verification/smart-runner";
|
|
20
|
+
import { isMonorepoOrchestratorCommand } from "../../verification/strategies/scoped";
|
|
21
21
|
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
22
22
|
|
|
23
23
|
const DEFAULT_SMART_RUNNER_CONFIG: SmartTestRunnerConfig = {
|
|
@@ -47,6 +47,41 @@ function buildScopedCommand(testFiles: string[], baseCommand: string, testScoped
|
|
|
47
47
|
return _smartRunnerDeps.buildSmartTestCommand(testFiles, baseCommand);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Read the npm package name from <dir>/package.json.
|
|
52
|
+
* Returns null if not found or file has no name field.
|
|
53
|
+
*/
|
|
54
|
+
async function readPackageName(dir: string): Promise<string | null> {
|
|
55
|
+
try {
|
|
56
|
+
const content = await Bun.file(join(dir, "package.json")).json();
|
|
57
|
+
return typeof content.name === "string" ? content.name : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Substitute {{package}} placeholder in a testScoped template.
|
|
65
|
+
*
|
|
66
|
+
* Reads the npm package name from <packageDir>/package.json.
|
|
67
|
+
* Returns null when package.json is absent or has no name field — callers
|
|
68
|
+
* should skip the template entirely in that case (non-JS/non-Node projects
|
|
69
|
+
* have no package identity to inject, so don't fall back to a dir name guess).
|
|
70
|
+
*
|
|
71
|
+
* @param template - Template string (e.g. "bunx turbo test --filter={{package}}")
|
|
72
|
+
* @param packageDir - Absolute path to the package directory
|
|
73
|
+
* @returns Resolved template, or null if {{package}} cannot be resolved
|
|
74
|
+
*/
|
|
75
|
+
async function resolvePackageTemplate(template: string, packageDir: string): Promise<string | null> {
|
|
76
|
+
if (!template.includes("{{package}}")) return template;
|
|
77
|
+
const name = await _verifyDeps.readPackageName(packageDir);
|
|
78
|
+
if (name === null) {
|
|
79
|
+
// No package.json or no name field — skip template, can't resolve {{package}}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return template.replaceAll("{{package}}", name);
|
|
83
|
+
}
|
|
84
|
+
|
|
50
85
|
export const verifyStage: PipelineStage = {
|
|
51
86
|
name: "verify",
|
|
52
87
|
enabled: (ctx: PipelineContext) => !ctx.fullSuiteGatePassed,
|
|
@@ -55,10 +90,9 @@ export const verifyStage: PipelineStage = {
|
|
|
55
90
|
async execute(ctx: PipelineContext): Promise<StageResult> {
|
|
56
91
|
const logger = getLogger();
|
|
57
92
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: ctx.config;
|
|
93
|
+
// PKG-003: use centrally resolved effective config (set once per story in iteration-runner)
|
|
94
|
+
// Falls back to ctx.config for contexts that predate PKG-003 (e.g., acceptance-loop)
|
|
95
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
62
96
|
|
|
63
97
|
// Skip verification if tests are not required
|
|
64
98
|
if (!effectiveConfig.quality.requireTests) {
|
|
@@ -82,10 +116,38 @@ export const verifyStage: PipelineStage = {
|
|
|
82
116
|
// Determine effective test command (smart runner or full suite)
|
|
83
117
|
let effectiveCommand = testCommand;
|
|
84
118
|
let isFullSuite = true;
|
|
85
|
-
const smartRunnerConfig = coerceSmartTestRunner(
|
|
86
|
-
const regressionMode =
|
|
119
|
+
const smartRunnerConfig = coerceSmartTestRunner(effectiveConfig.execution.smartTestRunner);
|
|
120
|
+
const regressionMode = effectiveConfig.execution.regressionGate?.mode ?? "deferred";
|
|
87
121
|
|
|
88
|
-
|
|
122
|
+
// Resolve {{package}} in testScoped template for monorepo stories.
|
|
123
|
+
// Returns null if package.json is absent (non-JS project) — falls through to smart-runner.
|
|
124
|
+
let resolvedTestScopedTemplate: string | undefined = testScopedTemplate;
|
|
125
|
+
if (testScopedTemplate && ctx.story.workdir) {
|
|
126
|
+
const resolved = await resolvePackageTemplate(testScopedTemplate, effectiveWorkdir);
|
|
127
|
+
resolvedTestScopedTemplate = resolved ?? undefined; // null → skip template
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Monorepo orchestrators (turbo, nx) handle change-aware scoping natively via their own
|
|
131
|
+
// filter syntax. Skip nax's smart runner — appending file paths would produce invalid syntax.
|
|
132
|
+
// Instead, use the testScoped template (with {{package}} resolved) to scope per-story.
|
|
133
|
+
const isMonorepoOrchestrator = isMonorepoOrchestratorCommand(testCommand);
|
|
134
|
+
|
|
135
|
+
if (isMonorepoOrchestrator) {
|
|
136
|
+
if (resolvedTestScopedTemplate && ctx.story.workdir) {
|
|
137
|
+
// Use the resolved scoped template (e.g. "bunx turbo test --filter=@koda/cli")
|
|
138
|
+
effectiveCommand = resolvedTestScopedTemplate;
|
|
139
|
+
isFullSuite = false;
|
|
140
|
+
logger.info("verify", "Monorepo orchestrator — using testScoped template", {
|
|
141
|
+
storyId: ctx.story.id,
|
|
142
|
+
command: effectiveCommand,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
logger.info("verify", "Monorepo orchestrator — running full suite (no package context)", {
|
|
146
|
+
storyId: ctx.story.id,
|
|
147
|
+
command: effectiveCommand,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else if (smartRunnerConfig.enabled) {
|
|
89
151
|
// MW-006: pass packagePrefix so git diff is scoped to the package in monorepos
|
|
90
152
|
const sourceFiles = await _smartRunnerDeps.getChangedSourceFiles(
|
|
91
153
|
effectiveWorkdir,
|
|
@@ -99,7 +161,7 @@ export const verifyStage: PipelineStage = {
|
|
|
99
161
|
logger.info("verify", `[smart-runner] Pass 1: path convention matched ${pass1Files.length} test files`, {
|
|
100
162
|
storyId: ctx.story.id,
|
|
101
163
|
});
|
|
102
|
-
effectiveCommand = buildScopedCommand(pass1Files, testCommand,
|
|
164
|
+
effectiveCommand = buildScopedCommand(pass1Files, testCommand, resolvedTestScopedTemplate);
|
|
103
165
|
isFullSuite = false;
|
|
104
166
|
} else if (smartRunnerConfig.fallback === "import-grep") {
|
|
105
167
|
// Pass 2: import-grep fallback
|
|
@@ -112,7 +174,7 @@ export const verifyStage: PipelineStage = {
|
|
|
112
174
|
logger.info("verify", `[smart-runner] Pass 2: import-grep matched ${pass2Files.length} test files`, {
|
|
113
175
|
storyId: ctx.story.id,
|
|
114
176
|
});
|
|
115
|
-
effectiveCommand = buildScopedCommand(pass2Files, testCommand,
|
|
177
|
+
effectiveCommand = buildScopedCommand(pass2Files, testCommand, resolvedTestScopedTemplate);
|
|
116
178
|
isFullSuite = false;
|
|
117
179
|
}
|
|
118
180
|
}
|
|
@@ -143,8 +205,8 @@ export const verifyStage: PipelineStage = {
|
|
|
143
205
|
const result = await _verifyDeps.regression({
|
|
144
206
|
workdir: effectiveWorkdir,
|
|
145
207
|
command: effectiveCommand,
|
|
146
|
-
timeoutSeconds:
|
|
147
|
-
acceptOnTimeout:
|
|
208
|
+
timeoutSeconds: effectiveConfig.execution.verificationTimeoutSeconds,
|
|
209
|
+
acceptOnTimeout: effectiveConfig.execution.regressionGate?.acceptOnTimeout ?? true,
|
|
148
210
|
});
|
|
149
211
|
|
|
150
212
|
// Store result on context for rectify stage
|
|
@@ -172,7 +234,7 @@ export const verifyStage: PipelineStage = {
|
|
|
172
234
|
if (!result.success) {
|
|
173
235
|
// BUG-019: Distinguish timeout from actual test failures
|
|
174
236
|
if (result.status === "TIMEOUT") {
|
|
175
|
-
const timeout =
|
|
237
|
+
const timeout = effectiveConfig.execution.verificationTimeoutSeconds;
|
|
176
238
|
logger.error(
|
|
177
239
|
"verify",
|
|
178
240
|
`Test suite exceeded timeout (${timeout}s). This is NOT a test failure — consider increasing execution.verificationTimeoutSeconds or scoping tests.`,
|
|
@@ -195,13 +257,19 @@ export const verifyStage: PipelineStage = {
|
|
|
195
257
|
logTestOutput(logger, "verify", result.output, { storyId: ctx.story.id });
|
|
196
258
|
}
|
|
197
259
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
260
|
+
// RUNTIME_CRASH and TIMEOUT are structural — escalate immediately (rectify can't fix them)
|
|
261
|
+
if (result.status === "TIMEOUT" || detectRuntimeCrash(result.output)) {
|
|
262
|
+
return {
|
|
263
|
+
action: "escalate",
|
|
264
|
+
reason:
|
|
265
|
+
result.status === "TIMEOUT"
|
|
266
|
+
? `Test suite TIMEOUT after ${effectiveConfig.execution.verificationTimeoutSeconds}s (not a code failure)`
|
|
267
|
+
: `Tests failed with runtime crash (exit code ${result.status ?? "non-zero"})`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// TEST_FAILURE: ctx.verifyResult is set with success:false — rectifyStage handles it next
|
|
272
|
+
return { action: "continue" };
|
|
205
273
|
}
|
|
206
274
|
|
|
207
275
|
logger.info("verify", "Tests passed", { storyId: ctx.story.id });
|
|
@@ -214,5 +282,5 @@ export const verifyStage: PipelineStage = {
|
|
|
214
282
|
*/
|
|
215
283
|
export const _verifyDeps = {
|
|
216
284
|
regression,
|
|
217
|
-
|
|
285
|
+
readPackageName,
|
|
218
286
|
};
|
package/src/pipeline/types.ts
CHANGED
|
@@ -58,6 +58,13 @@ export type AgentGetFn = (name: string) => import("../agents/types").AgentAdapte
|
|
|
58
58
|
export interface PipelineContext {
|
|
59
59
|
/** Ngent configuration */
|
|
60
60
|
config: NaxConfig;
|
|
61
|
+
/**
|
|
62
|
+
* Resolved config for this story's package.
|
|
63
|
+
* When story.workdir is set, this is root config merged with package config.
|
|
64
|
+
* When no workdir, this equals ctx.config (root).
|
|
65
|
+
* Set once per story in the iteration runner before pipeline execution.
|
|
66
|
+
*/
|
|
67
|
+
effectiveConfig: NaxConfig;
|
|
61
68
|
/** Full PRD document */
|
|
62
69
|
prd: PRD;
|
|
63
70
|
/** Current story (or batch leader) */
|
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}`);
|