@nathapp/nax 0.48.4 → 0.49.1

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.
@@ -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`: Tests failed (retry with escalation)
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
12
  import { basename, join } from "node:path";
13
- import { loadConfigForWorkdir } from "../../config/loader";
14
13
  import type { SmartTestRunnerConfig } from "../../config/types";
15
14
  import { getLogger } from "../../logger";
16
15
  import { logTestOutput } from "../../utils/log-test-output";
@@ -91,10 +90,9 @@ export const verifyStage: PipelineStage = {
91
90
  async execute(ctx: PipelineContext): Promise<StageResult> {
92
91
  const logger = getLogger();
93
92
 
94
- // MW-009: resolve effective config for per-package test commands
95
- const effectiveConfig = ctx.story.workdir
96
- ? await _verifyDeps.loadConfigForWorkdir(join(ctx.workdir, "nax", "config.json"), ctx.story.workdir)
97
- : 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;
98
96
 
99
97
  // Skip verification if tests are not required
100
98
  if (!effectiveConfig.quality.requireTests) {
@@ -118,8 +116,8 @@ export const verifyStage: PipelineStage = {
118
116
  // Determine effective test command (smart runner or full suite)
119
117
  let effectiveCommand = testCommand;
120
118
  let isFullSuite = true;
121
- const smartRunnerConfig = coerceSmartTestRunner(ctx.config.execution.smartTestRunner);
122
- const regressionMode = ctx.config.execution.regressionGate?.mode ?? "deferred";
119
+ const smartRunnerConfig = coerceSmartTestRunner(effectiveConfig.execution.smartTestRunner);
120
+ const regressionMode = effectiveConfig.execution.regressionGate?.mode ?? "deferred";
123
121
 
124
122
  // Resolve {{package}} in testScoped template for monorepo stories.
125
123
  // Returns null if package.json is absent (non-JS project) — falls through to smart-runner.
@@ -207,8 +205,8 @@ export const verifyStage: PipelineStage = {
207
205
  const result = await _verifyDeps.regression({
208
206
  workdir: effectiveWorkdir,
209
207
  command: effectiveCommand,
210
- timeoutSeconds: ctx.config.execution.verificationTimeoutSeconds,
211
- acceptOnTimeout: ctx.config.execution.regressionGate?.acceptOnTimeout ?? true,
208
+ timeoutSeconds: effectiveConfig.execution.verificationTimeoutSeconds,
209
+ acceptOnTimeout: effectiveConfig.execution.regressionGate?.acceptOnTimeout ?? true,
212
210
  });
213
211
 
214
212
  // Store result on context for rectify stage
@@ -236,7 +234,7 @@ export const verifyStage: PipelineStage = {
236
234
  if (!result.success) {
237
235
  // BUG-019: Distinguish timeout from actual test failures
238
236
  if (result.status === "TIMEOUT") {
239
- const timeout = ctx.config.execution.verificationTimeoutSeconds;
237
+ const timeout = effectiveConfig.execution.verificationTimeoutSeconds;
240
238
  logger.error(
241
239
  "verify",
242
240
  `Test suite exceeded timeout (${timeout}s). This is NOT a test failure — consider increasing execution.verificationTimeoutSeconds or scoping tests.`,
@@ -259,13 +257,19 @@ export const verifyStage: PipelineStage = {
259
257
  logTestOutput(logger, "verify", result.output, { storyId: ctx.story.id });
260
258
  }
261
259
 
262
- return {
263
- action: "escalate",
264
- reason:
265
- result.status === "TIMEOUT"
266
- ? `Test suite TIMEOUT after ${ctx.config.execution.verificationTimeoutSeconds}s (not a code failure)`
267
- : `Tests failed (exit code ${result.status ?? "non-zero"})`,
268
- };
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" };
269
273
  }
270
274
 
271
275
  logger.info("verify", "Tests passed", { storyId: ctx.story.id });
@@ -278,6 +282,5 @@ export const verifyStage: PipelineStage = {
278
282
  */
279
283
  export const _verifyDeps = {
280
284
  regression,
281
- loadConfigForWorkdir,
282
285
  readPackageName,
283
286
  };
@@ -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/utils/git.ts CHANGED
@@ -181,7 +181,11 @@ export async function autoCommitIfDirty(workdir: string, stage: string, role: st
181
181
  return gitRoot;
182
182
  }
183
183
  })();
184
- if (realWorkdir !== realGitRoot) return;
184
+ // Allow: workdir IS the git root, or workdir is a subdirectory (monorepo package)
185
+ // Reject: workdir has no git repo at all (realGitRoot would be empty/error)
186
+ const isAtRoot = realWorkdir === realGitRoot;
187
+ const isSubdir = realGitRoot && realWorkdir.startsWith(`${realGitRoot}/`);
188
+ if (!isAtRoot && !isSubdir) return;
185
189
 
186
190
  const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
187
191
  cwd: workdir,
@@ -199,7 +203,11 @@ export async function autoCommitIfDirty(workdir: string, stage: string, role: st
199
203
  dirtyFiles: statusOutput.trim().split("\n").length,
200
204
  });
201
205
 
202
- const addProc = _gitDeps.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
206
+ // Use "git add ." when workdir is a monorepo package subdir — only stages files under
207
+ // that package, preventing accidental cross-package commits.
208
+ // Use "git add -A" at repo root to capture renames/deletions across the full tree.
209
+ const addArgs = isSubdir ? ["git", "add", "."] : ["git", "add", "-A"];
210
+ const addProc = _gitDeps.spawn(addArgs, { cwd: workdir, stdout: "pipe", stderr: "pipe" });
203
211
  await addProc.exited;
204
212
 
205
213
  const commitProc = _gitDeps.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {