@nathapp/nax 0.50.1 → 0.50.2

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/README.md CHANGED
@@ -522,12 +522,16 @@ The hermetic requirement is injected into all code-writing prompts (test-writer,
522
522
 
523
523
  ### Configuration
524
524
 
525
+ Configured under `quality.testing` — supports **per-package override** in monorepos.
526
+
525
527
  ```json
526
528
  {
527
- "testing": {
528
- "hermetic": true,
529
- "externalBoundaries": ["claude", "acpx", "redis", "grpc"],
530
- "mockGuidance": "Use injectable deps for CLI spawning, ioredis-mock for Redis"
529
+ "quality": {
530
+ "testing": {
531
+ "hermetic": true,
532
+ "externalBoundaries": ["claude", "acpx", "redis", "grpc"],
533
+ "mockGuidance": "Use injectable deps for CLI spawning, ioredis-mock for Redis"
534
+ }
531
535
  }
532
536
  }
533
537
  ```
@@ -540,7 +544,9 @@ The hermetic requirement is injected into all code-writing prompts (test-writer,
540
544
 
541
545
  > **Tip:** `externalBoundaries` and `mockGuidance` complement `context.md`. nax provides the rule ("mock all I/O"), while `context.md` provides project-specific knowledge ("use `ioredis-mock` for Redis"). Use both for best results.
542
546
 
543
- > **Opt-out:** Set `testing.hermetic: false` if your project requires real integration calls (e.g. live database tests against a local dev container).
547
+ > **Monorepo:** Each package can override `quality.testing` in its own `packages/<name>/nax/config.json`. For example, `packages/api` can specify Redis boundaries while `packages/web` specifies HTTP-only.
548
+
549
+ > **Opt-out:** Set `quality.testing.hermetic: false` if your project requires real integration calls (e.g. live database tests against a local dev container).
544
550
 
545
551
  ---
546
552
 
package/dist/nax.js CHANGED
@@ -17678,7 +17678,7 @@ var init_zod = __esm(() => {
17678
17678
  });
17679
17679
 
17680
17680
  // src/config/schemas.ts
17681
- var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, TestingConfigSchema, DecomposeConfigSchema, NaxConfigSchema;
17681
+ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, NaxConfigSchema;
17682
17682
  var init_schemas3 = __esm(() => {
17683
17683
  init_zod();
17684
17684
  TokenPricingSchema = exports_external.object({
@@ -17818,7 +17818,12 @@ var init_schemas3 = __esm(() => {
17818
17818
  "SENTRY_AUTH_TOKEN",
17819
17819
  "DATADOG_API_KEY"
17820
17820
  ]),
17821
- environmentalEscalationDivisor: exports_external.number().min(1).max(10).default(2)
17821
+ environmentalEscalationDivisor: exports_external.number().min(1).max(10).default(2),
17822
+ testing: exports_external.object({
17823
+ hermetic: exports_external.boolean().default(true),
17824
+ externalBoundaries: exports_external.array(exports_external.string()).optional(),
17825
+ mockGuidance: exports_external.string().optional()
17826
+ }).optional()
17822
17827
  });
17823
17828
  TddConfigSchema = exports_external.object({
17824
17829
  maxRetries: exports_external.number().int().nonnegative(),
@@ -17965,11 +17970,6 @@ var init_schemas3 = __esm(() => {
17965
17970
  message: "Role must be one of: test-writer, implementer, verifier, single-session, tdd-simple"
17966
17971
  }), exports_external.string().min(1, "Override path must be non-empty")).optional()
17967
17972
  });
17968
- TestingConfigSchema = exports_external.object({
17969
- hermetic: exports_external.boolean().default(true),
17970
- externalBoundaries: exports_external.array(exports_external.string()).optional(),
17971
- mockGuidance: exports_external.string().optional()
17972
- });
17973
17973
  DecomposeConfigSchema = exports_external.object({
17974
17974
  trigger: exports_external.enum(["auto", "confirm", "disabled"]).default("auto"),
17975
17975
  maxAcceptanceCriteria: exports_external.number().int().min(1).default(6),
@@ -18000,8 +18000,7 @@ var init_schemas3 = __esm(() => {
18000
18000
  agent: AgentConfigSchema.optional(),
18001
18001
  precheck: PrecheckConfigSchema.optional(),
18002
18002
  prompts: PromptsConfigSchema.optional(),
18003
- decompose: DecomposeConfigSchema.optional(),
18004
- testing: TestingConfigSchema.optional()
18003
+ decompose: DecomposeConfigSchema.optional()
18005
18004
  }).refine((data) => data.version === 1, {
18006
18005
  message: "Invalid version: expected 1",
18007
18006
  path: ["version"]
@@ -18118,7 +18117,10 @@ var init_defaults = __esm(() => {
18118
18117
  "SENTRY_AUTH_TOKEN",
18119
18118
  "DATADOG_API_KEY"
18120
18119
  ],
18121
- environmentalEscalationDivisor: 2
18120
+ environmentalEscalationDivisor: 2,
18121
+ testing: {
18122
+ hermetic: true
18123
+ }
18122
18124
  },
18123
18125
  tdd: {
18124
18126
  maxRetries: 2,
@@ -18206,9 +18208,6 @@ var init_defaults = __esm(() => {
18206
18208
  maxSubstoryComplexity: "medium",
18207
18209
  maxRetries: 2,
18208
18210
  model: "balanced"
18209
- },
18210
- testing: {
18211
- hermetic: true
18212
18211
  }
18213
18212
  };
18214
18213
  });
@@ -20827,7 +20826,8 @@ function mergePackageConfig(root, packageOverride) {
20827
20826
  commands: {
20828
20827
  ...root.quality.commands,
20829
20828
  ...packageOverride.quality?.commands
20830
- }
20829
+ },
20830
+ testing: packageOverride.quality?.testing !== undefined ? { ...root.quality.testing, ...packageOverride.quality.testing } : root.quality.testing
20831
20831
  },
20832
20832
  context: {
20833
20833
  ...root.context,
@@ -22351,7 +22351,7 @@ var package_default;
22351
22351
  var init_package = __esm(() => {
22352
22352
  package_default = {
22353
22353
  name: "@nathapp/nax",
22354
- version: "0.50.1",
22354
+ version: "0.50.2",
22355
22355
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22356
22356
  type: "module",
22357
22357
  bin: {
@@ -22425,8 +22425,8 @@ var init_version = __esm(() => {
22425
22425
  NAX_VERSION = package_default.version;
22426
22426
  NAX_COMMIT = (() => {
22427
22427
  try {
22428
- if (/^[0-9a-f]{6,10}$/.test("5ff4e09"))
22429
- return "5ff4e09";
22428
+ if (/^[0-9a-f]{6,10}$/.test("c3a5edb"))
22429
+ return "c3a5edb";
22430
22430
  } catch {}
22431
22431
  try {
22432
22432
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -27659,13 +27659,13 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
27659
27659
  } else {
27660
27660
  switch (role) {
27661
27661
  case "test-writer":
27662
- prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).hermeticConfig(config2.testing).build();
27662
+ prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).hermeticConfig(config2.quality?.testing).build();
27663
27663
  break;
27664
27664
  case "implementer":
27665
- prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).hermeticConfig(config2.testing).build();
27665
+ prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).hermeticConfig(config2.quality?.testing).build();
27666
27666
  break;
27667
27667
  case "verifier":
27668
- prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).hermeticConfig(config2.testing).build();
27668
+ prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).hermeticConfig(config2.quality?.testing).build();
27669
27669
  break;
27670
27670
  }
27671
27671
  }
@@ -28788,11 +28788,11 @@ var init_prompt = __esm(() => {
28788
28788
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
28789
28789
  let prompt;
28790
28790
  if (isBatch) {
28791
- const builder = PromptBuilder.for("batch").withLoader(ctx.workdir, ctx.config).stories(ctx.stories).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(effectiveConfig.quality?.commands?.test).hermeticConfig(effectiveConfig.testing);
28791
+ const builder = PromptBuilder.for("batch").withLoader(ctx.workdir, ctx.config).stories(ctx.stories).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(effectiveConfig.quality?.commands?.test).hermeticConfig(effectiveConfig.quality?.testing);
28792
28792
  prompt = await builder.build();
28793
28793
  } else {
28794
28794
  const role = "tdd-simple";
28795
- const builder = PromptBuilder.for(role).withLoader(ctx.workdir, ctx.config).story(ctx.story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(effectiveConfig.quality?.commands?.test).hermeticConfig(effectiveConfig.testing);
28795
+ const builder = PromptBuilder.for(role).withLoader(ctx.workdir, ctx.config).story(ctx.story).context(ctx.contextMarkdown).constitution(ctx.constitution?.content).testCommand(effectiveConfig.quality?.commands?.test).hermeticConfig(effectiveConfig.quality?.testing);
28796
28796
  prompt = await builder.build();
28797
28797
  }
28798
28798
  ctx.prompt = prompt;
@@ -69678,10 +69678,10 @@ var FIELD_DESCRIPTIONS = {
69678
69678
  agent: "Agent protocol configuration (ACP-003)",
69679
69679
  "agent.protocol": "Protocol for agent communication: 'acp' | 'cli' (default: 'acp')",
69680
69680
  "agent.maxInteractionTurns": "Max turns in multi-turn interaction loop when interactionBridge is active (default: 10)",
69681
- testing: "Hermetic test enforcement configuration (ENH-010)",
69682
- "testing.hermetic": "Inject hermetic test requirement into prompts \u2014 never call real external services in tests (default: true)",
69683
- "testing.externalBoundaries": "Project-specific CLI tools/clients to mock (e.g. ['claude', 'acpx', 'redis'])",
69684
- "testing.mockGuidance": "Project-specific mocking guidance injected verbatim into the prompt"
69681
+ "quality.testing": "Hermetic test enforcement \u2014 per-package overridable (ENH-010)",
69682
+ "quality.testing.hermetic": "Inject hermetic test requirement into prompts \u2014 never call real external services in tests (default: true)",
69683
+ "quality.testing.externalBoundaries": "Project-specific CLI tools/clients to mock (e.g. ['claude', 'acpx', 'redis'])",
69684
+ "quality.testing.mockGuidance": "Project-specific mocking guidance injected verbatim into the prompt"
69685
69685
  };
69686
69686
 
69687
69687
  // src/cli/config-diff.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.50.1",
3
+ "version": "0.50.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -209,10 +209,10 @@ export const FIELD_DESCRIPTIONS: Record<string, string> = {
209
209
  "agent.protocol": "Protocol for agent communication: 'acp' | 'cli' (default: 'acp')",
210
210
  "agent.maxInteractionTurns":
211
211
  "Max turns in multi-turn interaction loop when interactionBridge is active (default: 10)",
212
- // Testing
213
- testing: "Hermetic test enforcement configuration (ENH-010)",
214
- "testing.hermetic":
212
+ // quality.testing (ENH-010) — per-package overridable
213
+ "quality.testing": "Hermetic test enforcement per-package overridable (ENH-010)",
214
+ "quality.testing.hermetic":
215
215
  "Inject hermetic test requirement into prompts — never call real external services in tests (default: true)",
216
- "testing.externalBoundaries": "Project-specific CLI tools/clients to mock (e.g. ['claude', 'acpx', 'redis'])",
217
- "testing.mockGuidance": "Project-specific mocking guidance injected verbatim into the prompt",
216
+ "quality.testing.externalBoundaries": "Project-specific CLI tools/clients to mock (e.g. ['claude', 'acpx', 'redis'])",
217
+ "quality.testing.mockGuidance": "Project-specific mocking guidance injected verbatim into the prompt",
218
218
  };
@@ -121,6 +121,9 @@ export const DEFAULT_CONFIG: NaxConfig = {
121
121
  "DATADOG_API_KEY",
122
122
  ],
123
123
  environmentalEscalationDivisor: 2,
124
+ testing: {
125
+ hermetic: true,
126
+ },
124
127
  },
125
128
  tdd: {
126
129
  maxRetries: 2,
@@ -211,7 +214,4 @@ export const DEFAULT_CONFIG: NaxConfig = {
211
214
  maxRetries: 2,
212
215
  model: "balanced",
213
216
  },
214
- testing: {
215
- hermetic: true,
216
- },
217
217
  };
@@ -15,7 +15,7 @@ import type { NaxConfig } from "./schema";
15
15
  * - execution: smartTestRunner, regressionGate (deep), verificationTimeoutSeconds
16
16
  * - review: enabled, checks, commands (deep), pluginMode
17
17
  * - acceptance: enabled, generateTests, testPath
18
- * - quality: requireTests, requireTypecheck, requireLint, commands (deep)
18
+ * - quality: requireTests, requireTypecheck, requireLint, commands (deep), testing (deep)
19
19
  * - context: testCoverage (deep)
20
20
  *
21
21
  * All other sections (models, autoMode, routing, agent, generate, tdd,
@@ -89,6 +89,11 @@ export function mergePackageConfig(root: NaxConfig, packageOverride: Partial<Nax
89
89
  ...root.quality.commands,
90
90
  ...packageOverride.quality?.commands,
91
91
  },
92
+ // ENH-010: deep-merge testing config so per-package overrides work
93
+ testing:
94
+ packageOverride.quality?.testing !== undefined
95
+ ? { ...root.quality.testing, ...packageOverride.quality.testing }
96
+ : root.quality.testing,
92
97
  },
93
98
  context: {
94
99
  ...root.context,
@@ -160,6 +160,8 @@ export interface QualityConfig {
160
160
  stripEnvVars: string[];
161
161
  /** Divisor for environmental failure early escalation (default: 2 = half the tier budget) */
162
162
  environmentalEscalationDivisor: number;
163
+ /** Hermetic test enforcement settings (ENH-010). Supports per-package override. */
164
+ testing?: TestingConfig;
163
165
  }
164
166
 
165
167
  /** TDD config */
@@ -495,8 +497,6 @@ export interface NaxConfig {
495
497
  decompose?: DecomposeConfig;
496
498
  /** Agent protocol settings (ACP-003) */
497
499
  agent?: AgentConfig;
498
- /** Hermetic test enforcement settings (ENH-010) */
499
- testing?: TestingConfig;
500
500
  /** Generate settings */
501
501
  generate?: GenerateConfig;
502
502
  }
@@ -175,6 +175,29 @@ const QualityConfigSchema = z.object({
175
175
  "DATADOG_API_KEY",
176
176
  ]),
177
177
  environmentalEscalationDivisor: z.number().min(1).max(10).default(2),
178
+ testing: z
179
+ .object({
180
+ /**
181
+ * When true (default), nax injects a hermetic test requirement into all code-writing prompts.
182
+ * Instructs the AI to mock all I/O boundaries (HTTP, CLI spawning, databases, etc.)
183
+ * and never invoke real external processes or services during test execution.
184
+ * Set to false only if your project requires real integration calls in tests.
185
+ */
186
+ hermetic: z.boolean().default(true),
187
+ /**
188
+ * Project-specific external boundaries the AI should watch for and mock.
189
+ * E.g. ["claude", "acpx", "redis", "grpc"] — any CLI tools, clients, or services
190
+ * the project uses that should never be called from tests.
191
+ */
192
+ externalBoundaries: z.array(z.string()).optional(),
193
+ /**
194
+ * Project-specific guidance on how to mock external dependencies.
195
+ * Injected verbatim into the hermetic requirement section of the prompt.
196
+ * E.g. "Use injectable deps for CLI spawning, ioredis-mock for Redis"
197
+ */
198
+ mockGuidance: z.string().optional(),
199
+ })
200
+ .optional(),
178
201
  });
179
202
 
180
203
  const TddConfigSchema = z.object({
@@ -362,28 +385,6 @@ export const PromptsConfigSchema = z.object({
362
385
  .optional(),
363
386
  });
364
387
 
365
- const TestingConfigSchema = z.object({
366
- /**
367
- * When true (default), nax injects a hermetic test requirement into all code-writing prompts.
368
- * Instructs the AI to mock all I/O boundaries (HTTP, CLI spawning, databases, etc.)
369
- * and never invoke real external processes or services during test execution.
370
- * Set to false only if your project requires real integration calls in tests.
371
- */
372
- hermetic: z.boolean().default(true),
373
- /**
374
- * Project-specific external boundaries the AI should watch for and mock.
375
- * E.g. ["claude", "acpx", "redis", "grpc"] — any CLI tools, clients, or services
376
- * the project uses that should never be called from tests.
377
- */
378
- externalBoundaries: z.array(z.string()).optional(),
379
- /**
380
- * Project-specific guidance on how to mock external dependencies.
381
- * Injected verbatim into the hermetic requirement section of the prompt.
382
- * E.g. "Use injectable deps for CLI spawning, ioredis-mock for Redis"
383
- */
384
- mockGuidance: z.string().optional(),
385
- });
386
-
387
388
  const DecomposeConfigSchema = z.object({
388
389
  trigger: z.enum(["auto", "confirm", "disabled"]).default("auto"),
389
390
  maxAcceptanceCriteria: z.number().int().min(1).default(6),
@@ -417,7 +418,6 @@ export const NaxConfigSchema = z
417
418
  precheck: PrecheckConfigSchema.optional(),
418
419
  prompts: PromptsConfigSchema.optional(),
419
420
  decompose: DecomposeConfigSchema.optional(),
420
- testing: TestingConfigSchema.optional(),
421
421
  })
422
422
  .refine((data) => data.version === 1, {
423
423
  message: "Invalid version: expected 1",
@@ -45,7 +45,7 @@ export const promptStage: PipelineStage = {
45
45
  .context(ctx.contextMarkdown)
46
46
  .constitution(ctx.constitution?.content)
47
47
  .testCommand(effectiveConfig.quality?.commands?.test)
48
- .hermeticConfig(effectiveConfig.testing);
48
+ .hermeticConfig(effectiveConfig.quality?.testing);
49
49
  prompt = await builder.build();
50
50
  } else {
51
51
  // Both test-after and tdd-simple use the tdd-simple prompt (RED/GREEN/REFACTOR)
@@ -56,7 +56,7 @@ export const promptStage: PipelineStage = {
56
56
  .context(ctx.contextMarkdown)
57
57
  .constitution(ctx.constitution?.content)
58
58
  .testCommand(effectiveConfig.quality?.commands?.test)
59
- .hermeticConfig(effectiveConfig.testing);
59
+ .hermeticConfig(effectiveConfig.quality?.testing);
60
60
  prompt = await builder.build();
61
61
  }
62
62
 
@@ -133,7 +133,7 @@ export async function runTddSession(
133
133
  .context(contextMarkdown)
134
134
  .constitution(constitution)
135
135
  .testCommand(config.quality?.commands?.test)
136
- .hermeticConfig(config.testing)
136
+ .hermeticConfig(config.quality?.testing)
137
137
  .build();
138
138
  break;
139
139
  case "implementer":
@@ -143,7 +143,7 @@ export async function runTddSession(
143
143
  .context(contextMarkdown)
144
144
  .constitution(constitution)
145
145
  .testCommand(config.quality?.commands?.test)
146
- .hermeticConfig(config.testing)
146
+ .hermeticConfig(config.quality?.testing)
147
147
  .build();
148
148
  break;
149
149
  case "verifier":
@@ -153,7 +153,7 @@ export async function runTddSession(
153
153
  .context(contextMarkdown)
154
154
  .constitution(constitution)
155
155
  .testCommand(config.quality?.commands?.test)
156
- .hermeticConfig(config.testing)
156
+ .hermeticConfig(config.quality?.testing)
157
157
  .build();
158
158
  break;
159
159
  }